分散式鎖
來自專欄分散式系統
概述
分散式系統中多個組件,或者同一組件的多個副本之間要協調一致的工作,經常會需要用鎖機制來保證大家的並發操作不會發生衝突。分散式系統中的鎖和進程間通信用到的鎖的區別就在於分散式系統中的進程分布在不同主機,甚至不同地域,想要使用同一把鎖去保護臨界資源的話,就需要一個統一的鎖管理器。不同主機上的進程都到這裡去申請這個分散式鎖。而在分散式系統中,常見的鎖管理器有以下三種實現:1. 通過集中式的sql資料庫;2. zookeep;3.redis。其實這三種都可以簡單的認為是資料庫。
SQL資料庫
一般是利用sql里的唯一性限制,簡單說,就是多個進程重複插入同一條數據時會失敗。基本思路如下:
1 多個進程同時向資料庫插入主鍵為A的記錄?最先到達的請求成功創建,並在記錄里打上時間戳,相當於獲取到了鎖?後續請求均失敗,進程選擇回退或等待?獲得鎖的進程操作完成後釋放鎖
幾個問題:
- 獲取鎖失敗的話,需要考慮重試時間,這個不太好掌握。如果應用於不需要重試的場景比較好?另外,可以根據資料庫的其它限制,或者自己增加一些限制條件,來做並發控制。比如,只允許某兩列同時更新等等
- 記錄里的時間戳用於強行釋放鎖,即如果獲得鎖的進程掛了,那其他等待進程可以在一個超時時間後刪除這個鎖,並重新競爭這個鎖?可以考慮共享鎖的實現。比如記錄里增加兩列,分別為rlock和wlock。每一個進程要申請共享鎖的時候去給rlock+1,當然where條件是wlock=0。一般資料庫都會返回更新的記錄數,如果為0,表示沒獲得到鎖
try;
insert(A, TIMESTAMP)
except DuplicateException:
# 獲取讀鎖
ret = update(A, TIMESTAMP)where (wlock==0)
#獲取寫鎖
#ret = update(A, TIMESTAMP)where (rlock==0)
if ret == 0:
retry
return ok
zookeeper
參考官網zookeeper lock,利用zookeeper的sequence和ephemera標記,也就是說自動生成序號和TTL功能,多個節點在一個鎖目錄下一起創建自己的節點,sequence標記會按請求的順序把節點編號,進程再把所有節點都讀出來,看看自己的節點號是不是最小的,如果是,就獲取了鎖;如果不是,通過watch功能監聽比自己小的那個節點,當比自己小的節點被刪除時(也就是該進程釋放了鎖),它就可以獲取鎖了。
- 調用create()在鎖目錄(假設是/lock下)創建節點/lock/node-,並設置sequence和ephemera標記。sequence會讓最終生成的節點為/lock/node-0000001,/lock/node-0000002這種序列
- 通過getChildren( )方法吧所有鎖節點讀出來。這時即使有節點還沒創建出來也無所謂,因為它們的序號肯定比當前的大
- 如果發現自己的序號是最小的,表示獲得了鎖
- 否則用exist()方法看看比自己小的那個節點是否存在,並watch它。exist和watch是一個原子操作,否則就會用並發問題了
- 如果exist返回false或者watch通知了前一個節點已經刪除,重複步驟2(getChildren)嘗試獲取鎖
官網還給了共享鎖以及可恢復的共享鎖的演算法,基本思路一致。另外可以使用zookeeper的庫curator封裝好了的鎖
redis-redlock
redis官網上有一篇antirez(redis作者)介紹redis分散式鎖的文章,針對這篇文章里介紹的演算法有一些爭議,認為這個鎖不靠譜。我們可以先看看這個演算法。
這個演算法的基礎是指條命令
SET resource_name my_random_value NX PX 30000
其中:
- resource_name:要保護的資源名,也可以認為是鎖名
- my_random_value:獲取鎖時寫入的一個隨機值,為了避免其他進程錯誤的釋放該鎖
- NX:表示只有首次設置才能成功。和SQL里的分散式鎖類似,SQL用insert保證只有首個能成功
- PX 30000:30秒的過期時間,和前面的鎖類似,都是為了在鎖持有者掛的情況下,該鎖能被釋放
另外,由於前面設置了my_random_value這個隨機值,所以釋放鎖的時候要把它取出來,再比較一下這個值是否正確,最後刪除。這個過程要求原子性,因為你取出來以後,可能鎖恰好過期了,別人又創了新鎖,但是你比較後,卻把這個鎖釋放了。為了保證這個原子性,這3個動作必須放在一個Lua腳本里執行,官網也給出了腳本
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1])else return 0end:
下面介紹一下redlock的演算法,不同於前面,redlock要到多個節點上去獲取分散式鎖。
- 獲取當前時間
- 向所有節點去獲取鎖(執行上面的命令)。因為要在多個節點都去獲取鎖,為了避免被某一個節點卡住,所以獲取鎖的請求也有個有效時間,大概幾十毫秒,而且如果其中某一個節點寫失敗了,應該立刻去寫下一個節點
- 節點如果成功在大多數節點上獲取到了鎖,且獲取鎖的時間不超過鎖的超時時間,則認為鎖獲取成功了
- 進程應該在鎖的有效時間=鎖的超時時間-獲取鎖鎖花費的時間
- 如果獲取鎖失敗,應該去所有節點上去釋放鎖(避免鎖獲取成功了,但是返回的成功消息丟了)
其他:
- 作者考慮了各個節點上的時鐘可能有偏差,因此鎖的有效時間應該減去一個修正值。因為各個節點上的時鐘誤差不會太大,這個值也不用太大,也就是:鎖的有效時間=鎖的超時時間-獲取鎖鎖花費的時間-修正值
- 獲取鎖失敗後的處理:
- 等待一個隨機時間後再重試,避免大家都同步進行操作
- 儘快到獲取成功的節點上去釋放,避免別的進程等待
這個方案看上去比較完美,但給我的第一感覺不是很靠譜,但是只是感覺。可能是redis的應用場景的問題。在我的理解里,它不提供強一致性,所以適合於對一致性,正確性要求不高的非關鍵場景,數據偶爾丟了或者不一致影響不大的場景。也就是說自己都不靠譜,還做分散式鎖,去為別人提供一致性和正確性,聽起來就不太靠譜了。
業界也有關於redlock的一些討論,感覺大神們還是厲害,篇幅有限下一篇再具體討論這個問題。
總結
個人不太推薦redlock(具體後面文章再討論);zookeeper應該是比較成熟的方案,穩定可靠,而且有比較成熟的庫,但要部署zookeeper;基於SQL的方案比較靈活,而且一般可以根據業務需要自己定製實現自己的鎖表,並加各種constrain,而且一般系統應用系統都會有SQL資料庫,不需額外組件。
推薦閱讀:
※分散式輕量級批量任務框架設計思想
※Flink源碼解析之State的實現
※分散式架構的套路No.74
※PacifiaA讀書筆記
※阿里最年輕合伙人胡喜:骨子裡沒點技術理想主義干不來自主研發