終於懂什麼是分佈式鎖

爲何要有分佈式鎖?

模擬一個秒殺接口:
image.png
商品表:
image.png
單機狀況下,用Jmeter發送1000個請求過來:
image.png
image.png
因爲加了sychronized進行方法同步,結果正常。nginx

如今模擬集羣環境,仍是用上面的接口,但啓動兩個服務,分別是8080和8081端口,用nginx負載均衡到兩個tomcat,用Jmeter發送1000個請求到nginx:
image.png
image.png
image.png
image.png
發現庫存並無-1000,而且控制的庫存量打印有重複。git

結論:
咱們在系統中修改已有數據時,須要先讀取,而後進行修改保存,此時很容易遇到併發問題。因爲修改和保存不是原子操做,在併發場景下,部分對數據的操做可能會丟失。在單服務器系統咱們經常使用本地鎖來避免併發帶來的問題,然而,當服務採用集羣方式部署時,本地鎖沒法在多個服務器之間生效,這時候保證數據的一致性就須要分佈式鎖來實現。github

MySql分佈式鎖

基於數據庫的分佈式鎖, 經常使用的一種方式是使用表的惟一約束特性。當往數據庫中成功插入一條數據時, 表明只獲取到鎖。將這條數據從數據庫中刪除,則釋放鎖。redis

