1、redis分佈式鎖
redis 最普通的分佈式鎖
- 加鎖: 第一個最普通的實現方式,就是在 redis 裏使用 setnx 命令創建一個 key,這樣就算加鎖。
SET resource_name my_random_value NX PX 30000
- 解鎖: 用下面的lua腳本保證原子性
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
問題:
1、依賴過期時間
事物提交前,必須先判斷鎖是否過期,然而判斷過期和提交事物是兩個操作,如果判斷沒有過期,在提交是之前發生GC,GC後鎖過期,再執行提交操作會出問題,如圖:
解決: 1、數據庫樂觀鎖,在每條記錄後面增加一個version字段
2、Martin的fencing token方案:每個獲取鎖生成一個token, 寫入數據是存儲服務判斷token是不是當前最大的,不是則寫入失敗,如圖:
RedLock 算法
這個場景是假設有一個 redis cluster,有 5 個 redis master 實例。然後執行如下步驟獲取一把鎖:
獲取當前時間戳,單位是毫秒;
跟上面類似,輪流嘗試在每個 master 節點上創建鎖,過期時間較短,一般就幾十毫秒;
嘗試在大多數節點上建立一個鎖,比如 5 個節點就要求是 3 個節點 n / 2 + 1;
客户端計算建立好鎖的時間,如果建立鎖的時間小於超時時間,就算建立成功了;
要是鎖建立失敗了,那麼就依次之前建立過的鎖刪除;
只要別人建立了一把分佈式鎖,你就得不斷輪詢去嘗試獲取鎖。
Redis 官方給出了以上兩種基於 Redis 實現分佈式鎖的方法,詳細説明可以查看:https://redis.io/topics/distlock 。
問題:
1、兩個客户端同時獲得鎖的問題(時間跳躍,節點崩潰,客户端GC延遲都可能導致這個問題):
- Client 1 acquires lock on nodes A, B, C. Due to a network issue, D and E cannot be reached.
- The clock on node C jumps forward, causing the lock to expire.
- Client 2 acquires lock on nodes C, D, E. Due to a network issue, A and B cannot be reached.
- Clients 1 and 2 now both believe they hold the lock.
解決:
1、Disqus提出的 delay restart, 宕機節點在所有的鎖超時後再啓動
2、延遲啓動仍然是依賴時間的,所以,第二個方案是設置fsync=always,每次寫入同步到磁盤才回復客户端。
- fsync=allways 會極大消弱Redis的性能,因為這種模式下每次write後都會調用fsync(Linux為調用fdatasync)。
- none 如果設置為no,則write後不會有fsync調用,由操作系統自動調度刷磁盤,性能是最好的。
- everysec 為最多每秒調用一次fsync,這種模式性能並不是很糟糕,一般也不會產生毛刺,這歸功於Redis引入了BIO線程,所有fsync操作都異步交給了BIO線程。
2、zk 分佈式鎖
方法1(非公平鎖):
zk 分佈式鎖,其實可以做的比較簡單,就是某個節點嘗試創建臨時 znode,此時創建成功了就獲取了這個鎖;這個時候別的客户端來創建鎖會失敗,只能註冊個監聽器監聽這個鎖。釋放鎖就是刪除這個 znode,一旦釋放掉就會通知客户端,然後有一個等待着的客户端就可以再次重新加鎖。
方法2(公平鎖):
創建一個鎖目錄 /lock;
當一個客户端需要獲取鎖時,在 /lock 下創建臨時的且有序的子節點;
客户端獲取 /lock 下的子節點列表,判斷自己創建的子節點是否為當前子節點列表中序號最小的子節點,如果是則認為獲得鎖;否則監聽自己的前一個子節點,獲得子節點的變更通知後重復此步驟直至獲得鎖;
執行業務代碼,完成後,刪除對應的子節點。
羊羣效應:
一個節點未獲得鎖,只需要監聽自己的前一個子節點,這是因為如果監聽所有的子節點,那麼任意一個子節點狀態改變,其它所有子節點都會收到通知(羊羣效應,一隻羊動起來,其它羊也會一哄而上),而我們只希望它的後一個子節點收到通知。
讀寫鎖:
這個時候我規定所有創建節點必須有序,當你是讀請求(要獲取共享鎖)的話,如果 沒有比自己更小的節點,或比自己小的節點都是讀請求 ,則可以獲取到讀鎖,然後就可以開始讀了。若比自己小的節點中有寫請求 ,則當前客户端無法獲取到讀鎖,只能等待前面的寫請求完成。
如果你是寫請求(獲取獨佔鎖),若 沒有比自己更小的節點 ,則表示當前客户端可以直接獲取到寫鎖,對數據進行修改。若發現 有比自己更小的節點,無論是讀操作還是寫操作,當前客户端都無法獲取到寫鎖 ,等待所有前面的操作完成。
redis 分佈式鎖和 zk 分佈式鎖的對比
- redis 分佈式鎖,其實需要自己不斷去嘗試獲取鎖,比較消耗性能。
- zk 分佈式鎖,獲取不到鎖,註冊個監聽器即可,不需要不斷主動嘗試獲取鎖,性能開銷較小。
- 如果是 redis 獲取鎖的那個客户端 出現 bug 掛了,那麼只能等待超時時間之後才能釋放鎖;而 zk 的話,因為創建的是臨時 znode,只要客户端掛了,znode 就沒了,此時就自動釋放鎖。
3、Mysql分佈式鎖
在Mysql新建一張表,設置一個unique key,這個key就是要鎖的key(商品ID),同一個key在數據庫只能插入一條,可以新增一個expire_time設置鎖的過期時間。
CREATE TABLE distributed_lock (
key varchar(64) primary key "鎖唯一key",
value varchar(64) not null "加鎖終端",
expire_time datetime not null "過期時間"
);
- 加鎖的時候插入一條記錄,key即是鎖,value是加鎖線程設置的唯一標識,expire是鎖的過期時間
- 如果加鎖失敗,查詢數據庫已存在的鎖,判斷是否過期,過期就刪除鎖,並重新加鎖
- 解鎖,將key和value作為條件,delete對應的記錄即可
總結
redis分佈式鎖是用的對最多的,有現成的開源庫Redission可以用,能滿足大部分需求。一般情況下使用redis鎖就可以。
zookeeper分佈式鎖可以實現讀寫鎖,公平鎖和非公平鎖,能實現的鎖種類更多,也是一種很好用的鎖,但是需要自己實現。
MySQL實現起來稍顯麻煩,也需要自己實現。
三種鎖均不是絕對安全的鎖,都可能存在失效的情況。
唯一安全的鎖是數據庫樂觀鎖,請看https://juejin.cn/post/7390689983789023247
參考
https://juejin.cn/post/7390689983789023247