一、分布式鎖簡介
1.什么是分布式鎖
- 當(dāng)在分布式模型下,數(shù)據(jù)只有一份(或有限制)故俐,此時需要利用鎖的技術(shù)控制某一時刻修改數(shù)據(jù)的進程數(shù)想鹰。
- 與單機模式下的鎖不僅需要保證進程可見,還需要考慮進程與鎖之間的網(wǎng)絡(luò)問題药版。
- 分布式鎖還是可以將標(biāo)記存在內(nèi)存辑舷,只是該內(nèi)存不是某個進程分配的內(nèi)存而是公共內(nèi)存如 Redis、Memcache槽片。至于利用數(shù)據(jù)庫何缓、文件等做鎖與單機的實現(xiàn)是一樣的,只要保證標(biāo)記能互斥就行还栓。
2.分布式鎖具備的條件
- 在分布式系統(tǒng)環(huán)境下碌廓,一個方法在同一時間只能被一個機器的一個線程執(zhí)行;
- 高可用的獲取鎖與釋放鎖剩盒;
- 高性能的獲取鎖與釋放鎖谷婆;
- 具備可重入特性;
- 具備鎖失效機制勃刨,防止死鎖波材;
- 具備非阻塞鎖特性股淡,即沒有獲取到鎖將直接返回獲取鎖失敗身隐。
二、采用Redis實現(xiàn)分布式鎖
1.常規(guī)代碼實現(xiàn)
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "product_001";
try {
/*Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "aaa"); //jedis.setnx
stringRedisTemplate.expire(lockKey, 30, TimeUnit.SECONDS); //設(shè)置超時*/
//為解決原子性問題將設(shè)置鎖和設(shè)置超時時間合并
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "aaa", 10, TimeUnit.SECONDS);
//未設(shè)置成功唯灵,當(dāng)前key已經(jīng)存在了贾铝,直接返回錯誤
if (!result) {
return "error_code";
}
//業(yè)務(wù)邏輯實現(xiàn),扣減庫存
....
} catch (Exception e) {
e.printStackTrace();
}finally {
stringRedisTemplate.delete(lockKey);
}
return "end";
}
2.問題分析
上述代碼可以看到埠帕,當(dāng)前鎖的失效時間為10s垢揩,如果當(dāng)前扣減庫存的業(yè)務(wù)邏輯執(zhí)行需要15s時,高并發(fā)時會出現(xiàn)問題:
- 線程1敛瓷,首先執(zhí)行到10s后叁巨,鎖(product_001)失效
- 線程2,在第10s后同樣進入當(dāng)前方法呐籽,此時加上鎖(product_001)
- 當(dāng)執(zhí)行到15s時锋勺,線程1刪除線程2加的鎖(product_001)
- 線程3,可以加鎖 .... 如此循環(huán)狡蝶,實際鎖已經(jīng)沒有意義
a)方案1:當(dāng)前線程刪除當(dāng)前線程所加的鎖
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "product_001";
//定義唯一的客戶端ID
String clientId = UUID.randomUUID().toString();
try {
//為解決原子性問題將設(shè)置鎖和設(shè)置超時時間合并,將clientID作為值放入鎖中
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
//未設(shè)置成功庶橱,當(dāng)前key已經(jīng)存在了,直接返回錯誤
if (!result) {
return "error_code";
}
//業(yè)務(wù)邏輯實現(xiàn)贪惹,扣減庫存
....
} catch (Exception e) {
e.printStackTrace();
}finally {
//只有在獲取鎖的值為當(dāng)前clientId時才會進行刪除鎖操作
if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
這樣能保證每個線程刪除的鎖為當(dāng)前線程添加的鎖苏章,但是 還是會有超賣的問題 :因為 線程1在還沒有執(zhí)行完成的時候,此時鎖已經(jīng)到達過期時間,此時線程2則會加鎖成功
b)方案2:續(xù)命鎖
定義一個子線程枫绅,定時去查看 是否存在主線程的持有當(dāng)前鎖 泉孩,如果 存在則為其延長過期時間。
c)方案3:Redisson
@Autowired
Redisson redisson;
@RequestMapping("/deduct_stock_redisson")
public String deductStockRedisson() {
String lockKey = "product_001";
RLock rlock = redisson.getLock(lockKey);
try {
rlock.lock();
//業(yè)務(wù)邏輯實現(xiàn)并淋,扣減庫存
....
} catch (Exception e) {
e.printStackTrace();
} finally {
rlock.unlock();
}
return "end";
}
- 多個線程去執(zhí)行l(wèi)ock操作预伺,僅有一個線程能夠加鎖成功订咸,其它線程循環(huán)阻塞。
- 加鎖成功酬诀,鎖超時時間 默認30s 脏嚷,并開啟后臺線程,加鎖的后臺會 每隔10秒 去檢測線程持有的鎖是否存在瞒御,還在的話父叙,就延遲鎖超時時間,重新設(shè)置為30s肴裙,即 鎖延期趾唱。
- 對于原子性,Redis分布式鎖底層借助 Lua腳本實現(xiàn)鎖的原子性 蜻懦。鎖延期是通過在底層用Lua進行延時甜癞,延時檢測時間是對超時時間timeout /3
三宛乃、采用Redisson分布式鎖的問題分析
1.主從同步問題
當(dāng)主Redis加鎖了悠咱,開始執(zhí)行線程,若還未將鎖通過異步同步的方式同步到從Redis節(jié)點,主節(jié)點就掛了谆奥,此時會把某一臺從節(jié)點作為新的主節(jié)點眼坏,此時別的線程就可以加鎖了,這樣就出錯了酸些,怎么辦宰译?
a)采用zookeeper代替Redis
由于zk集群的特點,其支持的是CP擂仍。而Redis集群支持的則是AP囤屹。
b)采用RedLock
假設(shè)有3個redis節(jié)點肋坚,這些節(jié)點之間既沒有主從,也沒有集群關(guān)系≈茄幔客戶端用相同的key和隨機值在3個節(jié)點上請求鎖诲泌,請求鎖的超時時間應(yīng)小于鎖自動釋放時間。當(dāng)在2個(超過半數(shù))redis上請求到鎖的時候铣鹏,才算是真正獲取到了鎖敷扫。如果沒有獲取到鎖,則把部分已鎖的redis釋放掉诚卸。
@RequestMapping("/deduct_stock_redlock")
public String deductStockRedlock() {
String lockKey = "product_001";
//TODO 這里需要自己實例化不同redis實例的redisson客戶端連接葵第,這里只是偽代碼用一個redisson客戶端簡化了
RLock rLock1 = redisson.getLock(lockKey);
RLock rLock2 = redisson.getLock(lockKey);
RLock rLock3 = redisson.getLock(lockKey);
// 向3個redis實例嘗試加鎖
RedissonRedLock redLock = new RedissonRedLock(rLock1, rLock2, rLock3);
boolean isLock;
try {
// 500ms拿不到鎖, 就認為獲取鎖失敗。10000ms即10s是鎖失效時間合溺。
isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
System.out.println("isLock = " + isLock);
if (isLock) {
//業(yè)務(wù)邏輯處理
...
}
} catch (Exception e) {
} finally {
// 無論如何, 最后都要解鎖
redLock.unlock();
}
}
具體使用存在爭議卒密,不太推薦使用。 如果考慮高可用并發(fā)推薦使用Redisson棠赛,考慮一致性推薦使用zookeeper 哮奇。
2.提高并發(fā):分段鎖
由于Redisson實際上就是將并行的請求,轉(zhuǎn)化為串行請求睛约。這樣就降低了并發(fā)的響應(yīng)速度鼎俘,為了解決這一問題,可以將鎖進行分段處理:例如秒殺商品001辩涝,原本存在1000個商品贸伐,可以將其分為20段,為每段分配50個商品...
以上就是有關(guān)Redis分布式鎖的學(xué)習(xí)筆記膀值,希望可以對大家學(xué)習(xí)Redis分布式鎖有幫助棍丐,喜歡的小伙伴可以幫忙轉(zhuǎn)發(fā)+關(guān)注,感謝大家沧踏!LZ也會不定時的分享干貨!