實現(xiàn)分布式鎖
分布式鎖的實現(xiàn)通常有三種方式,利用MySQL、zookeeper、Redis3種組件實現(xiàn)。
MySQL實現(xiàn)
通過MySQL實現(xiàn)分布式鎖相對來說比較好理解,主要思路就是通過主鍵自增長屬性來實現(xiàn),通常也叫AUTO-INC Locking自增長鎖。在InnnoDB存儲引擎的內(nèi)存結(jié)構(gòu)中,對每個含有自增長值的表都有一個自增長計數(shù)器(auto increment counter)。當(dāng)含有自增長計數(shù)器的表進(jìn)行插入操作時,這個計數(shù)器會被初始化,執(zhí)行如下的語句來得到計數(shù)器的值:
select MAX(auto_inc_col) from t for update;插入操作會依據(jù)這個自增長的計數(shù)器值+1賦予自增長序列,這個實現(xiàn)方式稱作AUTO-INC Locking。這種鎖其實是采用一種特殊的表鎖機(jī)制,為了提高插入性能,鎖不是在一個事物完成后才釋放的,而是在完成對自增長序列插入的SQL語句后立即釋放。
通過偽代碼模擬實現(xiàn)過程:
參考鏈接:https://juejin.cn/post/6844903688088059912
對于分布式鎖我們可以創(chuàng)建一個鎖表:

實現(xiàn)邏輯
為了達(dá)到可重入鎖的效果那么我們應(yīng)該先進(jìn)行查詢,如果有值,那么需要比較node_info是否一致,這里的node_info可以用機(jī)器IP和線程名字來表示,如果一致那么就加可重入鎖count的值,如果不一致那么就返回false。如果沒有值那么直接插入一條數(shù)據(jù)。需要注意的是這一段代碼需要加事務(wù),必須要保證這一系列操作的原子性。

阻塞式獲取鎖
如果獲取不到就睡眠3ms,繼續(xù)獲取直到拿到鎖。

非阻塞式獲取鎖
如果獲取不到那么就會馬上返回

釋放鎖
unlock的話如果這里的count為1那么可以刪除,如果大于1那么需要減去1。

總結(jié)
適用場景: Mysql分布式鎖一般適用于資源不存在數(shù)據(jù)庫,如果數(shù)據(jù)庫存在比如訂單,那么可以直接對這條數(shù)據(jù)加行鎖,不需要我們上面多的繁瑣的步驟,比如一個訂單,那么我們可以用select * from order_table where id = 'xxx' for update進(jìn)行加行鎖,那么其他的事務(wù)就不能對其進(jìn)行修改。
優(yōu)點:理解起來簡單,不需要維護(hù)額外的第三方中間件(比如Redis,Zk)。
缺點:雖然容易理解但是實現(xiàn)起來較為繁瑣,需要自己考慮鎖超時,加事務(wù)等等。性能局限于數(shù)據(jù)庫,一般對比緩存來說性能較低。對于高并發(fā)的場景并不是很適合。
Zookeeper實現(xiàn)
利用zookeeper的相同路徑下的節(jié)點不能重名的特性和自帶的監(jiān)聽機(jī)制。zookeeper有三類節(jié)點:
持久性節(jié)點:只要創(chuàng)建了節(jié)點,無論客戶端是否斷開鏈接,節(jié)點都會存在。
臨時性節(jié)點:一旦客戶端斷開鏈接,服務(wù)端不再保存該節(jié)點。
順序性節(jié)點:在創(chuàng)建節(jié)點的時候,zookeeper會自動給節(jié)點分配自增長編號,比如/lock/node_000001、/lock/node_000002等。
/lock是我們用于加鎖的目錄,/resource是我們鎖定的資源,其下面的節(jié)點按照我們加鎖的順序排列。

