1、背景:單體架構中使用同步訪問解決多線程併發問題,分佈式中須要有其餘方案。redis
2、分佈式鎖的考量:算法
1.能夠保證在分佈式部署的應用集羣中,同一個方法在同一時間只能被一臺機器-上的一個線程執行。
2.這把鎖要是一把可重入鎖(避免死鎖)
3.這把鎖最好是一把阻塞鎖(根據業務需求考慮要不要這條)
4.這把鎖最好是一把公平鎖(根據業務需求考慮要不要這條)
5.有高可用的獲取鎖和釋放鎖功能
保證了只有鎖的持有者才能來解鎖,不然任何競爭者都能解鎖
6.獲取鎖和釋放鎖的性能要好
7.若是作的好一點,須要有監控的平臺。數據庫
3、分佈式鎖的三種實現方式
1.基於數據庫實現排他鎖:利用version字段和for update操做獲取鎖。安全
優勢:易於理解
問題:
(1)鎖沒有失效時間,解鎖失敗時(宕機等緣由),其餘線程獲取不到鎖。
解決:作一個定時任務實現自動釋放鎖。多線程
(2)鎖屬於非阻塞,由於獲取鎖的是insert操做,一旦獲取失敗就報錯,沒有得到鎖的線程並不會進入排隊隊列,要想再次得到鎖就要再次觸發得到鎖操做。
解決:搞一個while循環,直到insert成功再返回成功。架構
(3)不是可重入鎖。
解決:加入鎖的機器字段,實現同一機器可重複加鎖。
另外在解鎖時,必須是鎖的持有者來解鎖,其餘競爭者沒法解鎖併發
(4)因爲是數據庫,對性能要求高的應用不合適用此實現。
解決:數據庫自己特性決定。
(5)在 MySQL 數據庫中採用主鍵衝突防重,在大併發狀況下有可能會形成鎖表現象。dom
解決:比較好的辦法是在程序中生產主鍵進行防重分佈式
(6)這把鎖是非公平鎖,全部等待鎖的線程憑運氣去爭奪鎖性能
解決:再建一張中間表,將等待鎖的線程全記錄下來,並根據建立時間排序,只有最早建立的容許獲取鎖。
(7)考慮到數據庫單點故障,須要實現數據庫的高可用。
注意:InnoDB 引擎在加鎖的時候,只有經過索引進行檢索的時候纔會使用行級鎖,不然會使用表級鎖
另外存在問題:
(1)行級鎖並不必定靠譜:雖然咱們對方法字段名使用了惟一索引,而且顯示使用 for update 來使用行級鎖。
可是,MySQL 會對查詢進行優化,即使在條件中使用了索引字段,可是否使用索引來檢索數據是由 MySQL 經過判斷不一樣執行計劃的代價來決定的,
若是MySQL 認爲全表掃效率更高,好比對一些很小的表,它就不會使用索引,這種狀況下 InnoDB 將使用表鎖,而不是行鎖。這種狀況是致命的。
(2)咱們要使用排他鎖來進行分佈式鎖的 lock,那麼一個排他鎖長時間不提交,就會佔用數據庫鏈接。
一旦相似的鏈接變得多了,就可能把數據庫鏈接池撐爆
2.基於redis實現(單機版):須要本身實現 必定要用 SET key value NX PX milliseconds 命令,而不要使用setnx 加expire
優勢: 性能高、超時失效比數據庫簡單。
開源實現:Redis官方提出一種算法,叫Redlock,認爲這種實現比普通的單實例實現更安全。
RedLock有多種語言的實現包,其中Java版本:Redisson。
缺點:
(1)失效時間沒法把控。可能設置太短或者過長的狀況.若是設置太短,其餘線程可能會獲取到鎖,沒法保證狀況。過長時其餘線程獲取不到鎖。
解決:Redisson的思路:客戶端起一個後臺線程,快到期時自動續期,若是宕機了,後臺線程也沒有了。
(2)若是採用 Master-Slave 模式,若是 Master 節點故障了,發生主從切換,主從切換的一瞬間,可能出現鎖丟失的問題。
解決:Redisson ,但存在爭議的,不過應該問題不大。
3.基於zookeeper實現(推薦):可靠性好,使用最普遍。實現:Curator
4.基於etcd的實現:優於zookeeper實現,若是項目中應用了etcd,那麼使用etcd。
5.Spring Integration 實現了分佈式鎖:
Gemfire
JDBC
Redis
Zookeeper
方案1
獲取鎖
INSERT INTO method_lock (method_name, desc) VALUES ('methodName', 'methodName');
對method_name作了惟一性約束,這裏若是有多個請求同時提交到數據庫的話,數據庫會保證只有一個操做能夠成功。
方案2
1 DROP TABLE IF EXISTS `method_lock`; 2 CREATE TABLE `method_lock` ( 3 `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', 4 `method_name` varchar(64) NOT NULL COMMENT '鎖定的方法名', 5 `state` tinyint NOT NULL COMMENT '1:未分配;2:已分配', 6 `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 7 `version` int NOT NULL COMMENT '版本號', 8 `PRIMARY KEY (`id`), 9 UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE 10 ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
先獲取鎖的信息
select id, method_name, state,version from method_lock where state=1 and method_name='methodName';
佔有鎖
update t_resoure set state=2, version=2, update_time=now() where method_name='methodName' and state=1 and version=2;
若是沒有更新影響到一行數據,則說明這個資源已經被別人佔位了。
缺點:
一、這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會致使業務系統不可用。
二、這把鎖沒有失效時間,一旦解鎖操做失敗,就會致使鎖記錄一直在數據庫中,其餘線程沒法再得到到鎖。
三、這把鎖只能是非阻塞的,由於數據的insert操做,一旦插入失敗就會直接報錯。沒有得到鎖的線程並不會進入排隊隊列,要想再次得到鎖就要再次觸發得到鎖操做。
四、這把鎖是非重入的,同一個線程在沒有釋放鎖以前沒法再次得到該鎖。由於數據中數據已經存在了。
解決方案:
一、數據庫是單點?搞兩個數據庫,數據以前雙向同步。一旦掛掉快速切換到備庫上。
二、沒有失效時間?只要作一個定時任務,每隔必定時間把數據庫中的超時數據清理一遍。
三、非阻塞的?搞一個while循環,直到insert成功再返回成功。
四、非重入的?在數據庫表中加個字段,記錄當前得到鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,若是當前機器的主機信息和線程信息在數據庫能夠查到的話,直接把鎖分配給他就能夠了。
獲取鎖:
SET resource_name my_random_value NX PX 30000
解鎖方式一:不可用。
if( (GET user_id) == "XXX" ){ //獲取到本身鎖後,進行取值判斷且判斷爲真。此時,這把鎖剛好失效。
DEL user_id
}
因爲GET取值判斷和DEL刪除並不是原子操做,當程序判經過該鎖的值判斷髮現這把鎖是本身加上的,準備DEL。
此時該鎖剛好失效,而另一個請求剛好得到key值爲user_id的鎖。
此時程序執行了了DEL user_id,刪除了別人加的鎖,尷尬!
解鎖方式二(推薦):爲了保證查詢和刪除的原子性操做,須要引入lua腳本支持。
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end
使用zookeeper實現分佈式鎖
zookeeper分佈式鎖應用了臨時順序節點
首先,在Zookeeper當中建立一個持久節點ParentLock。當第一個客戶端想要得到鎖時,須要在ParentLock這個節點下面建立一個臨時順序節點 Lock1。
以後,Client1查找ParentLock下面全部的臨時順序節點並排序,判斷本身所建立的節點Lock1是否是順序最靠前的一個。若是是第一個節點,則成功得到鎖。
這時候,若是再有一個客戶端 Client2 前來獲取鎖,則在ParentLock下載再建立一個臨時順序節點Lock2。
Client2查找ParentLock下面全部的臨時順序節點並排序,判斷本身所建立的節點Lock2是否是順序最靠前的一個,結果發現節點Lock2並非最小的。
因而,Client2向排序僅比它靠前的節點Lock1註冊Watcher,用於監聽Lock1節點是否存在。這意味着Client2搶鎖失敗,進入了等待狀態。
這時候,若是又有一個客戶端Client3前來獲取鎖,則在ParentLock下載再建立一個臨時順序節點Lock3。
Client3查找ParentLock下面全部的臨時順序節點並排序,判斷本身所建立的節點Lock3是否是順序最靠前的一個,結果一樣發現節點Lock3並非最小的。
因而,Client3向排序僅比它靠前的節點Lock2註冊Watcher,用於監聽Lock2節點是否存在。這意味着Client3一樣搶鎖失敗,進入了等待狀態。
這樣一來,Client1獲得了鎖,Client2監聽了Lock1,Client3監聽了Lock2。這偏偏造成了一個等待隊列,很像是Java當中ReentrantLock所依賴的AQS(AbstractQueuedSynchronizer)。
得到鎖的過程大體就是這樣,那麼Zookeeper如何釋放鎖呢?
釋放鎖的過程很簡單,只須要釋放對應的子節點就好。
釋放鎖分爲兩種狀況:
1.任務完成,客戶端顯示釋放
當任務完成時,Client1會顯示調用刪除節點Lock1的指令。
2.任務執行過程當中,客戶端崩潰
得到鎖的Client1在任務執行過程當中,若是Duang的一聲崩潰,則會斷開與Zookeeper服務端的連接。根據臨時節點的特性,相關聯的節點Lock1會隨之自動刪除。
因爲Client2一直監聽着Lock1的存在狀態,當Lock1節點被刪除,Client2會馬上收到通知。這時候Client2會再次查詢ParentLock下面的全部節點,確認本身建立的節點Lock2是否是目前最小的節點。若是是最小,則Client2瓜熟蒂落得到了鎖。
同理,若是Client2也由於任務完成或者節點崩潰而刪除了節點Lock2,那麼Client3就會接到通知。
最終,Client3成功獲得了鎖。