一個Redis分布式鎖的實現(xiàn)引發(fā)的思考
最近看了一個老項目(2018年的),發(fā)現(xiàn)其中用 Redis 來實現(xiàn)分布式鎖??。
代碼如下 ??
// jedis
public String lock(String lockName, long acquireTimeout) {
return lockWithTimeout(lockName, acquireTimeout, DEFAULT_EXPIRE);
}
public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
RedisConnection redisConnection = connectionFactory.getConnection();
/** 隨機生成一個value */
String identifier = UUID.randomUUID().toString();
String lockKey = LOCK_PREFIX + lockName;
int lockExpire = (int) (timeout / 1000);
long end = System.currentTimeMillis() + acquireTimeout; /** 獲取鎖的超時時間,超過這個時間則放棄獲取鎖 */
while (System.currentTimeMillis() < end) {
if (redisConnection.setNX(lockKey.getBytes(), identifier.getBytes())) {
redisConnection.expire(lockKey.getBytes(), lockExpire);
/** 獲取鎖成功,返回標(biāo)識鎖的value值,用于釋放鎖確認(rèn) */
RedisConnectionUtils.releaseConnection(redisConnection, connectionFactory);
return identifier;
}
/** 返回-1代表key沒有設(shè)置超時時間,為key設(shè)置一個超時時間 */
if (redisConnection.ttl(lockKey.getBytes()) == -1) {
redisConnection.expire(lockKey.getBytes(), lockExpire);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
log.warn("獲取分布式鎖:線程中斷!");
Thread.currentThread().interrupt();
}
}
RedisConnectionUtils.releaseConnection(redisConnection, connectionFactory);
return null;
}
public boolean releaseLock(String lockName, String identifier) {
if (StringUtils.isEmpty(identifier)) return false;
RedisConnectionFactory connectionFactory = redisTemplate.getConnectionFactory();
RedisConnection redisConnection = connectionFactory.getConnection();
String lockKey = LOCK_PREFIX + lockName;
boolean releaseFlag = false;
while (true) {
try {
byte[] valueBytes = redisConnection.get(lockKey.getBytes());
/** value為空表示鎖不存在或已經(jīng)被釋放*/
if (valueBytes == null) {
releaseFlag = false;
break;
}
/** 通過前面返回的value值判斷是不是該鎖,若是該鎖,則刪除,釋放鎖 */
String identifierValue = new String(valueBytes);
if (identifier.equals(identifierValue)) {
redisConnection.del(lockKey.getBytes());
releaseFlag = true;
}
break;
} catch (Exception e) {
log.warn("釋放鎖異常", e);
}
}
RedisConnectionUtils.releaseConnection(redisConnection, connectionFactory);
return releaseFlag;
}
public void lockTest(String lockName, Long acquireTimeout, CouponSummary couponSummary) {
String lockIdentify = redisLock.lock(lockName,acquireTimeout);
if (StringUtils.isNotEmpty(lockIdentify)){
// 業(yè)務(wù)代碼
redisLock.releaseLock(lockName, lockIdentify);
}
else{
System.out.println("get lock failed.");
}
}
分析
看完之后,有這幾點感悟
- setNX 和 expire 兩個操作是分開的,有一定的風(fēng)險(忘了釋放鎖,expire 失?。?/li>
- 加鎖時,除了 setNX ,還會去 ttl ,防止死鎖的發(fā)生。
- 釋放鎖時,會通過 UUID 去判斷這個鎖的值,避免釋放其他線程加的鎖,但是沒有考慮到這個 get 和 del 是兩個操作,還是會有意外,比如 releaseLock 時,執(zhí)行完 get ,判斷這個 uuid 是自己的,準(zhǔn)備刪除,但此時 鎖過期 了,其他線程剛好加鎖成功,結(jié)果又被你刪除了。
- 釋放鎖時沒有在 finally 塊中執(zhí)行
- 獲取不到鎖時,嘗試自旋等待鎖
再結(jié)合 redisson 框架來看的話,就會發(fā)現(xiàn)
- 少了 自動續(xù)期 的功能,如果業(yè)務(wù)執(zhí)行時間較長,鎖過期釋放掉了,就可能出現(xiàn)并發(fā)問題。
- 少了 可重入鎖 的功能,可以預(yù)見獲取鎖的線程,再次去加鎖也會失敗。
- 少了 lua腳本 ,lua 腳本能保證原子性操作,減少這個網(wǎng)絡(luò)開銷。
再把視角移到 Redis 服務(wù)器來,就會發(fā)現(xiàn) 單點問題 的存在,此時分布式鎖就無法使用了。
這個問題可以通過 主從,哨兵,集群 模式解決,但是又有了一個 故障轉(zhuǎn)移問題 。
先簡要介紹下這幾個模式
- Redis 主從復(fù)制模式:
一主多從,主節(jié)點負(fù)責(zé)寫,并同步到從節(jié)點。
從節(jié)點負(fù)責(zé)備份數(shù)據(jù),處理讀操作,提供讀負(fù)載均衡和故障切換。
- Redis 哨兵模式:
- 主從基礎(chǔ)上增加了哨兵節(jié)點(Sentinel),一個獨立進程,去監(jiān)控所有節(jié)點,當(dāng)主節(jié)點宕機時,會從 slave 中選舉出新的主節(jié)點,并通知其他從節(jié)點更新配置
- 哨兵節(jié)點負(fù)責(zé)執(zhí)行故障轉(zhuǎn)移、選舉新的主節(jié)點等操作
- Redis 集群模式:
- 多個主從組成,由 master 去瓜分 16384 個 slot, 將數(shù)據(jù)分片存儲在多個節(jié)點上。
- 節(jié)點間通過 Gossip 協(xié)議進行廣播通信,比如 新節(jié)點的加入,主從變更等
回到 分布式鎖 這個話題,通過主從切換,可以實現(xiàn)故障轉(zhuǎn)移。但是當(dāng)加鎖成功時,master 掛了,此時還沒同步鎖信息到這個 slave 上,那這個分布式鎖也是失效了。
網(wǎng)上的方案是通過 Redlock(紅鎖) 來解決。
Redlock 的大致意思就是給多個節(jié)點加鎖,超過半數(shù)成功的話,就認(rèn)為加鎖成功。
redisson 的紅鎖用法??
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同時加鎖:lock1 lock2 lock3
// 紅鎖在大部分節(jié)點上加鎖成功就算成功。
lock.lock();
...
lock.unlock();
我更偏向于解決這個 主從復(fù)制延遲 的問題,比如
- 升級硬件,更好的 CPU,帶寬
- 避免從節(jié)點阻塞,比如操作一些 大Key
- 調(diào)大 repl_backlog_size 參數(shù),避免全量同步
當(dāng)然,具體問題具體分析,可以根據(jù)業(yè)務(wù)準(zhǔn)備補償措施,但也要避免這個過度設(shè)計。
紅鎖爭論
在查閱資料時,看到了這么一個事情 ??
《數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計》的作者 Martin 去反駁這個 Redlock ,并用一個進程暫停(GC)的例子,指出了 Redlock 安全性問題:
- 客戶端 1 請求鎖定節(jié)點 A、B、C、D、E
- 客戶端 1 的拿到鎖后,進入 GC(時間比較久)
- 所有 Redis 節(jié)點上的鎖都過期了
- 客戶端 2 獲取到了 A、B、C、D、E 上的鎖
- 客戶端 1 GC 結(jié)束,認(rèn)為成功獲取鎖
- 客戶端 2 也認(rèn)為獲取到了鎖,發(fā)生「沖突」
圖片
還有 時鐘 漂移的問題
這里我就不過多 CV 了,可以看看原文??
相關(guān)文章
《一文講透Redis分布式鎖安全問題》:https://cloud.tencent.com/developer/article/2332108
《How to do distributed locking》https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
NPC 異常場景
- N:Network Delay,網(wǎng)絡(luò)延遲
- P:Process Pause,進程暫停(GC)
- C:Clock Drift,時鐘漂移