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