農林漁牧網

您現在的位置是:首頁 > 畜牧業

一文搞定Redis分散式鎖的實現和原理

2022-04-22由 Java架構成長之路 發表于 畜牧業

3個結點的樹有幾種形態

為什麼需要分散式鎖

我們知道,當多個執行緒併發操作某個物件時,可以透過synchronized來保證同一時刻只能有一個執行緒獲取到物件鎖進而處理synchronized關鍵字修飾的程式碼塊或方法。既然已經有了synchronized鎖,為什麼這裡又要引入分散式鎖呢?

因為現在的系統基本都是分散式部署的,一個應用會被部署到多臺伺服器上,synchronized只能控制當前伺服器自身的執行緒安全,並不能跨伺服器控制併發安全。比如下圖,同一時刻有4個執行緒新增同一件商品,其中兩個執行緒由伺服器A處理,另外兩個執行緒由伺服器B處理,那麼最後的結果就是兩臺伺服器各執行了一次新增動作。這顯然不符合預期。

一文搞定Redis分散式鎖的實現和原理

而本篇文章要介紹的分散式鎖就是為了解決這種問題的。

什麼是分散式鎖

分散式鎖,就是

控制分散式系統中不同程序共同訪問同一共享資源的一種鎖的實現。

所謂當局者迷,旁觀者清,先舉個生活中的例子,就拿高鐵舉例,每輛高鐵都有自己的執行路線,但這些路線可能會與其他高鐵的路線重疊,如果只讓高鐵內部的司機操控路線,那就可能出現撞車事故,因為司機不知道其他高鐵的執行路線是什麼。所以,中控室就發揮作用了,中控室會監控每輛高鐵,高鐵在什麼時間走什麼樣的路線全部由中控室指揮。

分散式鎖就是基於這種思想實現的,它需要在我們分散式應用的外面使用一個第三方元件(可以是資料庫、Redis、Zookeeper等)進行全域性鎖的監控,由這個元件決定什麼時候加鎖,什麼時候釋放鎖。

一文搞定Redis分散式鎖的實現和原理

Redis如何實現分散式鎖

在聊Redis如何實現分散式鎖之前,我們要先聊一下redis的一個命令:setnx key value。我們知道,Redis設定一個key最常用的命令是:set key value,該命令不管key是否存在,都會將key的值設定成value,並返回成功:

一文搞定Redis分散式鎖的實現和原理

setnx key value 也是設定key的值為value,不過,它會先判斷key是否已經存在,如果key不存在,那麼就設定key的值為value,並返回1;如果key已經存在,則不更新key的值,直接返回0:

一文搞定Redis分散式鎖的實現和原理

最簡單的版本:setnx key value

基於setnx命令的特性,我們就可以實現一個最簡單的分散式鎖了。我們透過向Redis傳送 setnx 命令,然後判斷Redis返回的結果是否為1,結果是1就表示setnx成功了,那本次就獲得鎖了,可以繼續執行業務邏輯;如果結果是0,則表示setnx失敗了,那本次就沒有獲取到鎖,可以透過迴圈的方式一直嘗試獲取鎖,直至其他客戶端釋放了鎖(delete掉key)後,就可以正常執行setnx命令獲取到鎖。流程如下:

一文搞定Redis分散式鎖的實現和原理

這種方式雖然實現了分散式鎖的功能,但有一個很明顯的問題:沒有給key設定過期時間,萬一程式在傳送delete命令釋放鎖之前宕機了,那麼這個key就會永久的儲存在Redis中了,其他客戶端也永遠獲取不到這把鎖了。

● 升級版本:設定key的過期時間

針對上面的問題,我們可以基於setnx key value的基礎上,同時給key設定一個過期時間。Redis已經提供了這樣的命令:set key value ex seconds nx。其中,ex seconds 表示給key設定過期時間,單位為秒,nx 表示該set命令具備setnx的特性。效果如下:

一文搞定Redis分散式鎖的實現和原理

我們設定name的過期時間為60秒,60秒內執行該set命令時,會直接返回nil。60秒後,我們再執行set命令,可以執行成功,效果如下:

一文搞定Redis分散式鎖的實現和原理

基於這個特性,升級後的分散式鎖流程如下:

一文搞定Redis分散式鎖的實現和原理