通過持久性節(jié)點實現(xiàn)分布式鎖
實現(xiàn)步驟
step1:client1創(chuàng)建了resource節(jié)點,創(chuàng)建成功即代表持有了鎖,client2再去創(chuàng)建resource節(jié)點時就會失敗,這個時候只能監(jiān)聽這個節(jié)點的變化。
step2:client1處理完業(yè)務(wù)后,刪除resource節(jié)點;client2得到通知后再去創(chuàng)建resource節(jié)點,獲取鎖。(多個client會并發(fā)的去競爭創(chuàng)建resource節(jié)點)
缺點
當(dāng)client1掛掉后沒能刪除resource,那么就出現(xiàn)了死鎖。
存在驚群效應(yīng),當(dāng)client很多時,只有一個client持有鎖,其他所有client都要監(jiān)聽這一個resource節(jié)點。
通過臨時性節(jié)點實現(xiàn)分布式鎖
臨時節(jié)點和永久節(jié)點的實現(xiàn)方式一樣,只不過在client1掛掉后,zookeeper會自動刪除resource節(jié)點,相當(dāng)于強(qiáng)制釋放了鎖。這樣就不會出現(xiàn)死鎖的風(fēng)險了。雖然臨時性節(jié)點解決了死鎖的問題,但是沒能解決鯨群效應(yīng)問題。
通過臨時性順序節(jié)點實現(xiàn)分布式鎖
在resource鎖資源下按照獲取鎖的順序為每個client維護(hù)一個臨時性的順序節(jié)點,每個節(jié)點只需要監(jiān)聽前一個節(jié)點狀態(tài),這樣只有前一個節(jié)點被刪除后,后面的監(jiān)聽節(jié)點就可以創(chuàng)建resource/xxxxx資源了。這樣就解決了驚群效應(yīng)鎖帶來的問題了。
Curator
Curator封裝了Zookeeper底層的Api,使我們更加容易方便的對Zookeeper進(jìn)行操作,并且它封裝了分布式鎖的功能,這樣我們就不需要再自己實現(xiàn)了。
Curator實現(xiàn)了可重入鎖(InterProcessMutex),也實現(xiàn)了不可重入鎖(InterProcessSemaphoreMutex)。在可重入鎖中還實現(xiàn)了讀寫鎖。
Redis實現(xiàn)
redis是單線程受理請求的。通過Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在時,為 key 設(shè)置指定的值。設(shè)置成功,返回 1 。設(shè)置失敗,返回 0 。當(dāng)返回結(jié)果為1時我們可以認(rèn)為該client持有鎖,當(dāng)client處理完業(yè)務(wù)后執(zhí)行del key刪除該鍵相當(dāng)于釋放鎖。
死鎖
這種方式雖然可以實現(xiàn)分布式鎖,但是也存在死鎖問題。如果持鎖的client掛掉了,此時該key會一直存在其他client就獲取不到鎖了。所以對key要加入過期時間限制,加入過期時間需要和setNx同一個原子操作,在Redis2.8之前我們需要使用Lua腳本達(dá)到我們的目的,但是redis2.8之后redis支持nx和ex操作是同一原子操作。
set resourceName value ex 5 nx鎖失效
雖然通過給key設(shè)置有效期能解決死鎖問題,但是又引發(fā)了一個新問題鎖失效,就是如果client1不是掛掉了,而是業(yè)務(wù)處理耗時長,在5ms之后Redis主動把這個key刪除了,那么另外的client2就可以獲取到鎖了,此時就存在了兩個client持有鎖的情況。這個時候其實可以把超時設(shè)置這一環(huán)節(jié)交給各個client來完成,具體思路就是客戶端client1創(chuàng)建一個key的時候,設(shè)置對應(yīng)的value為超時時間,可以用當(dāng)前時間戳+(過期窗口),這樣當(dāng)client2獲取到這個值之后發(fā)現(xiàn)當(dāng)前系統(tǒng)時間已經(jīng)超過value了,那么代表client1沒有釋放鎖,這個時候client2需要通過Redis提供的 GETSET KEY VALUE來獲取鎖并重置過期時間,之所以要用到GETSET就是為了在client2獲取鎖之后處理業(yè)務(wù)的時候,client3、client4也能夠發(fā)現(xiàn)key過期了并獲取到了鎖的問題。
Redission
Javaer都知道Jedis,Jedis是Redis的Java實現(xiàn)的客戶端,其API提供了比較全面的Redis命令的支持。Redission也是Redis的客戶端,相比于Jedis功能簡單。Jedis簡單使用阻塞的I/O和redis交互,Redission通過Netty支持非阻塞I/O。Jedis最新版本2.9.0是2016年的快3年了沒有更新,而Redission最新版本是2018.10月更新。Redission封裝了鎖的實現(xiàn),其繼承了java.util.concurrent.locks.Lock的接口,讓我們像操作我們的本地Lock一樣去操作Redission的Lock。

