上篇文章:
Redis原理之緩存https://blog.csdn.net/sniper_fandc/article/details/149141968?fromshare=blogdetail&sharetype=blogdetail&sharerId=149141968&sharerefer=PC&sharesource=sniper_fandc&sharefrom=from_link???????
目錄
1 基本實現
2 引入過期時間
3 引入校驗id
4 引入lua腳本
5 引入看門狗機制(watch dog)
6 引入Redlock算法
????????普通的鎖比如Java中的synchronized通常解決的是線程安全問題,但是到了多進程多服務器環境下,普通的鎖就無法保證多個進程或節點同時訪問同一個資源的問題,這就需要分布式鎖。
????????分布式鎖從本質來講是通過一個公共服務器來記錄加鎖狀態。所有的進程或節點訪問某個資源時先訪問該服務器嘗試加鎖,待某個已經加鎖的進程或節點訪問結束后再解鎖讓其他進程或節點訪問。
????????注意:個人理解的分布式鎖核心問題是進程間通信問題。分布式環境下,進程間通過網絡通信,網絡IO的時間是各種數據IO方式中最慢的(寄存器>緩存>內存>磁盤>網絡),因此進程對一個數據進行訪問就有了時延。而在這個時延期間,如果有其他進程也對該數據進行訪問(失去了原子性),就可能造成數據不一致或重復讀寫等問題,因此需要通過鎖來控制進程通信的順序問題(互斥性)。
1 基本實現
????????注意:實現分布式鎖可以有很多方式,比如Redis、MySQL和ZooKeeper等等。
????????這里使用Redis來實現分布式鎖。核心操作是當多個客戶端進程查詢同一個資源時,請求負載均衡到某個服務器后首先訪問Redis,第一個進程會在Redis設置關于該資源的key-value(加鎖)。此時其他進程(服務器)如果也想查詢該資源就會去Redis嘗試加鎖,如果發現key已經存在就加鎖失敗,可以選擇阻塞或返回(具體哪種看實現場景)。待第一個進程訪問資源結束后,就會把Redis上的這個key-value刪除(解鎖)。
????????使用setnx命令即可實現這個分布式鎖:key不存在則設置(加鎖),否則失敗(加鎖失敗)。使用del命令即可實現解鎖。
2 引入過期時間
????????當在Redis加鎖的服務器如果出現意外情況,比如宕機、斷電等等,鎖還未釋放,此時其它服務器就無法對該資源加鎖,從而無法訪問資源。
????????解決辦法:對key-value引入過期時間,即使用set ex nx命令來設置過期時間。
????????注意:不能使用setnx、expire兩條命令來實現上述效果,因為Redis的命令是串行化執行的,無法保證多條命令執行的原子性。
3 引入校驗id
????????上述方案還有問題,由于多個服務器都可以訪問Redis來執行各種命令,就可能出現一個服務器設置鍵值對(加鎖),另一個服務器刪除鍵值對(解鎖),從而導致問題(當然這個問題不是故意的)。
????????解決辦法:為每個服務器引入校驗id,加鎖時針對某個資源的key的value設置為服務器的校驗id值。當解鎖時,首先查詢準備執行del命令的服務器的校驗id是否和value值相同,如果相同則允許執行del;如果不同則不允許執行。上述過程由服務器來完成:
String key = [要加鎖的資源id];String serverId = [服務器的id];// 加鎖, 設置過期時間為 10sredis.set(key, serverId, "NX", "EX", "10s");// 執行各種業務邏輯, 比如修改數據庫數據.doSomeThing();// 解鎖, 刪除 key. 但是刪除前要檢驗下serverId是否匹配.if (redis.get(key) == serverId) {redis.del(key);}
4 引入lua腳本
????????新的問題又出現了,執行解鎖的過程中,由于get命令和del命令的執行不是原子性的,因此就會出現命令插隊的情況。比如同一個服務器多個線程都需要操作同一個資源,當執行解鎖時按照如下所示順序執行:
????????在線程2執行完get判斷鎖的校驗id和服務器id一樣后,線程1繼續執行del已經解鎖了。而此時服務器2判斷該資源是無鎖狀態就加鎖,此時線程2又執行del把剛加的鎖給解除了,從而引起問題。
????????解決辦法:保證解鎖時兩個命令執行的原子性。可以用redis事務來解決,但是更好的方案是lua腳本。Lua是Redis的內嵌腳本語言,Lua腳本的執行是原子性的:
if redis.call('get',KEYS[1]) == ARGV[1] thenreturn redis.call('del',KEYS[1])elsereturn 0end;
????????KEYS和ARGV是調用腳本時輸入的參數。上述代碼存于.lua后綴的文件,并發送給Redis服務器來執行。
5 引入看門狗機制(watch dog)
????????從引入過期時間開始就一直存留一個問題:過期時間設多長合適?過短的時間可能服務器還沒有訪問完資源鎖就被釋放了;過長的時間可能服務器早已經結束資源訪問但是鎖遲遲無法釋放(影響系統并發量和吞吐量)。
????????解決辦法:動態調整過期時間,初始設置一個時間,待時間快結束時(定期)查詢服務器是否訪問資源結束,如果未結束則延長時間(續約,重新設置過期時間);如果已經結束則通過lua腳本解鎖。這個動態調整過期時間的過程由服務器的一個線程專門負責,該線程也被成為看門狗,因此這個機制就被稱為看門狗機制。
????????上述過程,如果服務器宕機,看門狗線程也就掛了,此時key的過期時間無人續約,也就快速解鎖便于讓其它服務器進程能及時訪問資源。
6 引入Redlock算法
????????通常Redis節點的部署不是單機形式,至少是主從復制。主從復制存在從節點和主節點短時間的數據不一致,假設向master進行加鎖操作,master還未及時同步給slave就故障,此時其它服務器也就可以進行加鎖操作。
????????解決辦法:引入Redlock算法,具體而言,加鎖時不是向一個master節點寫入,而是向所有的master節點都加鎖。如果超過超時時間仍未加鎖成功,則認為對該master加鎖失敗(該節點可能宕機)。當向所有master都嘗試加鎖后,統計加鎖成功的節點數超過半數master節點數,則認為加鎖成功。通過這樣,即使有master節點掛了,其它master節點仍然保存著鎖的信息。
????????解鎖時,也要向所有的master節點嘗試解鎖。即Redlock算法的核心思想就是冗余備份,同時結合分布式的少數服從多數的思想。