CREATE TABLE `database_lock` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `resource` varchar(1024) NOT NULL DEFAULT "" COMMENT '資源',
    `lock_id` varchar(1024) NOT NULL DEFAULT "" COMMENT '惟一鎖編碼',
    `count` int(11) NOT NULL DEFAULT '0' COMMENT '鎖的次數,可重入鎖',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uiq_lock_id` (`lock_id`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='數據庫分佈式鎖表';

當咱們想要得到鎖時,能夠插入一條數據
INSERT INTO database_lock(resource,lock_id,count) VALUES ("resource","lock_id",1);數據庫

注意:在表database_lock中,lock_id字段作了惟一性約束,能夠是機器的mac地址+線程編號,這樣若是有多個請求同時提交到數據庫的話,數據庫能夠保證只有一個操做能夠成功(其它的會報錯:ERROR 1062 (23000): Duplicate entry ‘1’ for key ‘uiq_lock_id’),那麼咱們就能夠認爲操做成功的那個請求得到了鎖。tomcat

當須要釋放鎖的時,能夠刪除這條數據:
DELETE FROM database_lock where method_name ='resource' and cust_id = 'lock_id'服務器

可重入鎖:
UPDATE database_lock SET count = count + 1 WHERE method_name ='resource' AND cust_id = 'lock_id'網絡

僞代碼:併發

public void test(){
    String resource = "resource";
 String lock_id = "lock_id";
 if(!checkReentrantLock(resource,lock_id)){
        lock(resource,lock_id);//加鎖
 }else{
        reentrantLock(resource,lock_id); //可重入鎖+1
 }
    //業務處理
 unlock(resource,lock_id);//釋放鎖
}

這種實現方式很是的簡單,可是須要注意如下幾點:負載均衡

  1. 這種鎖沒有失效時間,一旦釋放鎖的操做失敗就會致使鎖記錄一直在數據庫中,其它線程沒法得到鎖。這個缺陷也很好解決,好比能夠作一個定時任務去定時清理。
  2. 這種鎖的可靠性依賴於數據庫。建議設置備庫,避免單點,進一步提升可靠性。
  3. 這種鎖是非阻塞的,由於插入數據失敗以後會直接報錯,想要得到鎖就須要再次操做。若是須要阻塞式的,能夠弄個for循環、while循環之類的,直至INSERT成功再返回。
  4. 這種鎖也是非可重入的,由於同一個線程在沒有釋放鎖以前沒法再次得到鎖,由於數據庫中已經存在同一份記錄了。想要實現可重入鎖,能夠在數據庫中添加一些字段,好比得到鎖的主機信息、線程信息等,那麼在再次得到鎖的時候能夠先查詢數據,若是當前的主機信息和線程信息等能被查到的話,能夠直接把鎖分配給它。

Redis分佈式鎖

Redis 鎖主要利用 Redis 的 setnx 命令。

  • 加鎖命令:SETNX key value,當鍵不存在時,對鍵進行設置操做並返回成功,不然返回失敗。KEY 是鎖的惟一標識,通常按業務來決定命名。
  • 解鎖命令:DEL key,經過刪除鍵值對釋放鎖,以便其餘線程能夠經過 SETNX 命令來獲取鎖。
  • 鎖超時:EXPIRE key timeout, 設置 key 的超時時間,以保證即便鎖沒有被顯式釋放,鎖也能夠在必定時間後自動釋放,避免資源被永遠鎖住。
if (setnx(key, 1) == 1){
    expire(key, 30)
    try {
        //TODO 業務邏輯
    } finally {
        del(key)
    }
}

使用SpingBoot集成Redis後使用分佈式鎖:
image.png
image.png
image.png
image.png
能夠看到打印的日誌再也不有重複的庫存量,最小的庫存量與數據庫中的一致。

Redis分佈式鎖可能存在一些問題:
1.設置過時時間
A客戶端獲取鎖成功,可是在釋放鎖以前崩潰了,此時該客戶端實際上已經失去了對公共資源的操做權,但卻沒有辦法請求解鎖(刪除 Key-Value 鍵值對),那麼,它就會一直持有這個鎖,而其它客戶端永遠沒法得到鎖。
在加鎖時爲鎖設置過時時間,當過時時間到達,Redis 會自動刪除對應的 Key-Value,從而避免死鎖。
2.SETNX 和 EXPIRE 非原子性
若是SETNX成功,在設置鎖超時時間以前,服務器掛掉、重啓或網絡問題等,致使EXPIRE命令沒有執行,鎖沒有設置超時時間變成死鎖。Redis 2.8 以後 Redis 支持 nx 和 ex 操做是同一原子操做。
3.鎖誤解除
若是線程 A 成功獲取到了鎖,而且設置了過時時間 30 秒,但線程 A 執行時間超過了 30 秒,鎖過時自動釋放,此時線程 B 獲取到了鎖;隨後 A 執行完成,線程 A 使用 DEL 命令來釋放鎖,但此時線程 B 加的鎖尚未執行完成,線程 A 實際釋放的線程 B 加的鎖。
經過在 value 中設置當前線程加鎖的標識,在刪除以前驗證 key 對應的 value 判斷鎖是不是當前線程持有。可生成一個 UUID 標識當前線程
image.png
4.超時解鎖致使併發
若是線程 A 成功獲取鎖並設置過時時間 30 秒,但線程 A 執行時間超過了 30 秒,鎖過時自動釋放,此時線程 B 獲取到了鎖,線程 A 和線程 B 併發執行。
通常有兩種方式解決該問題:
將過時時間設置足夠長,確保代碼邏輯在鎖釋放以前可以執行完成。
爲獲取鎖的線程增長守護線程,爲將要過時但未釋放的鎖增長有效時間。
更好的方法是是使用Redission,WatchDog機制會爲將要過時但未釋放的鎖增長有效時間。
image.png
5.redis主從複製
A客戶端在Redis的master節點上拿到了鎖,可是這個加鎖的key尚未同步到slave節點,master故障,發生故障轉移,一個slave節點升級爲master節點,B客戶端也能夠獲取同個key的鎖,但客戶端A也已經拿到鎖了,這就致使多個客戶端都拿到鎖。
使用RedLock
image.png

  1. 首先生成多個Redis集羣的Rlock,並將其構形成RedLock。
  2. 若是循環加鎖的過程當中加鎖失敗,那麼須要判斷加鎖失敗的次數是否超出了最大值,這裏的最大值是根據集羣的個數,好比三個那麼只容許失敗一個,五個的話只容許失敗兩個,要保證多數成功。
  3. 加鎖的過程當中須要判斷是否加鎖超時,有可能咱們設置加鎖只能用3ms,第一個集羣加鎖已經消耗了3ms了。那麼也算加鎖失敗。
  4. 2,3步裏面加鎖失敗的話,那麼就會進行解鎖操做,解鎖會對全部的集羣在請求一次解鎖。

能夠看見RedLock基本原理是利用多個Redis集羣,用多數的集羣加鎖成功,減小Redis某個集羣出故障,形成分佈式鎖出現問題的機率。

ZooKeeper分佈式鎖

1.多個客戶端建立一個鎖節點下的一個接一個的臨時順序節點
2.若是本身是第一個臨時順序節點,那麼這個客戶端加鎖成功;若是本身不是第一個節點,就對本身上一個節點加監聽器
3.當某個客戶端監聽到上一個節點釋放鎖,本身就排到前面去了,此時繼續執行步驟2,至關因而一個排隊機制。
使用Curator框架進行加鎖和釋放鎖
image.png
image.png
image.png
image.png
image.png

參考:
再有人問你分佈式鎖,這篇文章扔給他
分佈式鎖的實現之 redis 篇
七張圖完全講清楚ZooKeeper分佈式鎖的實現原理

相關文章
相關標籤/搜索