一個Redis分布式鎖的實現引發的思考
最近看了一個老項目(2018年的),發現其中用 Redis 來實現分布式鎖??。
代碼如下 ??
// 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);
/** 獲取鎖成功,返回標識鎖的value值,用于釋放鎖確認 */
RedisConnectionUtils.releaseConnection(redisConnection, connectionFactory);
return identifier;
}
/** 返回-1代表key沒有設置超時時間,為key設置一個超時時間 */
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為空表示鎖不存在或已經被釋放*/
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)){
// 業務代碼
redisLock.releaseLock(lockName, lockIdentify);
}
else{
System.out.println("get lock failed.");
}
}
分析
看完之后,有這幾點感悟
- setNX 和 expire 兩個操作是分開的,有一定的風險(忘了釋放鎖,expire 失敗)
- 加鎖時,除了 setNX ,還會去 ttl ,防止死鎖的發生。
- 釋放鎖時,會通過 UUID 去判斷這個鎖的值,避免釋放其他線程加的鎖,但是沒有考慮到這個 get 和 del 是兩個操作,還是會有意外,比如 releaseLock 時,執行完 get ,判斷這個 uuid 是自己的,準備刪除,但此時 鎖過期 了,其他線程剛好加鎖成功,結果又被你刪除了。
- 釋放鎖時沒有在 finally 塊中執行
- 獲取不到鎖時,嘗試自旋等待鎖
再結合 redisson 框架來看的話,就會發現
- 少了 自動續期 的功能,如果業務執行時間較長,鎖過期釋放掉了,就可能出現并發問題。
- 少了 可重入鎖 的功能,可以預見獲取鎖的線程,再次去加鎖也會失敗。
- 少了 lua腳本 ,lua 腳本能保證原子性操作,減少這個網絡開銷。
再把視角移到 Redis 服務器來,就會發現 單點問題 的存在,此時分布式鎖就無法使用了。
這個問題可以通過 主從,哨兵,集群 模式解決,但是又有了一個 故障轉移問題 。
先簡要介紹下這幾個模式
- Redis 主從復制模式:
一主多從,主節點負責寫,并同步到從節點。
從節點負責備份數據,處理讀操作,提供讀負載均衡和故障切換。
- Redis 哨兵模式:
- 主從基礎上增加了哨兵節點(Sentinel),一個獨立進程,去監控所有節點,當主節點宕機時,會從 slave 中選舉出新的主節點,并通知其他從節點更新配置
- 哨兵節點負責執行故障轉移、選舉新的主節點等操作
- Redis 集群模式:
- 多個主從組成,由 master 去瓜分 16384 個 slot, 將數據分片存儲在多個節點上。
- 節點間通過 Gossip 協議進行廣播通信,比如 新節點的加入,主從變更等
回到 分布式鎖 這個話題,通過主從切換,可以實現故障轉移。但是當加鎖成功時,master 掛了,此時還沒同步鎖信息到這個 slave 上,那這個分布式鎖也是失效了。
網上的方案是通過 Redlock(紅鎖) 來解決。
Redlock 的大致意思就是給多個節點加鎖,超過半數成功的話,就認為加鎖成功。
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
// 紅鎖在大部分節點上加鎖成功就算成功。
lock.lock();
...
lock.unlock();
我更偏向于解決這個 主從復制延遲 的問題,比如
- 升級硬件,更好的 CPU,帶寬
- 避免從節點阻塞,比如操作一些 大Key
- 調大 repl_backlog_size 參數,避免全量同步
當然,具體問題具體分析,可以根據業務準備補償措施,但也要避免這個過度設計。
紅鎖爭論
在查閱資料時,看到了這么一個事情 ??
《數據密集型應用系統設計》的作者 Martin 去反駁這個 Redlock ,并用一個進程暫停(GC)的例子,指出了 Redlock 安全性問題:
- 客戶端 1 請求鎖定節點 A、B、C、D、E
- 客戶端 1 的拿到鎖后,進入 GC(時間比較久)
- 所有 Redis 節點上的鎖都過期了
- 客戶端 2 獲取到了 A、B、C、D、E 上的鎖
- 客戶端 1 GC 結束,認為成功獲取鎖
- 客戶端 2 也認為獲取到了鎖,發生「沖突」
圖片
還有 時鐘 漂移的問題
這里我就不過多 CV 了,可以看看原文??
相關文章
《一文講透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,網絡延遲
- P:Process Pause,進程暫停(GC)
- C:Clock Drift,時鐘漂移