前言
公司交給了萌新小猿一個光榮而艱鉅的專案,該專案需要使用分散式鎖,這可難道了小猿,只是聽說過分散式鎖很牛掰,其他就一概不知了,唉不懂就問唄,遂向老闆請教。
老闆:我們每天不都在經歷分散式鎖嗎,我來給你回憶回憶。
小猿:好勒,瓜子板凳已備好。
本文結構
- 為什麼要使用分散式鎖
- 分散式鎖有哪些特點
- 分散式鎖流行演算法及其優缺點
- 基本演算法
- 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】【掘金】【語雀】【微信公眾號】