分布式鎖的3種實現方式
1.基於數據庫
1.1 悲觀鎖
具有強烈的獨占性和排他性,認為別人會更新數據,所以拿到數據后就會上鎖。悲觀鎖主要用於保護數據的完整性, 在多個事務並發執行時。只要某個事務拿到鎖之后,此時其他事務就要等到該事務執行完成,其他事務才能對該數據進行修改操作。悲觀並發控制主要用於數據爭用激烈的環境,以及發生並發沖突時使用鎖保護數據的成本要低於回滾事務的成本的環境中。
使用悲觀鎖實現分布式鎖,在查詢語句后面增加for update
數據庫會在查詢過程中給數據庫表增加悲觀鎖
select * from methodLock where method_name=xxx for update;
當任務執行完成后,commit當前事務。來達到釋放鎖的目的。
注意:method_name這個字段需要加索引,否則會鎖表。
1.2 樂觀鎖
每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但在做更新操作的時候會判斷一下期間別人有沒有更新數據,一般是使用version版本號來控制。
實現分布式鎖的方式:
可以創建一張專門的鎖表,然后通過操作該表中的數據來實現,當需要操作某個資源時候,可以通過版本號,通過查詢當前的版本號,然后執行完任務之后,再判斷一下任務前的版本號和任務后的版本號是否一致,如果一致,就將當前版本號+1.
還有一種方式是,當我們要操作某個資源時,就在表中新增一條記錄,等到執行完之后將該條數據刪除。前提是需要對method_name這個字段做唯一約束,保證有多個事務請求提交到數據庫,只有一個能操作成功。操作成功的那個請求獲得鎖,就可以執行具體的方法,執行完成之后就刪除該條記錄釋放鎖。
2.基於Redis
利用Redis的String(String)數據類型來實現分布式鎖。
實現方式1:
通過SETNX key 命令加鎖,並為該key添加一個過期時間,設置過期時間是為了防止死鎖;給value設值可以為UUID ,在釋放鎖前先判斷,判斷當前鎖的UUID是否與傳入的UUID值相等。最后使用DEL命令刪除key達到釋放鎖的目的。
實現方式2:
當並發請求進來是先判斷某個key是否存在,如果不存在就set key,並為該key設置一個過期時間防止死鎖。當前請親獲得鎖,當其他請求進來時發現該key已經存在就等待,當請求執行完成之后就DEL key,達到釋放鎖的目的。
實現方式3:redlock算法(后面再看看)
實現方式4:Redisson(后面再看看)
3.基於Zookeeper
實現方式1:
利用節點名稱的唯一性來實現。
並發請求進來時,所有請求都創建同一個節點lock/methdlock
,最終只有一各創建成功,創建成功的即獲得鎖。當任務執行完成后,通過刪除幾點的方式達到釋放鎖的目的。
實現方式2:
通過臨時順序節點實現。
並發請求進來時,在 /lock目
錄下創建臨時順序節點,創建節點前先獲取/lock
目錄下所有的子節點,然后獲取比自己小的兄弟節點,如果不存在,則說明當前節點順序號最小,即獲得鎖;當任務執行完成后就刪除當前節點,即為釋放鎖。一旦客戶端獲取到鎖之后突然掛掉,那么這個臨時節點就會自動刪除掉。其他客戶端就可以再次獲得鎖。避免了死鎖。
4.三種方式的優缺點
基於數據庫:
優點:實現方式很好理解;直接使用數據庫,不需要在項目里額外的新增組件,實現簡單。
缺點:
1、有鎖表的風險,表中的數據少的時候,數據庫會不走索引,導致鎖表。
2、如果任務執行時間過長,其他線程會不斷的請求,會占用較多的數據庫連接資源。
3、非阻塞,操作失敗后,需要輪詢,占用cpu資源;
基於Redis:
優點:reids性能好,redis有較好的命令支持,實現起來方便
缺點:
1、如果鎖刪除失敗,使用key的過期時間來控制鎖的釋放,這個過期時間不好控制。過長或者過短對整個項目的性能都有影響。
2、非阻塞,操作失敗后,需要輪詢,占用cpu資源;
3、再集群模式下,需要考慮的鎖失效問題很多,復雜讀高。
基於ZooKeeper:
優點:獲取不到鎖,只需要注冊個監聽器即可,不需要不斷主動嘗試獲取鎖,性能開銷小。zookeeper具有分布式強一致性。鎖的模型健壯、簡單易用、適合做分布式鎖。
缺點:性能相較於redis差。一般項目中沒有引入ZK,需要額外的添加外部依賴才能實現。
5.(總結)選型
以上幾種方式,各有優缺點,都無法做到完美,再實際項目中需要根據實際情況來選擇合適的方式;
從理解的難易程度角度(從低到高):數據庫 > 緩存 > Zookeeper
從實現的復雜性角度(從低到高):Zookeeper >= 緩存 > 數據庫
從性能角度(從高到低):緩存 > Zookeeper >= 數據庫
從可靠性角度(從高到低):Zookeeper > 緩存 > 數據庫
項目追求速度的話就用redis,要安全可靠的話就用ZK