分佈式鎖知識總結
爲什麼要用分佈式鎖?
在單體架構中,我們可以通過sychronized來保證併發安全,但是在分佈式架構中,sychronized沒用,sychronized只能保證一個進程(JVM)中的線程安全,無法跨進程
分佈式鎖的執行流程
分佈式鎖的實現方式
1、redis實現分佈式鎖
最簡單的redis實現就是通過 redis的setnx(set if not exists)命令來實現
RedisTemplate對redis進行了封裝,使用redistemplate就能實現 redis的setnx(set if not exists)命令
bool result = stringRedisTemplate.opsForValue().setIfAbsent( "key","value" ); // setIfAbsent方法就相當於 setnx命令
setIfAbsent方法 返回 true 說明key不存在,插入數據成功
setIfAbsent方法 返回 true 說明key已經存在了,插入數據失敗
大概的代碼邏輯就是
bool result = stringRedisTemplate.opsForValue().setIfAbsent( "key","value" ); // 設置key(相當於競爭鎖 )
if(!result){
return "key已經存在";
}
執行業務邏輯代碼(比如扣庫存)
stringRedisTemplate.delete("key"); // 刪除key (相當於釋放鎖)
想想這裏面可能出現的問題:
1、線程在運行業務代碼的時候出現異常,那麼此時就無法釋放鎖,就會形成死鎖。怎麼辦? =====》try finally ,在finally中執行delete操作
2、線程在執行業務代碼的時候,服務器宕機了,此時也無法釋放鎖,也會形成死鎖。怎麼辦? =====》 給 鎖的key設置一個timeout
int timeout = 10;
stringRedisTemplate.expire("key",timeout ,TimeUnit.SECONDS);
3、此時加鎖與設置timeout之間是有時間損耗的,也就是說如果線程在加完鎖正準備設置timeout時,此時服務宕機了,timeout就設置不了,就跟上面情況類似了,也會產生死鎖,怎麼辦? ================》 保證 加鎖與設置timeout 的原子性 ( RedisTemplate 提供了一個重載的setIfAbsent方法來保證原則性,此方法底層就是使用了lua腳本)
redis會將lua腳本中所有的命令當作一個原子操作執行
int timeout = 10;
// bool result = stringRedisTemplate.opsForValue().setIfAbsent( "key","value" );
// stringRedisTemplate.expire("key",timeout ,TimeUnit.SECONDS);
bool result = stringRedisTemplate.opsForValue().setIfAbsent( "key","value" ,timeout ,TimeUnit.SECONDS); // 這行代碼就相當於把上面兩行代碼進行了原子性操作
4、線程 1 還沒執行完業務代碼,此時鎖timeout了,redis把線程1 加的鎖刪了 ,此時線程 2 過來了加了鎖,然後執行業務代碼,注意! 此時線程 1 和線程 2 可能同時操作共享資源 ,然後線程1 此時執行完了刪鎖,注意! 此時線程 1 刪除的鎖是線程 2 加的鎖,然後此時線程 3過來了加了鎖,然後執行業務代碼 ,注意! 此時線程 2 和線程 3可能同時操作共享資源,然後線程2 此時執行完了刪鎖,注意! 此時線程2 刪除的鎖是線程 3 加的鎖,......往復循環 ,導致鎖永久失效。
怎麼辦?=====》給自己加的鎖 加一個標識,這個標識只有自己知道,此時鎖就只能自己釋放,別人就釋放不了了
5、線程 1 還沒執行完業務代碼,此時鎖timeout了,redis把線程1 加的鎖刪了 ,此時線程 2 過來了加了鎖,然後執行業務代碼,注意! 此時線程 1 和線程 2 可能同時操作共享資源 ,然後線程1 此時執行完了業務代碼之後刪鎖,哦吼,我的鎖怎麼沒了,然後就報錯了。怎麼辦?
======》加大timeout的值
=======》但是有個問題,timeout越大 ====== 》 服務宕機之後,死鎖時間越長 =======》其他線程等待的時間越長 ======》用戶的脾氣越大。 怎麼辦?
======》開啓一個異步線程,開啓一個定時任務,每個 1/3 timeout掃描一次,看看當前鎖的key 還在不在,當前線程還在不在,是不是死(宕機 )了,然後線程還在,鎖的key也還在,那麼就給鎖續命,重新設置鎖的key 的 timeout ,只要線程沒執行完就給鎖的key續命,知道線程執行完自動刪除鎖的key(釋放鎖)
此時就不得不介紹一個神奇的框架了 !!!!redisson框架
redisson框架 ---- 一個 redis java client
redisson框架 中解決了上面,底層就是用的setnx ,lua腳本保證原子性,鎖的key默認timeout 30s ,其他線程自旋 , 用timerTask 定時任務 默認 每隔 1/3 timeout 續命一次。
redisson分佈式鎖實現原理:
redisson框架 使用如下:
6、如果redis用了集羣(一主多從),線程 1 的鎖的key剛把鎖的key存入redis中, redis的master 主節點掛了,鎖的key還沒同步到slave從節點,然後此時slave從節點升爲 master主節點,但是此時這個新的master主節點中沒有線程 1 的鎖的key ,但是此時線程 1 還在執行業務代碼,然後纔是 線程 2 來了,由於此時新的master主節點上沒有鎖的key,所以線程2申請鎖成功,然後此時線程 1 和 線程 2 就可能同時訪問並使用共享資源 。 此時就有問題了呀! 怎麼辦?
=======》①用zookeeper 有延時 ,要同步半數以上的follower才能加鎖成功,所以此時不用擔心leader掛了,follower中沒有鎖的key ②人工補償 ③redis自己解決 用redlock
redlock 實現原理
大概就是搞多個對等的redis節點,節點之間沒有依賴(主從依賴),通過 setnx命令 加鎖 ,同時發給每個redis節點,發送的這些節點中要有半數以上的redis節點加鎖成功,纔算認爲線程拿到了鎖,才繼續往下執行業務。(原理跟zookeeper類似 , 犧牲了性能,如果沒超過半數,涉及到鎖回滾問題)
7、高併發性下請求到了redis,但是redis是單線程工作模式,在redis中就也不是併發執行,而是串行執行,影響性能。怎麼辦?
====》 用庫存舉例, 將庫存分段存儲,分段加鎖=======》把每段的鎖的key放到redis集羣中,把不同鎖的key放到不同的redis master主節點上
一個段的庫存不夠減怎麼辦?去減下面段的,合併幾個段一起扣減。
2、zookeeper實現分佈式鎖
由於zookeeper上面的臨時節點是唯一的,只會創建一個,而鎖也只有一把,所以它能代表這個鎖,多個客戶端去搶這個鎖,也就是去zookeeper上面創建臨時節點,看誰創建的快,誰第一個創建了這個臨時節點誰就獲取到了鎖。
此時clientA第一個成功創建了臨時節點,就代表clientA已經獲取到了鎖,其他client再去創建這個臨時節點,發現這個節點已經存在了,他們就不能去創建了,就都阻塞了,他們一直監聽這個節點的變化,也就是在監聽這個鎖的變化(通過zookeeper Watch功能)
clientA完成了它的業務操作 ,結束了與zookeeper的會話 臨時節點被自動刪除,就代表釋放了鎖,此時其他client監聽到了臨時節點被刪除了(鎖釋放了),就都會去競爭鎖,也就是去創建臨時節點。
同一時間有多個客戶端在競爭鎖
======》上面這個實現方式有什麼問題呢 ?想想如果上千個client去監聽這個臨時節點的變化,一旦這個臨時節點變化了,然後此時就是上千個client去競爭這個鎖(羊羣效應 / 驚羣效應),這對於zookeeper的壓力是非常大的,所以這個方案不可行。那怎麼辦呢?
======》使用臨時順序節點 + 監聽
使用臨時順序節點創建出來的節點都是有序的,只有最小的節點能拿到鎖,其他的節點監聽比它小的前一個節點。
獲取鎖大概流程: 每個client 分別 創建臨時順序節點,然後誰的節點最小就誰能拿到鎖,其他節點就監聽比自己小的前一個節點
比如下圖:A B C D分別創建節點,A最先創建,所以它的節點編號最小,所以它能獲取鎖,然後B監聽A,C監聽B,D監聽C
釋放鎖大概流程:clientA 執行完自己的業務之後結束了與zookeeper的會話,會話一結束,節點會自動刪除(釋放了鎖),同時clientB監聽到了clientA的這個節點被刪除後,會去判斷自己的節點是不是最小的節點,如果是最小的就獲取鎖,如果不是就繼續監聽等待,不做任務處理,clientA節點刪除只對clientB有影響,對後面的 C D 等節點沒有影響,因爲C只監聽B,B是沒有變化的,所以C不會受到影響,不會變,D只監聽C,C沒有變,D也不會有影響,也不會變.......就解決的羊羣(驚羣)效應。
同一時間只有一個客戶端在競爭鎖
大概流程: 自己看圖 , 直接通過 curator框架就可以實現, curator已經將這些問題解決了,封裝好了,直接用就行。
3、數據庫實現分佈式鎖
基於數據庫表(主要原理:數據庫的主鍵不能重複,作爲分佈式鎖的實現)
0、首先先創建一個tb_lock表,用於記錄當前哪個線程正在使用數據,表裏就一個 業務ID 或者業務名稱 字段 (唯一主鍵),當作鎖
1、當線程要訪問數據時,會先將要執行的業務的業務id或者業務名稱 ,insert插入tb_lock表中
2、當插入成功,代表該線程獲得了鎖,即可執行業務邏輯
3、當其他線程在執行相同業務的時候也會先將要執行業務的 業務id或者業務名稱 插入tb_lock表中,由於主鍵衝突,此時會導致插入失敗,就代表獲取鎖失敗
4、獲取鎖成功的線程在執行完業務代碼後,在刪除tb_lock表中刪除對應業務的 業務id或者業務名稱 ,代表釋放鎖
想想這樣實現會出現問題嗎?
1、一旦數據庫掛掉,會導致業務系統不可用。
====》用兩個數據庫,一主一從,數據之前雙向同步。一旦掛掉快速切換到備庫上。
2、這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖。
====》做一個異步定時任務,每隔一定時間把數據庫中的超時數據清理一遍。
3、這把鎖只能是非阻塞的,因爲數據的insert操作,一旦插入失敗就會直接報錯。沒有獲得鎖的線程並不會進入排隊隊列,要想再次獲得鎖就要再次觸發獲得鎖操作。
====》用while循環,直到insert成功再返回成功。
4、這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因爲數據中數據已經存在了。
====》在數據庫表中加個字段,記錄當前獲得鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,如果當前機器的主機信息和線程信息在數據庫可以查到的話,直接把鎖分配給他就可以了。
基於數據庫表的排它鎖實現(基於MySql的InnoDB引擎)
1、在執行業務邏輯之前,先通過一個帶有for update 的查詢語句 拿到排它鎖
select * from table where productId = 1 for update
行鎖 : 當前連接要執行的帶有 for update 的SQL語句以後,指定了主鍵查詢,代表當前連接鎖定了這條數據(productId = 1)
select * from table for update
表鎖:當前連接執行帶有for update的SQL語句以後,沒有指定主鍵查詢,那麼會將表進行鎖定,只有當前連接可以對這張表進行操作
2、獲得排它鎖的線程即可獲得分佈式鎖,執行業務邏輯
3、執行完業務邏輯之後,再通過JDBC的connection.commit() 方法來釋放鎖
想想這樣實現會出現問題嗎?
1、一旦數據庫掛掉,會導致業務系統不可用。
====》使用這種方式,此問題不會發生,服務宕機之後數據庫會自己把鎖釋放掉。
2、這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖。
====》做一個異步定時任務,每隔一定時間把數據庫中的超時數據清理一遍。
3、這把鎖只能是非阻塞的,因爲數據的insert操作,一旦插入失敗就會直接報錯。沒有獲得鎖的線程並不會進入排隊隊列,要想再次獲得鎖就要再次觸發獲得鎖操作。
====》使用這種方式,此問題不會發生,for update
語句會在執行成功後立即返回,在執行失敗時一直處於阻塞狀態,直到成功。
4、這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因爲數據中數據已經存在了。
====》在數據庫表中加個字段,記錄當前獲得鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,如果當前機器的主機信息和線程信息在數據庫可以查到的話,直接把鎖分配給他就可以了。