這種方式雖然解決了一些問題,但卻引來了另外一個問題:存在鎖誤刪的情況,也就是把別人加的鎖釋放了。例如,client1獲得鎖之後開始執行業務處理,但業務處理耗時較長,超過了鎖的過期時間,導致業務處理還沒結束時,鎖卻過期自動刪除了(相當於屬於client1的鎖被釋放了),此時,client2就會獲取到這把鎖,然後執行自己的業務處理,也就在此時,client1的業務處理結束了,然後向Redis傳送了delete key的命令來釋放鎖,Redis接收到命令後,就直接將key刪掉了,但此時這個key是屬於client2的,所以,相當於client1把client2的鎖給釋放掉了:

一文搞定Redis分散式鎖的實現和原理

● 二次升級版本:value使用唯一值,刪除鎖時判斷value是否當前執行緒的

要解決上面的問題,最省事的做法就是把鎖的過期時間設定長一點,要遠大於業務處理時間,但這樣就會嚴重影響系統的效能,假如一臺伺服器在釋放鎖之前宕機了,而鎖的超時時間設定了一個小時,那麼在這一個小時內,其他執行緒訪問這個服務時就一直阻塞在那裡。所以,一般不推薦使用這種方式。

另一種解決方法就是在set key value ex seconds nx時,把value設定成一個唯一值,每個執行緒的value都不一樣,在刪除key之前,先透過get key命令得到value,然後判斷value是否是自己執行緒生成的,如果是,則刪除掉key釋放鎖,如果不是,則不刪除key。正常流程如下:

一文搞定Redis分散式鎖的實現和原理

當業務處理還沒結束的時候,key自動過期了,也可以正常釋放自己的鎖,不影響其他執行緒:

一文搞定Redis分散式鎖的實現和原理

二次升級後的方案看起來似乎已經沒什麼問題了,但其實不然。仔細分析流程後我們發現,判斷鎖是否屬於當前執行緒和釋放鎖兩個步驟並不是原子操作。正常來說,如果執行緒1透過get操作從Redis中得到的value是123,那麼就會執行刪除鎖的操作,但假如在執行刪除鎖的動作之前,系統卡頓了幾秒鐘,恰好在這幾秒鐘內,key自動過期了,執行緒2就順利獲取到鎖開始執行自己的邏輯了,此時,執行緒1卡頓恢復了,開始繼續執行刪除鎖的動作,那麼此時刪除的還是執行緒2的鎖。

一文搞定Redis分散式鎖的實現和原理

● 終極版本:Lua指令碼

針對上述Redis原始命令無法滿足部分業務原子性操作的問題,Redis提供了Lua指令碼的支援。Lua指令碼是一種輕量小巧的指令碼語言,它支援原子性操作,Redis會將整個Lua指令碼作為一個整體執行,中間不會被其他請求插入,因此Redis執行Lua指令碼是一個原子操作。

在上面的流程中,我們把get key value、判斷value是否屬於當前執行緒、刪除鎖這三步寫到Lua指令碼中,使它們變成一個整體交個Redis執行,改造後流程如下:

一文搞定Redis分散式鎖的實現和原理

這樣改造之後,就解決了釋放鎖時取值、判斷值、刪除鎖等多個步驟無法保證原子操作的問題了。關於Lua指令碼的語法可以自行學習,並不複雜,很簡單,這裡就不做過多講述。

既然Lua指令碼可以在釋放鎖時使用,那肯定也能在加鎖時使用,而且一般情況下,推薦使用Lua指令碼,因為在使用上面set key value ex seconds nx命令加鎖時,並不能做到重入鎖的效果,也就是當一個執行緒獲取到鎖後,在沒有釋放這把鎖之前,當前執行緒自己也無法再獲得這把鎖,這顯然會影響系統的效能。使用Lua指令碼就可以解決這個問題,我們可以在Lua指令碼中先判斷鎖(key)是否存在,如果存在則再判斷持有這把鎖的執行緒是否是當前執行緒,如果不是則加鎖失敗,否則當前執行緒再次持有這把鎖,並把鎖的重入次數+1。在釋放鎖時,也是先判斷持有鎖的執行緒是否是當前執行緒,如果是則將鎖的重入次數-1,直至重入次數減至0,即可刪除該鎖(key)。

一文搞定Redis分散式鎖的實現和原理

實際專案開發中,其實基本不用自己寫上面這些分散式鎖的實現邏輯,而是使用一些很成熟的第三方工具,當下比較流行的就是Redisson,它既提供了Redis的基本命令的封裝,也提供了Redis分散式鎖的封裝,使用非常簡單,只需直接呼叫相應方法即可。但工具雖然好用,底層原理還是要理解的,這就是本篇文章的目的。

點關注不迷路,跟我一起學技術!

關注同名微信公眾號【Java架構成長之路】獲取更多文章~