通過數據庫實現分佈式鎖

引語

針對分佈式鎖的實現目前有多種方案,包括數據庫實現分佈式鎖,基於緩存(redis,memcached)實現分佈式鎖,以及基於Zookeeper實現分佈式鎖。現在我們要討論的是使用數據庫來實現分佈式鎖。

 


 

使用數據庫表來實現方法級別加鎖

示例。直接建一張表,裏面記錄鎖定的方法名、時間即可。需要加鎖時,就插入一條數據,釋放鎖時就刪除數據。

CREATE TABLE `methodLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的方法名',
`desc` varchar(1024) NOT NULL DEFAULT '備註信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存數據時間,自動生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';

如果我們想給某個方法加鎖,則執行以下SQL

insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)

因爲我們對method_name做了唯一性約束,這裏如果有多個請求同時提交到數據庫的話,數據庫會保證只有一個操作可以成功,那麼我們就可以認爲

操作成功的那個線程獲得了該方法的鎖,可以執行方法體內容。

當方法執行完畢之後,想要釋放鎖的話,需要執行以下Sql:

delete from methodLock where method_name ='method_name'

 

請求鎖和釋放鎖的步驟可以總結如下

假設我們現在有線程A和B,他們都請求方法method01的鎖。

一,線程A向數據庫表執行select查詢是否有method01的記錄,如果沒有該記錄,則insert插入一條method01的記錄,表示佔用了method01的鎖。

二,線程B也請求method01的鎖,它執行select查詢是否有method01的記錄,因爲線程A剛剛插入了記錄,線程B獲知已經有其他線程持有該鎖,所以線程B應該暫停一段時間,過後再請求鎖。

三,線程A執行完業務邏輯後,需要釋放鎖,對應操作就是delete把剛剛插入的method01的記錄刪除。

四,線程B先select,沒有method01的記錄,接下去就是加鎖、執行業務邏輯、釋放鎖。

 

 

存在的問題和相應的解決辦法

①這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會導致業務系統不可用。解決辦法是,單點問題可以用多數據庫實例,同時塞N個表,N/2+1個成功就任務鎖定成功。

②這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖。解決辦法是,寫一個定時任務,隔一段時間清除一次過期的數據。

③這把鎖只能是非阻塞的。沒有獲得鎖的線程並不會進入排隊隊列,要想再次獲得鎖就要再次觸發獲得鎖操作。解決辦法是,寫一個while循環,不斷的重試插入,直到成功。

④這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因爲數據中數據已經存在了。解決辦法是,在數據庫表中加個字段,記錄當前獲得鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,如果當前機器的主機信息和線程信息在數據庫可以查到的話,直接把鎖分配給他就可以了。

 

總結

優點: 直接藉助數據庫,容易理解。

缺點: 會有各種各樣的問題,在解決問題的過程中會使整個方案變得越來越複雜。操作數據庫需要一定的開銷,性能問題需要考慮。


 

通過version控制來實現樂觀鎖

比如有一張紅包表(t_bonus),有一個字段(left_count)記錄禮物的剩餘個數,用戶每領取一個獎品,對應的left_count減1,在併發的情況下如何要保證left_count不爲負數,樂觀鎖的實現方式爲在紅包表上添加一個版本號字段(version),默認爲0。異常實現流程

-- 可能會發生的異常情況
-- 線程1查詢,當前left_count爲1,則有記錄
select * from t_bonus where id = 10001 and left_count > 0

-- 線程2查詢,當前left_count爲1,也有記錄
select * from t_bonus where id = 10001 and left_count > 0

-- 線程1完成領取記錄,修改left_count爲0,
update t_bonus set left_count = left_count - 1 where id = 10001

-- 線程2完成領取記錄,修改left_count爲-1,產生髒數據
update t_bonus set left_count = left_count - 1 where id = 10001

在上面所述情況中,可能會導致結果異常。

 

通過樂觀鎖實現

-- 添加版本號控制字段
ALTER TABLE table ADD COLUMN version INT DEFAULT '0' NOT NULL AFTER t_bonus;

-- 線程1查詢,當前left_count爲1,則有記錄,當前版本號爲1234
select left_count, version from t_bonus where id = 10001 and left_count > 0

-- 線程2查詢,當前left_count爲1,有記錄,當前版本號爲1234
select left_count, version from t_bonus where id = 10001 and left_count > 0

-- 線程1,更新完成後當前的version爲1235,update狀態爲1,更新成功
update t_bonus set version = 1235, left_count = left_count-1 where id = 10001 and version = 1234

-- 線程2,更新由於當前的version爲1235,udpate狀態爲0,更新失敗,再針對相關業務做異常處理
update t_bonus set version = 1235, left_count = left_count-1 where id = 10001 and version = 1234

 

關於redis、Zookeeper實現的分佈式鎖,可以參考以下文章

https://www.jianshu.com/p/535efcab356d

https://juejin.im/post/5a20cd8bf265da43163cdd9a

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章