分布式鎖
1,簡介
傳統的單體應用使用本地鎖(synchronized、reentrantLock),隨着分布式的快速發現者,本地鎖無法解決並發問題,需要一種能跨微服務/跨虛擬機的鎖機制->分布式鎖
作用:
- 並發正確性(資源獨占)
- 效率:避免重復處理
作用:
- 互斥性:基本功能,一個獲取鎖,另外一個就不能獲取
- 可重入性能:一個線程獲取到鎖之后,可以再次獲取(多次獲取)
- 鎖超時:持有鎖的線程掛掉后,一定時間鎖自動釋放
- 高效:加鎖/釋放鎖速度快
- 高可用:集群、容災
- 支持阻塞和非阻塞
- 支持公平鎖和非公平鎖
常用的分布式鎖中間件:
- mysql
- zookeeper
- redis
- etcd
- chubby
2,分布式鎖
2.1,mysql
方案1:使用專用的數據表
若需要加鎖的資源恰好有對應的數據表,可以在數據表中增加響應的字段,達到服用數據的目的
阻塞式獲取鎖
循環調用lock()函數,直到返回true
非阻塞式獲取鎖
循環調用lock()函數,直到返回true,或者超時
啟動一個定時任務循環遍歷鎖,長時間未被釋放的即為超時,直接刪除
鎖的釋放
適用場景:沒有其他中間件可以使用,需要加鎖的資源恰好有對應的數據表
優點:理解起來簡單,不需要維護其他中間件
缺點:需要自己實現加鎖/解鎖過程,性能較差
2.2,zookeeper
zookeeper是以paxos算法為基礎分布式應用協調服務
data:Znode存儲的數據信息
ACL:記錄Znode的訪問權限
stat:包含Znode的各種元數據
child:子節點(樹狀結構,很像ldap數據倉庫)
鎖的實現原理:
線程去創建/resource_name子節點時會自動編號,第一個編號是/0000001。
第一個線程去創建鎖成功並且發現編號是/0000001並且是最小編號,那就直接保留執行程序;
第二個線程再去獲取鎖時,創建的子節點會自動編號為/0000002,該線程會發現這個節點不是最小節點,就向上一個節點/0000001設置一個watcher監視器,待/0000001線程執行完畢釋放的時候就直接觸發/0000002執行程序;
第三個個線程再去獲取鎖時,創建的子節點會自動編號為/0000003,該線程會發現這個節點不是最小節點,就向上一個節點/000000x設置一個watcher監視器,待/000000x線程執行完畢釋放的時候就直接觸發/0000003執行程序;
天生的公平鎖
加鎖流程:
- 進行重入的判斷(利用ThreadLocal)
- 在被鎖資源上建立EPHEMERAL_SEQUENTIAL節點
- 判斷自己的節點是否位於第一個
- 若是第一個,則獲取到鎖,返回
- 若不是第一個,在前一個節點上注冊watcher
- 進行阻塞等待
解鎖流程:
- 進行重入的判斷(利用ThreadLocal)
- 若為重入,在重入次數減1,返回
- 刪除zookeeper上的有序節點
curator已經實現了上述的zookeeper分布式鎖
優點:
- 對於鎖超時有現成的處理方法
- 天然的公平鎖
- ZK集群保證高可用
缺點:
- 增加開發與維護成本
- 性能和MySQL想差不大,依然很差
2.3,chubby
chubby谷歌開發的分布式應用程序協調服務,功能上與zookeeper類似
優點:
- 創建序列號時,提供了API檢查此序列號是否有效
- lock-delay,當客戶端失聯的時候,並不會立即釋放鎖(會去真實的確認是否真的失聯)
缺點:
- 未開源,無法二次開發
2.4,Etcd
Etcd是一個高可用的分布式鍵值(key-value)數據庫,內部采用raft協議作為一致性算法。
特性:
- lease機制,即租約機制,為存儲的key-value對設置租約,當租約到期,k-v將失效刪除
- revision機制:每個k帶有revision號,每一次事務加一,全局唯一
- prefix機制,即前綴機制,也稱為目錄機制
- watcher機制,即監聽機制,支持watch某個k,也支持watch一個范圍(前綴機制)
原理:
- /lock/resource為前綴創建key(/lock/resource/nodeX),並設置租約長度
- 客戶端創建一個定時任務作為“心跳”,定時進行續約(看程序執行的時間,如果耗時長需要去續約)
- 將創建的key寫入Etcd,獲得revision號
- 獲取/lock/resource下的所有key
- 若revision為最小,獲取鎖成功
- 若非最小,watch前一個revision號,待前面的釋放才獲取到
- 完成業務后,刪除響應的key釋放鎖
etcdV3已經實現分布式鎖
優點:
- V3接口提供現場的分布式鎖實現
- 天然是公平鎖(與zookeeper類似)
- Etcd集群保證了高可用
缺點:
- 性能一般
2.5,redis
redis(remote dictionary server)是一個k-v存儲中間件。
實現操作:
redisV2.8之前:使用lua腳本實現,因為setnx命令不支持設置過期ex
redisV2.8之后:set resourceName value ex
(ex設置過期時間,ex做獨占操作),這個命令可以保證原子性。以前的版本不能保證原子性!
加鎖問題:
-
進程A未續約(設置有效期),導致B獲取了鎖
-
復雜操作需要及時續約:expire resourceName
解鎖問題:
-
進程B解鎖時,key已經被A刪除,導致B異常
-
解鎖時需要判斷是否是自身持有的鎖
使用業務代碼判斷,判斷和刪除非原子操作,有安全問題(前面判斷在了,后面就刪除,但是這兩個操作之間有可能就被其他線程B獲取到鎖了!!!)
使用lua腳本判斷,判斷和刪除是原子操作
redisson封裝了鎖的實現
繼承了java.util.concurrent.locks.Lock接口
實現3種:阻塞式的(lock),非阻塞式(tryLock),異步非阻塞式的(tryLockAsync)
實現原理:
嘗試加鎖,首先會嘗試進行加鎖,由於保證操作是原子性,那么就只能使用lua腳本,相關的lua腳本如下:
redisson並沒使用set nx,而是使用hash結構
原理:
- 如果嘗試獲取鎖失敗,判斷是否超時,如果超時則返回false
- 如果加鎖失敗之后,沒有超時,那么需要在名字為redissopnm_name_channel+lockName的channel上進行訂閱,用於訂閱解鎖消息,然后一直阻塞直到超時,或者有解鎖消息
- 重試上述步驟,直到最后獲取到鎖,或者某一步獲取鎖超時
- 解鎖時通過lua腳本,如果是可重入鎖,只是減1;如果是非加鎖線程解鎖,那么解鎖失敗
redLock紅鎖
redis主從與集群並不是強一致性的,所以在極端情況下,會有一致性問題,若redis未及時持久化,重啟會丟失數據。為了解決上述問題,redis作者提出了RedLock紅鎖算法。
原理:
- 首先生成多個redis集群的Rlock,並將其構造程RedLock
- 依次循環對三個集群進行加鎖,加鎖方式和redission一致
- 如果循環加鎖的過程中加鎖失敗,那么需要判斷加鎖失敗的次數是否超出了最大值(要多數成功)
- 加鎖的過程中需要判斷是否加鎖超時
- 若失敗,向所有節點請求解鎖
進程A依次向master1,master2,master3獲取鎖
優點:
- redis在項目中很常見
- 容易取得可靠性和性能的平衡
缺點:
- RedLock算法需要多套redis實例,資源耗費
3,安全問題
3.1,GC導致鎖超時
線程A獲取到鎖,正常情況下程序1秒執行完畢,然后釋放鎖;但是突然系統來了一個stop-the-world GC pause耗時2秒鍾,此時鎖已經自動釋放,線程A恢復運行;這時線程B是可以獲取到鎖!(線程不安全)
chubby lock-delay:當客戶端失聯的時候,並不會立即釋放鎖,而是在一定時間內(默認1min)阻止其他客戶端拿到這個鎖
3.2,網絡I/O導致鎖超時
與上面的GC類似,網絡不穩定,請求某些接口耗時特別長導致這個事務整體耗時變長,分布式鎖超時釋放了!
chubby:提供API,供storage服務在收到請求時校驗當前序號,如果查詢獲取到當前釋放的鎖已經被過期了那么就直接拒絕!
3.3,時鍾跳躍導致的鎖超時
從NTP服務收到了一個大的時鍾更新,導致一大批鎖直接過期!
解決辦法:少量多次更新時間,例如更新時間是10分鍾,我們分為10次,每次更新1分鍾,來逐步更新系統時間,這樣相對會好一些