叢集環境下的秒殺問題
前序
在單機環境下的併發問題,我們可以使用相關鎖來解決;但是在叢集環境中,筆者測試透過Nginx
做的反向代理和負載均衡,請求的時候鎖會出現失效的問題。
原因:我們部署多個服務(存在多個tomcat
伺服器),每個tomcat
都有一個屬於自己的jvm
.每個鎖在同容器中有效,但是跨容器後就無法實現互斥效果。
引出分散式鎖:
- 分散式就是指資料和程式可以不位於一個伺服器上,而是分散到多個伺服器,以網路上分散分佈的地理資訊資料及受其影響的資料庫操作為研究物件的一種理論計算模型。
- 分散式鎖提供了多個伺服器節點訪問共享資源互斥的一種手段。
一個最基本的分散式鎖需要滿足:
- 互斥 :任意一個時刻,鎖只能被一個執行緒持有;
- 高可用 :鎖服務是高可用的。並且,即使客戶端的釋放鎖的程式碼邏輯出現問題,鎖最終一定還是會被釋放,不會影響其他執行緒對共享資源的訪問。
- 可重入:一個節點獲取了鎖之後,還可以再次獲取鎖
分散式鎖的實現:
- 基於redis中的
SETNX
實現分散式鎖 - 基於Zookeeper的節點唯一性和有序性實現互斥的分散式鎖
- 基於MySQL本身的互斥鎖機制
基於Redis的分散式鎖
基本實現
GitHub完整程式碼:https://github.com/xbhog/hm-dianping/tree/20230211-xbhog-redisCloud
鎖介面實現:20230211-xbhog-redisCloud
/**
* @author xbhog
* @describe:
* @date 2023/2/16
*/
public interface ILock {
boolean tryLock(Long timeOutSec);
void unLock();
}
加鎖解鎖實現類:
@Override
public boolean tryLock(Long timeOutSec) {
String threadId = ID_PREFIX + Thread.currentThread().getId();
Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + keyName, threadId + "", timeOutSec, TimeUnit.SECONDS);
//防止拆箱引發空值異常
return Boolean.TRUE.equals(isLock);
}
@Override
public void unlock() {
//透過del刪除鎖
stringRedisTemplate.delete(KEY_PREFIX + name);
}
鎖誤刪問題
現在有兩個鎖,執行緒1獲取鎖時,由於業務的阻塞超時釋放了,這是執行緒2開始操作,獲取鎖,線上程2執行業務期間,執行緒1業務在一段時間內不阻塞且業務完成,這是開始執行釋放鎖的操作,但是這是鎖是執行緒2,由此造成鎖的誤刪問題;
正確流程:
解決的方式:
修改之前的分散式鎖實現,滿足:在獲取鎖時存入執行緒標示(可以用UUID表示) 在釋放鎖時先獲取鎖中的執行緒標示,判斷是否與當前執行緒標示一致
- 如果一致則釋放鎖
- 如果不一致則不釋放鎖
核心邏輯:在存入鎖時,放入自己執行緒的標識,在刪除鎖時,判斷當前這把鎖的標識是不是自己存入的,如果是,則進行刪除,如果不是,則不進行刪除。
處理流程:
程式碼實現:
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(Long timeOutSec) {
String threadId = ID_PREFIX + Thread.currentThread().getId();
Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + keyName, threadId + "", timeOutSec, TimeUnit.SECONDS);
//防止拆箱引發空值異常
return Boolean.TRUE.equals(isLock);
}
@Override
public void unLock() {
String threadId = ID_PREFIX + Thread.currentThread().getId();
//獲取當前分散式鎖中的value
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + keyName);
//鎖相同則刪除
if(threadId.equals(id)){
stringRedisTemplate.delete(KEY_PREFIX + keyName);
}
}