前言
公司交給了萌新小猿一個光榮而艱鉅的項目,該項目需要使用分佈式鎖,這可難道了小猿,只是聽說過分佈式鎖很牛掰,其他就一概不知了,唉不懂就問唄,遂向老闆請教。
老闆:我們每天不都在經歷分佈式鎖嗎,我來給你回憶回憶。
小猿:好勒,瓜子板凳已備好。
本文結構
- 爲什麼要使用分佈式鎖
- 分佈式鎖有哪些特點
- 分佈式鎖流行算法及其優缺點
- 基本算法
- relock算法
- token算法
- 數據庫排它鎖、ZooKeeper分佈式鎖、Google的Chubby分佈式鎖
- 總結
1、爲什麼要使用分佈式鎖
這個問題應該拆分成以下2個問題回答。
1.1、爲什麼使用鎖
保證在同一時刻共享資源只能被一個客戶端訪問;
根據鎖用途分爲以下兩種:
- 共享資源只允許一個客戶端操作;
- 共享資源允許多個客戶端操作;
1.1.1、僅允許一個客戶端訪問
共享資源的操作不具備冪等性。
常見於 數據的修改、刪除操作;
在上面的例子中,
人物事件 | 系統含義 |
---|---|
經理A-N | 多個線程 |
碼農小猿-調高空調溫度 | 非冪等共享資源 |
祕書的允許 | 獲取鎖 |
1.1.2、允許多個客戶端操作
主要應用場景是:共享資源的操作具有冪等性;
如 數據的查詢。
既然都具有冪等性了,爲什麼還需要分佈式鎖呢,通常是爲了效率或性能,避免重複操作(尤其是消耗資源的操作)。例如我們常見的緩存方案。
在上面的例子中,
人物事件 | 系統含義 |
---|---|
經理A-N | 多個線程 |
碼農小猿-整理昨天的資料 | 冪等共享資源 |
祕書的允許 | 獲取鎖 |
自己存資料 | 緩存 |
由於此處的資源是冪等的,通常會將這類資源做緩存,這就是常見的鎖+緩存架構。
常適用於 獲取較爲消耗資源(時間、內存、CPU等)的冪等資源,如:
- 查詢用戶信息;
- 查詢歷史訂單;
當然,如果資源僅在一段時間範圍內具有冪等性,這時候,架構就應該升級了:
鎖+緩存+緩存失效/失效重新獲取/緩存定時更新。
1.2、鎖爲什麼需要分佈式的?
還是以上面的緩存方案爲例,此處略作變化。
人物事件 | 系統含義 |
---|---|
系統A、B | 彼此獨立的系統 |
碼農小猿-調高空調溫度 | 非冪等共享資源 |
李祕書的允許 | 獲取鎖 |
王祕書的允許 | 獲取鎖 |
李祕書、王祕書信息絕對互通 | 單一鎖升級爲分佈式鎖 |
2、高級分佈式鎖有哪些特點?
2.1、互斥性
- 在任意時刻,僅允許有一個客戶端獲得鎖;
PS:如果多個客戶端都能同時獲得鎖,那鎖就沒意義了,共享資源的安全性也就無法保證了。
老闆:當我在會議室接待客戶A時,其他客戶只有等待,你需要等到我空閒了才能把其他人帶到我辦公室。
小猿:明白。
接待客戶(非冪等共享資源);等到老闆空閒(獲取鎖)。
2.2、可重入性
- 客戶端A獲得了鎖,只要鎖沒有過期,客戶端A可以繼續獲得該鎖。
鎖在我這裏,我還要繼續使用,其他人不準搶。
這種特性可以很好的支持【鎖續約】功能。
例如:客戶端A獲取鎖,鎖釋放時間爲10S,即將到達10S時,客戶端A未完成任務,需要再申請5S。若鎖沒有可重入性,客戶端A將無法續約,導致鎖可能被其他客戶端搶走。
小猿:受教了,老闆3分鐘後你還有一場面試。
老闆:小猿啊,難得你這麼好學,我很欣慰,我們的交流時間延10分鐘吧,其他會議延後。
2.3、高性能
- 獲取鎖的效率應該足夠高;
- 總不能讓業務阻塞在獲取鎖上面吧?
小猿:好的,我已在釘釘申請將會議延長10分鐘了;
老闆:嗯,我已經接受會議邀請了;
小猿:老闆你真高效。
2.4、高可用
分佈式、微服務環境下,必須保證服務的高可用,否則輕則影響其他業務模塊,重則引發服務雪崩。
老闆:我手機24小時開機,有會議時聯繫不上我也可以聯繫我祕書。
2.5、支持阻塞和非阻塞式鎖
- 獲取鎖失敗,是直接返回失敗,還是一直阻塞知道獲取成功?
不同的業務場景有不同的答案。
例如:
鎖阻塞性 | 示例 |
---|---|
非阻塞式 | 常見的工單系統,員工A、B同時想操作訂單1(搶單)。當員工A獲得鎖並如願操作訂單1;員工B獲取鎖失敗,不能一直阻塞,應該告知失敗,讓員工B去做其他事,否則員工B就光明正大上班划水了。 |
阻塞式 | 打電話給老闆審覈方案,老闆在通話中(獲取鎖失敗),此時需要每隔一段時間就給老闆打電話,直到聯繫上老闆纔行。誰讓老闆下了死命令今天必須審覈通過呢,嗚嗚嗚。 |
2.6、解鎖權限
- 客戶端僅能釋放(解鎖)自己加的鎖;
常見的解決方案是,給鎖加隨機數(或ThreadID)。
老闆:小猿啊,給你講了這麼多,都明白了嗎?
籠子裏的鸚鵡:明白啦,明白啦。
老闆:閉嘴,我問的是小猿,只有小猿自己有資格回答。
2.7、避免死鎖
- 加鎖方異常終止無法主動釋放鎖;
常規做法是 加鎖時設置超時時間,如果未主動釋放鎖,則利用Redis的自動過期被動釋放鎖。
祕書破門而入:老闆,你們10分鐘的會議已經到點了,隔壁的李總已經等不及了;
老闆:一不留神就忘記時間了,我得去見李總了。
小猿:老闆,我們還沒聊完呢,
2.8、異常處理
- 常見的異常情況有Redis宕機、時鐘跳躍、網絡故障等;
小猿:不管出現哪種情況,我獲取鎖都會失敗啊,這可怎麼辦呢?
PS:這就複雜了,需要根據具體的業務場景分析。對於必須同步處理的業務,則必須失敗告警,對於允許延遲處理的業務可以考慮記錄失敗信息待其他系統處理。
3、分佈式鎖流行算法
3.1、基本方案SETNX
基於Redis的SETNX指令完成鎖的獲取;
3.1.1、獲取鎖 SET lock:resource_name random_value NX PX 30000
lock:resource_name:資源名字,加鎖對象的唯一標記;
random_value:通常存儲加鎖方的唯一標記,如“UUID+ThreadID”;
NX:key不存在才設置,即鎖未被其他人加鎖才能加鎖;
PX:鎖超時時間;
當然,此種加鎖方式是不支持“鎖重入性”的。
3.1.2、釋放鎖(LUA腳本)
checkValueThenDelete:檢查解鎖方是否是加鎖方,是則允許解鎖,否則不允許解鎖;
僞代碼是:
public class RedisTool {
// 釋放鎖成功標記
private static final Long RELEASE_LOCK_SUCCESS = 1L;
/**
* 釋放分佈式鎖
*
* @param jedis Redis客戶端
* @param lockKey 鎖標記
* @param lockValue 加鎖方標記
* @return 是否釋放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String lockValue) {
String script = "" +
"if redis.call('get', KEYS[1]) == ARGV[1] then" +
" return redis.call('del', KEYS[1]) " +
"else" +
" return 0 " +
"end";
// Collections.singletonList():用於只有一個元素的場景,減少內存分配
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
if (RELEASE_LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
3.2、Redlock算法
此算法由Redis作者antirez提出,作爲一種分佈式場景下的鎖實現方案;
3.2.1、Redlock算法原理
【核心】大多數節點獲取鎖成功且鎖依舊有效;
- Step1、獲取當前時間(毫秒數);
- Step2、按序想N個Redis節點獲取鎖;
- Step2.1、設置隨機字符串random_value;
- Step2.2、設置鎖過期時間;
- Note1:獲取鎖需設置超時時間(防止某個節點不可用),且timeout應遠小於鎖有效時間(幾十毫秒級);
- Note2:某節點獲取鎖失敗後,立即向下一個節點獲取鎖(任何類型失敗,包含該節點上的鎖已被其他客戶端持有);
- Step3、計算獲取鎖的總耗時totalTime;
- Step4、獲取鎖成功
- 獲取鎖成功:客戶端從大多數節點(>=N/2+1)成功獲取鎖,且totalTime不超過鎖的有效時間;
- 重新計算鎖有效時間:最初鎖有效時間減3.1計算的獲取鎖消耗的時間;
- Step5、獲取鎖失敗
- 獲取失敗後應立即向【所有】客戶端發起釋放鎖(Lua腳本);
- Step6、釋放鎖
- 業務完成後應立即向【所有】客戶端發起釋放鎖(Lua腳本);
3.2.2、Redlock算法優點
- 可用性高,大多數節點正常即可;
- 單Redis節點的分佈式鎖在failover時鎖失效問題不復存在;
3.2.3、Redlock算法問題點
- Redis節點崩潰將影響鎖安全性
A、節點崩潰前鎖未持久化,節點重啓後鎖將丟失;
B、Redis默認AOF持久化是每秒刷盤(fsync)一次,最壞情況將丟失1秒的數據; - 需避免時鐘跳躍;
A、管理員手動修改時鐘;
B、使用[不會跳躍調整系統時鐘]的ntpd(時鐘同步)程序,對時鐘修改通過多次微調實現; - 客戶端阻塞導致鎖過期,導致共享資源不安全;
- 如果獲取鎖消耗時間較長,導致效時間很短,是否應該立即釋放鎖?多段纔算短?
3.3、帶fencing token的實現
分佈式系統專家Martin Kleppmann討論提出RedLock存在安全性問題;
3.3.1、神仙之戰
Martin Kleppmann認爲Redis作者antirez提出的RedLock算法有安全性問題,雙方在網絡上多輪探討交鋒。Martin指出RedLock算法的核心問題點如下:
- 鎖過期或者網絡延遲將導致鎖衝突:
A、客戶端A進程pause》鎖過期》客戶端B持有鎖》客戶端A恢復並向共享資源發起寫請求;
B、網絡延遲也會產生類似效果; - RedLock安全性對系統時鐘有強依賴;
3.3.2、fencing token算法原理
- fencing token是一個單調遞增的數字,當客戶端成功獲取鎖時隨同鎖一起返回給客戶端;
- 客戶端訪問共享資源時帶上token;
- 共享資源服務檢查token,拒絕延遲到來的請求;
3.3.3、fencing token算法問題點
- 需要改造共享資源服務;
- 如果資源服務也是分佈式,如何保證token在多個資源服務節點遞增;
- 2個fencing token到達資源服務的順序顛倒,服務檢查將異常;
- 【antirez】既然存在fencing機制保持資源互斥訪問,爲什麼還需要分佈式鎖且要求強安全性呢;
3.4、其他分佈式鎖
3.4.1、數據庫排它鎖
- 獲取鎖(select for update ,悲觀鎖);
- 處理業務邏輯;
- 釋放鎖(connection.commit());
- 注意:InnoDB引擎在加鎖的時候,只有通過索引進行檢索的時候纔會使用行級鎖,否則會使用表級鎖。So 必須給lock_name加索引。
3.4.2、ZooKeeper分佈式鎖
- 客戶端創建znode節點,創建成功則獲取鎖成功;
- 持有鎖的客戶端訪問共享資源完成後刪除znode;
- znode創建成ephemeral(znode特性),保證創建znode的客戶端崩潰後,znode會被自動刪除;
- 【問題】Zookeeper基於客戶端與Zookeeper某臺服務器維護Session,Session依賴定期心跳(heartbeat)維持。Zookeeper長時間收不到客戶端心跳,就任務Session過期,這個Session所創建的所有ephemeral類型的znode節點都將被刪除。
3.4.3、Google的Chubby分佈式鎖
- sequencer機制(類似fencing token)緩解延遲導致的問題;
- 鎖持有者可隨時請求一個sequencer;
- 客戶端操作資源時將sequencer傳給資源服務器;
- 資源服務器檢查sequencer有效性;
- ①調用Chubby的API(CheckSequencer)檢查;
- ②對比檢查客戶端、資源服務器當前觀察到的sequencer(類似fencing token);
- ③lock-delay:允許客戶端爲持有鎖指定一個lock-delay延遲時間,Chubby發現客戶端失去聯繫時,在lock-delay時間內組織其他客戶端獲取鎖;
4、總結
4.1、我們該使用怎樣的分佈式鎖算法?
- 技術都是爲業務服務的,避免選擇“高大上”的炫技;
- 依託業務場景,儘可能選擇最簡單的做法;
- 最簡單的分佈式鎖導致偶發性異常如何處理呢?
- 建議增加額外的機制甚至人工介入保證業務準確性,通常這部分成本低於複雜的分佈式鎖的開發、運維成本。
4.2、分佈式鎖的另類玩法
- “分而治之”經久不衰:
- 如果共享資源本身可以拆分,那就分開處理吧。
- 比如電商系統防止超賣,假設有10000個口罩將被秒殺,常規做法是一個鎖控制所有資源。另類玩法就是將10000個口罩交由20個鎖控制,整體性能瞬間提升幾十倍。
- PS:此處超賣僅是舉例,真實場景下的秒殺超賣有更加複雜的場景,慎重。
敬請關注後續《玩轉Redis》系列文章。
祝君好運!
Life is all about choices!
將來的你一定會感激現在拼命的自己!
【CSDN】【GitHub】【OSCHINA】【掘金】【語雀】【微信公衆號】