常用的分布式鎖
一、基於數據庫實現分布式鎖
1. 悲觀鎖
利用select … where … for update 排他鎖
注意: 其他附加功能與實現一基本一致,這里需要注意的是“where name=lock ”,name字段必須要走索引,否則會鎖表。有些情況下,比如表不大,mysql優化器會不走這個索引,導致鎖表問題。
2. 樂觀鎖
所謂樂觀鎖與前邊最大區別在於基於CAS思想,是不具有互斥性,不會產生鎖等待而消耗資源,操作過程中認為不存在並發沖突,只有update version失敗后才能覺察到。我們的搶購、秒殺就是用了這種實現以防止超賣。
通過增加遞增的版本號字段實現樂觀鎖
二、基於jdk的實現方式
缺點:這種方式的分布式鎖看似簡單,但是要考慮可用性、可靠性、效率、擴展性的話,編碼難度會比較高。
三、基於緩存(Redis等)實現分布式鎖
1、官方叫做 RedLock 算法,是 redis 官方支持的分布式鎖算法。
這個分布式鎖有 3 個重要的考量點:
- 1.互斥(只能有一個客戶端獲取鎖)
- 2.不能死鎖
- 3.容錯(只要大部分 redis 節點創建了這把鎖就可以)
2、下面是redis分布式鎖的各種實現方式和缺點,按照時間的發展排序
- 1、直接setnx
直接利用setnx,執行完業務邏輯后調用del釋放鎖,簡單粗暴
缺點:如果setnx成功,還沒來得及釋放,服務掛了,那么這個key永遠都不會被獲取到 - 2、setnx設置一個過期時間
為了改正第一個方法的缺陷,我們用setnx獲取鎖,然后用expire對其設置一個過期時間,如果服務掛了,過期時間一到自動釋放
缺點:setnx和expire是兩個方法,不能保證原子性,如果在setnx之后,還沒來得及expire,服務掛了,還是會出現鎖不釋放的問題 - 3、set nx px
redis官方為了解決第二種方式存在的缺點,在2.8版本為set指令添加了擴展參數nx和ex,保證了setnx+expire的原子性,使用方法:
set key value ex 5 nx
缺點:
①如果在過期時間內,事務還沒有執行完,鎖提前被自動釋放,其他的線程還是可以拿到鎖
②上面所說的那個缺點還會導致當前的線程釋放其他線程占有的鎖 - 4、加一個事務id
上面所說的第一個缺點,沒有特別好的解決方法,只能把過期時間盡量設置的長一點,並且最好不要執行耗時任務
第二個缺點,可以理解為當前線程有可能會釋放其他線程的鎖,那么問題就轉換為保證線程只能釋放當前線程持有的鎖,即setnx的時候將value設為任務的唯一id,釋放的時候先get key比較一下value是否與當前的id相同,是則釋放,否則拋異常回滾,其實也是變相地解決了第一個問題
缺點:get key和將value與id比較是兩個步驟,不能保證原子性 - 5、set nx px + 事務id + lua
我們可以用lua來寫一個getkey並比較的腳本,jedis/luttce/redisson對lua腳本都有很好的支持
缺點:集群環境下,對master節點申請了分布式鎖,由於redis的主從同步是異步進行的,master在內存中寫入了nx之后直接返回,客戶端獲取鎖成功,此時master節點掛了,並且數據還沒來得及同步,另一個節點被升級為master,這樣其他的線程依然可以獲取鎖 - 6、redlock
為了解決上面提到的redis集群中的分布式鎖問題,redis的作者antirez的提出了red lock的概念,假設集群中所有的n個master節點完全獨立,並且沒有主從同步,此時對所有的節點都去setnx,並且設置一個請求過期時間re和鎖的過期時間le,同時re必須小於le(可以理解,不然請求3秒才拿到鎖,而鎖的過期時間只有1秒也太蠢了),此時如果有n / 2 + 1個節點成功拿到鎖,此次分布式鎖就算申請成功
缺點:可靠性還沒有被廣泛驗證,並且嚴重依賴時間,好的分布式系統應該是異步的,並不能以時間為擔保,程序暫停、系統延遲等都可能會導致時間錯誤(網上還有很多人都對這個方法提出了質疑,比如full gc發生的鎖的正確性問題,但是antirez都一一作出了解答,感興趣的同學可以參考一下這位同學的文章)
四、基於zookeeper實現的分布式鎖
1. 實現方式
ZooKeeper是一個為分布式應用提供一致性服務的開源組件,它內部是一個分層的文件系統目錄樹結構,規定同一個目錄下只能有一個唯一文件名。基於ZooKeeper實現分布式鎖的步驟如下:
(1)創建一個目錄mylock;
(2)線程A想獲取鎖就在mylock目錄下創建臨時順序節點;
(3)獲取mylock目錄下所有的子節點,然后獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖;
(4)線程B獲取所有節點,判斷自己不是最小節點,設置監聽比自己次小的節點;
(5)線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是不是最小的節點,如果是則獲得鎖。
這里推薦一個Apache的開源庫Curator,它是一個ZooKeeper客戶端,Curator提供的InterProcessMutex是分布式鎖的實現,acquire方法用於獲取鎖,release方法用於釋放鎖。
優點:具備高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。
缺點:因為需要頻繁的創建和刪除節點,性能上不如Redis方式。
2. 兩種利用特性實現原理:
- 1、利用臨時節點特性
zookeeper的臨時節點有兩個特性,一是節點名稱不能重復,二是會隨着客戶端退出而銷毀,因此直接將key作為節點名稱,能夠成功創建的客戶端則獲取成功,失敗的客戶端監聽成功的節點的刪除事件
缺點:所有客戶端監聽同一個節點,但是同時只有一個節點的事件觸發是有效的,造成資源的無效調度 - 2、利用順序臨時節點特性
zookeeper的順序臨時節點擁有臨時節點的特性,同時,在一個父節點下創建創建的子臨時順序節點,會根據節點創建的先后順序,用一個32位的數字作為后綴,我們可以用key創建一個根節點,然后每次申請鎖的時候在其下創建順序節點,接着獲取根節點下所有的順序節點並排序,獲取順序最小的節點,如果該節點的名稱與當前添加的名稱相同,則表示能夠獲取鎖,否則監聽根節點下面的處於當前節點之前的節點的刪除事件,如果監聽生效,則回到上一步重新判斷順序,直到獲取鎖。
總結
基於數據庫分布式鎖實現
優點:直接使用數據庫,實現方式簡單。
缺點:
- db操作性能較差,並且有鎖表的風險
- 非阻塞操作失敗后,需要輪詢,占用cpu資源;
- 長時間不commit或者長時間輪詢,可能會占用較多連接資源
基於jdk的並發工具自己實現的鎖
優點:不需要引入中間件,架構簡單
缺點:編寫一個可靠、高可用、高效率的分布式鎖服務,難度較大
基於redis緩存
1. redis set px nx + 唯一id + lua腳本
優點:redis本身的讀寫性能很高,因此基於redis的分布式鎖效率比較高
缺點:依賴中間件,分布式環境下可能會有節點數據同步問題,可靠性有一定的影響,如果發生則需要人工介入
2. 基於redis的redlock
優點:可以解決redis集群的同步可用性問題
缺點:
- 依賴中間件,並沒有被廣泛驗證,維護成本高,需要多個獨立的master節點;需要同時對多個節點申請鎖,降低了一些效率
- 鎖刪除失敗 過期時間不好控制
- 非阻塞,操作失敗后,需要輪詢,占用cpu資源;
基於zookeeper的分布式鎖
優點:不存在redis的超時、數據同步(zookeeper是同步完以后才返回)、主從切換(zookeeper主從切換的過程中服務是不可用的)的問題,可靠性很高
缺點:依賴中間件,保證了可靠性的同時犧牲了一部分效率(但是依然很高)。性能不如redis。
jdk的方式不太推薦。
- 從理解的難易程度角度(從低到高)數據庫 > 緩存 > Zookeeper
- 從實現的復雜性角度(從低到高)Zookeeper >= 緩存 > 數據庫
- 從性能角度(從高到低)緩存 > Zookeeper >= 數據庫
- 從可靠性角度(從高到低)Zookeeper > 緩存 > 數據庫