如何理解分布式鎖
為了保證在多線程下處理共享數(shù)據(jù)的安全性,需要保證同一時(shí)刻只有一個(gè)線程能處理共享數(shù)據(jù)
Java 語(yǔ)言提供了線程鎖,開(kāi)放了處理鎖機(jī)制的 API,比如 Synchronized,Lock 等
當(dāng)一個(gè)鎖被某個(gè)線程持有時(shí),另一個(gè)線程嘗試去獲取這個(gè)鎖會(huì)失敗或者阻塞
直到持有鎖的線程釋放了該鎖
單臺(tái)服務(wù)器內(nèi)存,可以通過(guò)線程加鎖的方式來(lái)同步,避免并發(fā)問(wèn)題

分布式鎖的常用實(shí)現(xiàn)

基于關(guān)系型數(shù)據(jù)庫(kù)
基于關(guān)系型數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖,是依賴數(shù)據(jù)庫(kù)的唯一性來(lái)實(shí)現(xiàn)資源鎖定,比如主鍵和唯一索引等
以唯一索引為例,創(chuàng)建一張鎖表,定義方法或資源名,失效時(shí)間等字段
同時(shí)針對(duì)鎖的信息添加唯一索引,比如方法名
當(dāng)要鎖住某個(gè)方法或資源時(shí),就在該表中插入對(duì)應(yīng)方法的一條記錄
插入成功表示獲取了鎖,想要釋放鎖的時(shí)候就刪除這條記錄
- 創(chuàng)建一張基于數(shù)據(jù)庫(kù)的分布式鎖表
CREATE TABLE `methodLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的方法或資源',
PRIMARY KEY (`id`),
UNIQYE KEY `uidx_method_name` (`method_name`) USIGN BTREE
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='對(duì)方法加鎖';
當(dāng)希望對(duì)某個(gè)方法加鎖時(shí),執(zhí)行以下SQL語(yǔ)句
insert into methodLock(method_name) values ('method_name');
如果有多個(gè)請(qǐng)求同時(shí)提交到數(shù)據(jù)庫(kù)的話,數(shù)據(jù)庫(kù)會(huì)保證只有一個(gè)操作可以成功
可以認(rèn)為操作成功的那個(gè)線程獲得了該方法的鎖,可以執(zhí)行后面的業(yè)務(wù)邏輯
當(dāng)方法執(zhí)行完畢后,想要釋放鎖,在數(shù)據(jù)庫(kù)中刪除對(duì)應(yīng)的記錄即可
優(yōu)化
- 存在單點(diǎn)故障風(fēng)險(xiǎn)
數(shù)據(jù)實(shí)現(xiàn)方式強(qiáng)依賴數(shù)據(jù)庫(kù)的可用性,一旦數(shù)據(jù)庫(kù)掛掉,則會(huì)導(dǎo)致業(yè)務(wù)系統(tǒng)不可用
解決方法:配置數(shù)據(jù)庫(kù)主從機(jī)器,防止單點(diǎn)故障 - 超時(shí)無(wú)法失效
一旦解鎖操作失敗,會(huì)導(dǎo)致鎖記錄一直在數(shù)據(jù)庫(kù)中,其他線程無(wú)法再獲得鎖
解決方法:可以添加獨(dú)立的定時(shí)任務(wù),通過(guò)時(shí)間戳對(duì)比等方式,刪除超時(shí)數(shù)據(jù) - 不可重入
以Java語(yǔ)言為例,常見(jiàn)的Synchronize,Lock 等都支持可重入
在數(shù)據(jù)庫(kù)實(shí)現(xiàn)方式中,同一個(gè)線程在沒(méi)有釋放鎖
實(shí)現(xiàn)可重入,需要改造加鎖方法,額外存儲(chǔ)在判斷線程信息,不阻塞獲得鎖的線程再次請(qǐng)求加鎖 - 對(duì)阻塞操作不友好
其他線程在請(qǐng)求對(duì)應(yīng)方法時(shí),插入數(shù)據(jù)失敗會(huì)直接返回,不會(huì)阻塞線程
如果需要阻塞其他線程,需要不斷的重試 insert 操作,直到數(shù)據(jù)插入成功
應(yīng)用 Redis 緩存
緩存的性能更好,各種緩存組件也提供了多種集群方案,可以解決單點(diǎn)問(wèn)題
常見(jiàn)的開(kāi)源緩存組件都支持分布式鎖,包括 Redis,Memcached 即 Tair
應(yīng)用 Redis 實(shí)現(xiàn)分布式鎖,最直接的想法是利用 setnx 和 expire 命令實(shí)現(xiàn)加鎖
在 Redis 中,setnx 是【set if not exists】如果不存在,則 SET 的意思
- 當(dāng)一個(gè)線程執(zhí)行 setnx 返回 1 ,說(shuō)明 key 不存在,該線程獲得鎖
- 當(dāng)一個(gè)線程執(zhí)行 setnx 返回 0 ,說(shuō)明key 已存在,獲取鎖失敗
if (setnx(key, value) == 1) {
expire(key, expireTime)
try {
// 業(yè)務(wù)處理
} finally {
// 釋放鎖
del(key)
}
}
在 Redis 版本更新中,添加了 SETEX 命令
SETEX 支持 setnx 和 expire 指令組合的原子操作
解決了加鎖過(guò)程中失敗的問(wèn)題
基于 ZooKeeper 實(shí)現(xiàn)
ZooKeeper 有四種節(jié)點(diǎn)類型:
- 持久節(jié)點(diǎn)
- 持久順序節(jié)點(diǎn)
- 臨時(shí)節(jié)點(diǎn)
- 臨時(shí)順序節(jié)點(diǎn)
利用 ZooKeeper 支持臨時(shí)順序節(jié)點(diǎn)的特性,可以實(shí)現(xiàn)分布式鎖

當(dāng)客戶端對(duì)某個(gè)方法加鎖時(shí),在 ZooKeeper中該方法對(duì)指定節(jié)點(diǎn)目錄下生成唯一有序節(jié)點(diǎn)。
判斷是否獲取鎖,只需要判斷持有的節(jié)點(diǎn)是否是有序節(jié)點(diǎn)中的序號(hào)最小的一個(gè)
當(dāng)釋放鎖的時(shí)候,將這個(gè)臨時(shí)節(jié)點(diǎn)刪除即可
這種方法可以避免服務(wù)宕機(jī)導(dǎo)致的鎖無(wú)法釋放而產(chǎn)生的死鎖問(wèn)題
使用 Zookeeper 實(shí)現(xiàn)分布式鎖的算法流程,根節(jié)點(diǎn)為 /lock
- 客戶端連接 ZooKeeper ,并在 /lock 下創(chuàng)建臨時(shí)有序子節(jié)點(diǎn)
第一個(gè)客戶端對(duì)應(yīng)的子節(jié)點(diǎn)為 /lock/lock01/0000001 ,第二個(gè)為 /locl/lock01/00000002 - 其他客戶端獲取 /lock01 下的子節(jié)點(diǎn)列表,判斷自己創(chuàng)建的子節(jié)點(diǎn)是否為當(dāng)前列表中序號(hào)最小的子節(jié)點(diǎn)
- 如果是則任務(wù)獲得鎖,執(zhí)行業(yè)務(wù)代碼,否則通過(guò) watch 事件監(jiān)聽(tīng) /lock01 的子節(jié)點(diǎn)變更消息,獲得變更通知后重復(fù)此步驟直至獲得鎖
- 完成業(yè)務(wù)流程后,刪除對(duì)應(yīng)的子節(jié)點(diǎn),釋放分布式鎖
在實(shí)際開(kāi)發(fā)中,可以應(yīng)用 Apache Curator 來(lái)快速實(shí)現(xiàn)分布式鎖
Curator 是 Netflix 公司開(kāi)源的一個(gè) ZooKeeper 客戶端
對(duì) ZooKeeper 原生 API 做了抽象和封裝