聊聊分散式鎖的解決方案
阿新 • • 發佈:2022-04-01
今天來聊聊分散式鎖的解決方案,就從什麼是分散式鎖和分散式鎖的解決方案以及具體實現來進行分析,內容純屬個人見解,如有紕漏及錯誤還請指正!
什麼是分散式鎖
- 傳統的單機應用中,需求在一臺jvm上如果有執行緒併發的安全問題使用jvm自帶的鎖機制就能很好的解決;但是在微服務的分散式部署下理論上會有N臺JVM叢集,顯然單機鎖已經解決不了,那麼如何保證他們對共有資源的訪問安全呢?這就需要引入分散式鎖;分散式鎖同樣能保證在某一時刻內共有資源只被一個應用的某個執行緒所佔用,其他資源無法搶佔,除非該執行緒執行完自己的業務操作主動釋放鎖,那麼分散式鎖有哪些主流解決方案呢?
分散式鎖的三種解決方案
通過資料庫實現
實現思路
- 通過建立一個具有uk、開始時間、過期時間的表,然後業務側拿到資源後將資源id作為uk插入該表,如果插入成功即成功搶佔鎖;為了防止搶佔到鎖的資源宕機導致釋放鎖失敗還需要新建定時任務固定時間內刪除已經失效的鎖記錄
表結構參考
create table LOCK_INFO ( ID VARCHAR2(100), busi_Id VARCHAR2(255), CREATE_TIME DATE, EXPIRE_TIME DATE ); create unique index UK_LOCK on LOCK_INFO (busi_Id); comment on table LOCK_INFO is '分散式鎖資訊表'; comment on column LOCK_INFO.ID is '自增主鍵'; comment on column LOCK_INFO.busi_Id is '共享資源的id'; comment on column LOCK_INFO.CREATE_TIME is '鎖的建立時間'; comment on column LOCK_INFO.EXPIRE_TIME is '鎖的過期時間';
加鎖
insert into LOCK_INFO values('10020201','BUSI_0001',sysdate,sysdate+numtodsinterval(5,'minute'))
說明:加鎖只強調了insert程式碼的sql,實際應用中是需要在程式碼中trycatch捕獲異常的,不然萬一加鎖失敗整個業務都進行不下去了
釋放鎖
- 業務正常執行完畢的釋放
delete from LOCK_INFO where busi_id = 'BUSI_0001';
- 定時任務的釋放
delete from LOCK_INFO where expire_time < sysdate
缺點
- 按照上述思路實現還有一個問題即拿到鎖的程式執行時間大於刪除鎖的定時任務的時間該怎麼辦?這樣會導致定時任務把鎖刪掉了別的程式會搶佔到該鎖但是原程式仍在執行,這就會造成資料不一致的問題了
- 解決方案
當拿鎖的程式執行時間過長開啟非同步程式去資料庫續期即可
實際運用
由於工作中我們的分散式鎖用的就是資料庫實現的,所以這裡聊下我們的業務場景以及具體實現
業務場景
系統內的定時任務需要使用分散式鎖
具體實現
- 1、自定義註解@Lock,需要使用鎖的方法直接加上該註解即可,註解類如下
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Lock {
/**
* 鎖名稱
* @return
*/
String name() default CommonConstant.EMPLY;
/**
* 鎖型別
* @return
*/
String type() default "DB";
}
- 2、利用AOP攔截方法上有@Lock註解的請求,執行加鎖、執行業務、解鎖的操作,關鍵程式碼如下:
@Aspect
@Component
public class LockAop {
@Resource
private LockService lockService;
private final static Logger log = LoggerFactory.getLogger(LockAop.class);
@Pointcut("@annotation(com.darling.annotation.Lock)")
public void lockPointCut(){}
@Around("lockPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature)signature;
Method method = methodSignature.getMethod();
Method targetMethod = point.getTarget().getClass().getMethod(method.getName(), method.getParameterTypes());
String className = method.getDeclaringClass().getName()+"."+method.getName();
Lock lock = targetMethod.getAnnotation(Lock.class);
Object result = null;
if (Objects.nonNull(lock)) {
String name = lock.name();
if (StringUtils.isBlank(name)) {
name = className;
}
try {
lockService.lock(name);
try {
result = point.proceed();
} finally {
lockService.unLock(name);
}
} catch (Exception e) {
log.info("搶佔失敗,name = " + name + e);
}
}
return result;
}
}
說明:LockService裡封裝的就是加減鎖對應的insert、delete操作
- 3、開發定時任務定期刪除已過期的鎖,這裡是五分鐘刪除一次,釋放鎖的邏輯在lockService裡實現
@Scheduled(cron = "0 0/5 * * * ?")
@ScheProfile(value = PRD)
private void autoUnlock() {
lockService.autoUnLock();
}
通過redis實現
實現原理
通過redis的 setnx這個原子操作命令來實現,為了降低資料一致性風險建議設定key的時候新增過期時間,如set busiId 999 nx ex 300;但是需要注意的是業務側一定要保證每次加鎖的命令一定是標準化的,因為如果像上面的命令加鎖成功後其他業務設定了set busiId 888 ex 300不僅能設定成功而且還會覆蓋掉原值,這樣鎖就失效了,如下所示:
優缺點
- 優點:redis基於記憶體所以效率上肯定比操作資料庫快,並且redis可以自己管理過期時間不用寫任務去刪除
- 缺點:單點故障、續期問題
單點故障的解決方案-紅鎖
- 一臺redis的單點故障問題很容易會想到用主從複製的架構來解決,如果當應用在master上拿到鎖執行業務後master掛掉了,那麼其他應用很容易會在slave上拿到鎖,這樣就又破壞了資料的一致性;所以紅鎖的解決方案應運而生;
- 紅鎖即部署由使用者可知個數和順序的redis叢集,叢集內各個例項互不影響,這裡提到的個數建議為奇數個,應用側按照順序依次對叢集內的redis例項進行setnx ex操作,如果成功的例項數大於叢集內總例項數的一半加一時即認為加鎖成功;這種方案明顯提升了redis作為分散式鎖的可靠性,但是顯然增加了維護和部署的難度;
- 缺點:當應用拿到鎖的某個redis例項宕機需要重啟的時候其記憶體是沒有對應key值的,這時候就又會被其他程式搶鎖成功而先搶到鎖的應用還沒有執行完業務;這就又產生了一致性的問題,解決方案就是延緩宕機例項的重啟時間,可以設定1小時或者更久後重啟,理論上1小時先搶到鎖的應用業務肯定也執行完畢了
通過ZK+資料庫樂觀鎖實現
思考 假設應用側是一個部署了10臺JVM應用的叢集,其中一個JVM應用(稱之為A應用)利用紅鎖拿到了鎖去執行業務了,假設該應用A在執行過程中做了一次很長時間的io或者乾脆STW了,時間長到大於redis裡設定的過期時間了,此時redis裡的key過期失效了,而另一個應用B搶到了該鎖去執行任務了,此時應用A恢復正常響應繼續執行業務,那麼就又造成了資料一致性的問題了!
- 上面問題可以通過ZK的臨時順序節點+mysql的樂觀鎖來實現,具體的實現步驟我就不用文字一一描述了,這裡貼上我畫的流程圖: