模擬一個秒殺接口:
商品表:
單機狀況下,用Jmeter發送1000個請求過來:
因爲加了sychronized進行方法同步,結果正常。nginx
如今模擬集羣環境,仍是用上面的接口,但啓動兩個服務,分別是8080和8081端口,用nginx負載均衡到兩個tomcat,用Jmeter發送1000個請求到nginx:
發現庫存並無-1000,而且控制的庫存量打印有重複。git
結論:
咱們在系統中修改已有數據時,須要先讀取,而後進行修改保存,此時很容易遇到併發問題。因爲修改和保存不是原子操做,在併發場景下,部分對數據的操做可能會丟失。在單服務器系統咱們經常使用本地鎖來避免併發帶來的問題,然而,當服務採用集羣方式部署時,本地鎖沒法在多個服務器之間生效,這時候保證數據的一致性就須要分佈式鎖來實現。github
基於數據庫的分佈式鎖, 經常使用的一種方式是使用表的惟一約束特性。當往數據庫中成功插入一條數據時, 表明只獲取到鎖。將這條數據從數據庫中刪除,則釋放鎖。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);//釋放鎖 }
這種實現方式很是的簡單,可是須要注意如下幾點:負載均衡
Redis 鎖主要利用 Redis 的 setnx 命令。
if (setnx(key, 1) == 1){ expire(key, 30) try { //TODO 業務邏輯 } finally { del(key) } }
使用SpingBoot集成Redis後使用分佈式鎖:
能夠看到打印的日誌再也不有重複的庫存量,最小的庫存量與數據庫中的一致。
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 標識當前線程
4.超時解鎖致使併發
若是線程 A 成功獲取鎖並設置過時時間 30 秒,但線程 A 執行時間超過了 30 秒,鎖過時自動釋放,此時線程 B 獲取到了鎖,線程 A 和線程 B 併發執行。
通常有兩種方式解決該問題:
將過時時間設置足夠長,確保代碼邏輯在鎖釋放以前可以執行完成。
爲獲取鎖的線程增長守護線程,爲將要過時但未釋放的鎖增長有效時間。
更好的方法是是使用Redission,WatchDog機制會爲將要過時但未釋放的鎖增長有效時間。
5.redis主從複製
A客戶端在Redis的master節點上拿到了鎖,可是這個加鎖的key尚未同步到slave節點,master故障,發生故障轉移,一個slave節點升級爲master節點,B客戶端也能夠獲取同個key的鎖,但客戶端A也已經拿到鎖了,這就致使多個客戶端都拿到鎖。
使用RedLock
能夠看見RedLock基本原理是利用多個Redis集羣,用多數的集羣加鎖成功,減小Redis某個集羣出故障,形成分佈式鎖出現問題的機率。
1.多個客戶端建立一個鎖節點下的一個接一個的臨時順序節點
2.若是本身是第一個臨時順序節點,那麼這個客戶端加鎖成功;若是本身不是第一個節點,就對本身上一個節點加監聽器
3.當某個客戶端監聽到上一個節點釋放鎖,本身就排到前面去了,此時繼續執行步驟2,至關因而一個排隊機制。
使用Curator框架進行加鎖和釋放鎖
參考:
再有人問你分佈式鎖,這篇文章扔給他
分佈式鎖的實現之 redis 篇
七張圖完全講清楚ZooKeeper分佈式鎖的實現原理