目錄
一、什么是分布式鎖
二、分布式鎖的基礎實現
三、引入過期時間
四、引入校驗 id
五、引入lua
六、引入 watch dog (看門狗)
七、引入 Redlock 算法
八、其他功能
redis學習🥳
一、什么是分布式鎖
在一個分布式的系統中,也會涉及到多個節點訪問同一個公共資源的情況. 此時就需要通過 鎖 來做互
斥控制,避免出現類似于 "線程安全" 的問題.
而 java 的 synchronized 或者 C++ 的 std::mutex,這樣的鎖都是只能在當前進程中生效,在分布式
系統中,是有很多進程的(每個服務器,都是獨立的進程)因此,之前的鎖,就難以對現在分布式系
統中的多個進程之間產生制約。分布式系統中,多個進程之間的執行順序也是不確定的 =>隨機性,此
時就需要使用到 “分布式鎖”.
? 本質上就是使用一個公共的服務器,來記錄 加鎖狀態.
這個公共的服務器可以是 Redis,也可以是其他組件(比如 MySQL 或者 ZooKeeper 等),還可以是我們自己寫的一個服務.
二、分布式鎖的基礎實現
思路非常簡單. 本質上就是通過一個鍵值對來標識鎖的狀態.
舉個例子: 考慮買票的場景,現在存在多個服務器節點,每個車次的票數都是固定的.
現在車站提供了若干個車次,都可能需要處理這個買票的邏輯: 先查詢指定車次的余票,如果余票 > 0,
則設置余票值 -= 1.
顯然上述的場景是存在 "線程安全" 問題的,需要使用鎖來控制.
否則就可能出現 "超賣" 的情況.
此時如何進行加鎖呢?我們可以在上述架構中引入一個 Redis?作為分布式鎖的管理器.
此時,如果?買票服務器1 嘗試買票,就需要先訪問 Redis,在 Redis 上設置一個鍵值對. 比如 key 就是車次,value 隨便設置個值 (比如 1).
如果這個操作設置成功,就視為當前沒有節點對該 001 車次加鎖,就可以進行數據庫的讀寫操作. 操作
完成之后,再把 Redis 上剛才的這個鍵值對給刪除掉.
如果在買票服務器1 操作數據庫的過程中,買票服務器2 也想買票,也會嘗試給 Redis 上寫一個鍵值對
key 同樣是車次. 但是此時設置的時候發現該車次的 key 已經存在了,則認為已經有其他服務器正在持
有鎖,此時 服務器2 就需要等待或者暫時放棄.
🎉 Redis 中提供了 setnx 操作,正好適合這個場景. 即: key 不存在就設置,存在則直接失敗.
但是上述方案并不完整.
三、引入過期時間
當 服務器1 加鎖之后,開始處理買票的過程中,如果 服務器1 意外宕機了,就會導致解鎖操作 (刪除該
key) 不能執行. 就可能引起其他服務器始終無法獲取到鎖的情況.
為了解決這個問題,可以在設置?key 的同時引入過期時間. 即這個鎖最多持有多久,就應該被釋放.
🌰 可以使用 set ex nx 的方式,在設置鎖的同時把過期時間設置進去.
注意! 此處的過期時間只能使用一個命令的方式設置.
如果分開多個操作,比如 setnx 之后,再來一個單獨的 expire,由于 Redis 的多個指令之間不存在關聯,并且即使使用了事務也不能保證這兩個操作都一定成功,因此就可能出現 setnx 成功,但是 expire 失敗的情況.
此時仍然會出現無法正確釋放鎖的問題.
四、引入校驗 id
對于 Redis 中寫入的加鎖鍵值對,其他的節點也是可以刪除的.
比如 服務器1 寫入一個 "001": 1 這樣的鍵值對,服務器2 是完全可以把 "001" 給刪除掉的.
當然,服務器2 不會進行這樣的 "惡意刪除" 操作,不過不能保證因為一些 bug 導致 服務器2 把鎖誤刪除.
為了解決上述問題,我們可以引入一個校驗 id.
比如可以把設置的鍵值對的 value,不再是簡單的設為一個 1,而是設成服務器的編號.
形如 "001":"服務器1" .
這樣就可以在刪除 key (解鎖)的時候,先校驗當前刪除 key 的服務器是否是當初加鎖的服務器,如果
是,才能真正刪除;不是則不能刪除。邏輯用偽代碼描述如下:
String key = [要加鎖的資源 id];
String serverId = [服務器的編號];// 加鎖, 設置過期時間為 10s
redis.set(key, serverId, "NX", "EX", "10s");// 執行各種業務邏輯, 比如修改數據庫數據.
doSomeThing();// 解鎖, 刪除 key. 但是刪除前要檢驗下 serverId 是否匹配.
if (redis.get(key) == serverId) {redis.del(key);
}
但是很明顯,解鎖邏輯是兩步操作 "get" 和 "del",這樣做并非是原子的.
五、引入lua
為了使解鎖操作原子,可以使用 Redis 的 Lua 腳本功能.
🎁 Lua 也是一個編程語言. 讀作 "擼啊". 是葡萄牙語中的 "月亮" 的意思. (出自于 Lua 官方文檔https://www.lua.org/about.html)
Lua 的語法類似于 JS,是一個動態弱類型的語言. Lua 的解釋器一般使用 C 語言實現. Lua 語法
簡單精煉,執行速度快,解釋器也比較輕量 (Lua 解釋器的可執行程序體積只有 200KB 左右).
因此 Lua 經常作為其他程序內部嵌入的腳本語言. Redis 本身就支持 Lua 作為內嵌腳本.
很多程序都支持內嵌腳本,比如 MySQL 8 支持 JS 作為內嵌腳本,比如 Vim 支持 VimScript
和 Python 作為內嵌腳本.... 通過內嵌腳本來實現更復雜的功能,提供更強的擴展性.
Lua 除了和 Redis 搭伙之外,在很多場景也會作為內嵌腳本. 比如在游戲開發領域常常作為編寫邏輯的語言. (比如魔獸世界、大話西游等)
使用 Lua 腳本完成上述解鎖功能:
if redis.call('get',KEYS[1]) == ARGV[1] thenreturn redis.call('del',KEYS[1])
elsereturn 0
end;
上述代碼可以編寫成一個 .lua 后綴的文件,由 redis-cli 或者 redis-plus-plus 或者 jedis 等客戶端加載,并發送給 Redis 服務器,由 Redis 服務器來執行這段邏輯.
一個 lua 腳本會被 Redis 服務器以原子的方式來執行.
redis-plus-plus 和 jedis 如何調用 lua?,咱們此處不做過多介紹. 具體 api 的寫法大家可以自行研究.
六、引入 watch dog (看門狗)
上述方案仍然存在一個重要問題. 當我們設置了?key 過期時間之后 (比如 10s),仍然存在一定的可能
性,當任務還沒執行完,key 就先過期了. 這就導致鎖提前失效.
把這個過期時間設置的足夠長,比如 30s, 是否能解決這個問題呢?很明顯,設置多長時間合適,是無止境的. 即使設置再長,也不能完全保證就沒有提前失效的情況.
而且如果設置的太長了,萬一對應的服務器掛了,此時其他服務器也不能及時的獲取到鎖.
因此相比于設置一個固定的長時間,不如動態的調整時間更合適.
所謂 watch dog,本質上是加鎖的服務器上的一個單獨的線程,通過這個線程來對鎖過期時間進行?"續
約".
注意,這個線程是業務服務器上的,不是 Redis 服務器的.
? 舉個具體的例子:
初始情況下設置過期時間為 10s. 同時設定看門狗線程每隔 3s 檢測一次.
那么當 3s 時間到的時候,看門狗就會判定當前任務是否完成.
? 如果任務已經完成,則直接通過 lua 腳本的方式,釋放鎖(刪除 key).
? 如果任務未完成,則把過期時間重寫設置為 10s. (即 "續約")
這樣就不擔心鎖提前失效的問題了. 而且另一方面,如果該服務器掛了,看門狗線程也就隨之掛了,此
時無人續約,這個 key 自然就可以迅速過期,讓其他服務器能夠獲取到鎖了.
七、引入 Redlock 算法
實踐中的 Redis 一般是以集群的方式部署的 (至少是主從的形式,而不是單機). 那么就可能出現以下比
較極端的大冤種情況:
🍰 服務器1 向 master 節點進行加鎖操作. 這個寫入 key 的過程剛剛完成,master 掛了;slave節點升級成了新的 master 節點. 但是由于剛才寫入的這個 key 尚未來得及同步給 slave 呢,此時就相當于 服務器1 的加鎖操作形同虛設了,服務器2 仍然可以進行加鎖 (即給新的 master 寫入 key. 因為新的 master 不包含剛才的 key).
因此之前學的 哨兵(分布式場景中,涉及的數據量不大)和集群(更多解決的是存儲空間不足的問
題)模式 是不可取的,為了解決這個問題,Redis 的作者提出了 Redlock 算法.
我們引入一組 Redis 節點. 其中每一組 Redis 節點都包含一個主節點和若干從節點. 并且組和組之間存
儲的數據都是一致的,相互之間是"備份"關系 (而并非是數據集合的一部分,這點有別于 Redis cluster).
加鎖的時候,按照一定的順序,寫多個 master 節點. 在寫鎖的時候需要設定操作的 "超時時間". 比如
50ms. 即如果 setnx 操作超過了 50ms 還沒有成功,就視為加鎖失敗.
如果給某個節點加鎖失敗,就立即再嘗試下一個節點.
當加鎖成功的節點數超過總節點數的一半,才視為加鎖成功.
如上圖,一共五個節點,三個加鎖成功,兩個失敗,此時視為加鎖成功.
這樣的話,即使有某些節點掛了,也不影響鎖的正確性.
🎼 那么是否可能出現上述節點都同時遇到了 "大冤種" 情況呢?理論上這件事是可能發生的,但是概率太小了. 工程上就可以忽略不計了.
同理,釋放鎖的時候,也需要把所有節點都進行解鎖操作. (即使是之前超時的節點,也要嘗試解鎖,盡
量保證邏輯嚴密).
簡而言之,Redlock 算法的核心就是,加鎖操作不能只寫給一個 Redis 節點,而要寫個多個!分布式
系統中任何一個節點都是不可靠的. 最終的加鎖成功結論是 "少數服從多數的".
由于一個分布式系統不至于大部分節點都同時出現故障, 因此這樣的可靠性要比單個節點來說靠譜不少.
八、其他功能
上述描述中我們解釋了基于 Redis 的分布式鎖的基本實現原理.
上述鎖只是一個簡單的互斥鎖. 但是實際上我們在一些特定場景中,還有一些其他特殊的鎖,比如:
? 可重入鎖
? 公平鎖(遵循先來后到原則)
? 讀寫鎖
? ......
基于 Redis 的分布式鎖,也可以實現上述特性. (當然了對應的實現邏輯也會更復雜).
此處我們不做過多討論了.
實際開發中,我們也并不會真的自己實現一個分布式鎖. 已經有很多現成的庫幫我們封裝好了,我們直
接使用即可.
比如 Java 中的 Redisson,C++ 中的 redis-plus-plus. 當然,有些大廠也會有自己版本的分布式鎖的實現.
redis學習打卡🥳