定義
正常的單機(jī)狀態(tài)的精盅,共享資源都是在通過(guò)一個(gè)數(shù)據(jù)庫(kù)下的宏胯,可以在單機(jī)中進(jìn)行加鎖,保證共享數(shù)據(jù)的線程安全肢专,分布式環(huán)境下巾乳,因?yàn)椴皇窃谕惶摂M機(jī)進(jìn)程的,全局的某些唯一資源需要進(jìn)行鎖定鸟召,這時(shí)候就需要分布式鎖胆绊。
現(xiàn)如今都是分布式系統(tǒng),需要部署多臺(tái)服務(wù)器欧募,進(jìn)行負(fù)載均衡压状。如圖:
上圖可以看到,變量A存在JVM1、JVM2种冬、JVM3三個(gè)JVM內(nèi)存中(這個(gè)變量A主要體現(xiàn)是在一個(gè)類中的一個(gè)成員變量镣丑,是一個(gè)有狀態(tài)的對(duì)象,例如:UserController控制器中的一個(gè)整形類型的成員變量)娱两,如果不加任何控制的話莺匠,變量A同時(shí)都會(huì)在JVM分配一塊內(nèi)存,三個(gè)請(qǐng)求發(fā)過(guò)來(lái)同時(shí)對(duì)這個(gè)變量操作十兢,顯然結(jié)果是不對(duì)的趣竣!即使不是同時(shí)發(fā)過(guò)來(lái),三個(gè)請(qǐng)求分別操作三個(gè)不同JVM內(nèi)存區(qū)域的數(shù)據(jù)旱物,變量A之間不存在共享遥缕,也不具有可見性,處理的結(jié)果也是不對(duì)的宵呛!
如果我們業(yè)務(wù)中確實(shí)存在這個(gè)場(chǎng)景的話单匣,我們就需要一種方法解決這個(gè)問(wèn)題!
為了保證一個(gè)方法或?qū)傩栽诟卟l(fā)情況下的同一時(shí)間只能被同一個(gè)線程執(zhí)行宝穗,在傳統(tǒng)單體應(yīng)用單機(jī)部署的情況下户秤,可以使用Java并發(fā)處理相關(guān)的API(如ReentrantLock或Synchronized)進(jìn)行互斥控制。在單機(jī)環(huán)境中逮矛,Java中提供了很多并發(fā)處理相關(guān)的API虎忌。但是,隨著業(yè)務(wù)發(fā)展的需要橱鹏,原單體單機(jī)部署的系統(tǒng)被演化成分布式集群系統(tǒng)后膜蠢,由于分布式系統(tǒng)多線程、多進(jìn)程并且分布在不同機(jī)器上莉兰,這將使原單機(jī)部署情況下的并發(fā)控制鎖策略失效挑围,單純的Java API并不能提供分布式鎖的能力。
為了解決這個(gè)問(wèn)題就需要一種跨JVM的互斥機(jī)制來(lái)控制共享資源的訪問(wèn)糖荒,這就是分布式鎖要解決的問(wèn)題杉辙!
滿足條件
1、在分布式系統(tǒng)環(huán)境下捶朵,一個(gè)方法在同一時(shí)間只能被一個(gè)機(jī)器的一個(gè)線程執(zhí)行蜘矢;
2、高可用的獲取鎖與釋放鎖综看;
3品腹、高性能的獲取鎖與釋放鎖;
4红碑、具備可重入特性舞吭;
5泡垃、具備鎖失效機(jī)制,防止死鎖羡鸥;
6蔑穴、具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗惧浴。
具體例子
-
交易訂單鎖定
需要處理防止重復(fù)下單存和。
解決業(yè)務(wù)層面的冪等問(wèn)題
-
MQ消息消費(fèi)的冪等性
發(fā)送的消息重復(fù)。
消息消費(fèi)端去重衷旅。
比如手機(jī)提現(xiàn)捐腿,不能重復(fù)提現(xiàn)。
在用戶對(duì)商品下單后芜茵,訂單狀態(tài)為待支付叙量,在某一時(shí)刻用戶正在對(duì)該訂單做支付操作倡蝙,商家正在進(jìn)行改價(jià)操作九串??寺鸥? 這時(shí)候猪钮,該狀態(tài)需要做串行處理,避免出現(xiàn)數(shù)據(jù)錯(cuò)亂胆建。
解決方式
1.基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖烤低;
創(chuàng)建一個(gè)表:
DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`method_name` varchar(64) NOT NULL COMMENT '鎖定的方法名',
`desc` varchar(255) NOT NULL COMMENT '備注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
(2)想要執(zhí)行某個(gè)方法,就使用這個(gè)方法名向表中插入數(shù)據(jù)
INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '測(cè)試的methodName');
因?yàn)槲覀儗?duì)method_name做了唯一性約束笆载,這里如果有多個(gè)請(qǐng)求同時(shí)提交到數(shù)據(jù)庫(kù)的話扑馁,數(shù)據(jù)庫(kù)會(huì)保證只有一個(gè)操作可以成功,那么我們就可以認(rèn)為操作成功的那個(gè)線程獲得了該方法的鎖凉驻,可以執(zhí)行方法體內(nèi)容腻要。
(3)成功插入則獲取鎖,執(zhí)行完成后刪除對(duì)應(yīng)的行數(shù)據(jù)釋放鎖:
delete from method_lock where method_name ='methodName';
注意:這只是使用基于數(shù)據(jù)庫(kù)的一種方法涝登,使用數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖還有很多其他的玩法雄家!
使用基于數(shù)據(jù)庫(kù)的這種實(shí)現(xiàn)方式很簡(jiǎn)單,但是對(duì)于分布式鎖應(yīng)該具備的條件來(lái)說(shuō)胀滚,它有一些問(wèn)題需要解決及優(yōu)化:
1趟济、因?yàn)槭腔跀?shù)據(jù)庫(kù)實(shí)現(xiàn)的,數(shù)據(jù)庫(kù)的可用性和性能將直接影響分布式鎖的可用性及性能咽笼,所以顷编,數(shù)據(jù)庫(kù)需要雙機(jī)部署、數(shù)據(jù)同步剑刑、主備切換勾效;
2、不具備可重入的特性,因?yàn)橥粋€(gè)線程在釋放鎖之前层宫,行數(shù)據(jù)一直存在杨伙,無(wú)法再次成功插入數(shù)據(jù),所以萌腿,需要在表中新增一列限匣,用于記錄當(dāng)前獲取到鎖的機(jī)器和線程信息,在再次獲取鎖的時(shí)候毁菱,先查詢表中機(jī)器和線程信息是否和當(dāng)前機(jī)器和線程相同米死,若相同則直接獲取鎖;
3贮庞、沒有鎖失效機(jī)制峦筒,因?yàn)橛锌赡艹霈F(xiàn)成功插入數(shù)據(jù)后,服務(wù)器宕機(jī)了窗慎,對(duì)應(yīng)的數(shù)據(jù)沒有被刪除物喷,當(dāng)服務(wù)恢復(fù)后一直獲取不到鎖,所以遮斥,需要在表中新增一列峦失,用于記錄失效時(shí)間,并且需要有定時(shí)任務(wù)清除這些失效的數(shù)據(jù)术吗;
4尉辑、不具備阻塞鎖特性,獲取不到鎖直接返回失敗较屿,所以需要優(yōu)化獲取邏輯隧魄,循環(huán)多次去獲取。
2.基于redis做分布式鎖
為什么隘蝎?
redis本身是單線程购啄,唯一線程串行處理。
實(shí)現(xiàn)方式
Redis Setnx命令末贾,在指定的key不存在時(shí)闸溃,為key設(shè)置指定的值.多個(gè)線程并發(fā)的請(qǐng)求去設(shè)置時(shí),只有一個(gè)可以設(shè)置成功拱撵。其他的會(huì)返回失敗辉川。一般設(shè)置五秒鐘。
//設(shè)置成功拴测,返回1乓旗,設(shè)置失敗,返回 0
Setnx KEY_NAME VALUE Expire Time
分析存在問(wèn)題:
-
單點(diǎn)問(wèn)題
單機(jī)模式集索,設(shè)置T1 T2兩個(gè)線程.如果T1剛設(shè)置成功屿愚,單機(jī)掛了汇跨,重啟,請(qǐng)求丟了妆距,T2去請(qǐng)求再去拿鎖穷遂,會(huì)獲取不到,這時(shí)候會(huì)獲取不到key娱据。(因?yàn)榉植际芥i一般不考慮做持久化蚪黑,所以這里不考慮持久化。)
主從模式中剩,主從數(shù)據(jù)異步忌穿,會(huì)存在鎖失效的問(wèn)題,主服務(wù)器還未同步到從服務(wù)器结啼,這時(shí)候主掛了掠剑,從服務(wù)器獲取不到鎖。
鎖時(shí)間不可以控制郊愧,無(wú)法續(xù)租期
Redis本身建議:使用RedLock算法來(lái)保證朴译,但是問(wèn)題是需要至少三個(gè)Redis主從實(shí)例來(lái)完成,維護(hù)成本很高糕珊。這個(gè)等同于自己簡(jiǎn)單實(shí)現(xiàn)的一致性協(xié)議动分,細(xì)節(jié)繁瑣毅糟,且容易出錯(cuò)红选。
是否能使用
業(yè)務(wù)場(chǎng)景來(lái)規(guī)定,在設(shè)計(jì)交易時(shí)姆另,只能發(fā)一次交易請(qǐng)求喇肋,這時(shí)候不適合。如果是MQ消息消費(fèi)場(chǎng)景迹辐,依次獲取不到蝶防,可以在發(fā)送一次消息保證能被消費(fèi)。
CAP問(wèn)題
分布式鎖明吩,主要選擇滿足C P模型间学,而redis實(shí)現(xiàn)的主要滿足AP模型。不太ok印荔。
代碼實(shí)現(xiàn)
使用命令介紹:
(1)SETNX
SETNX key val:當(dāng)且僅當(dāng)key不存在時(shí)低葫,set一個(gè)key為val的字符串,返回1仍律;若key存在,則什么都不做,返回0姥宝。
(2)expire
expire key timeout:為key設(shè)置一個(gè)超時(shí)時(shí)間掏婶,單位為second窒盐,超過(guò)這個(gè)時(shí)間鎖會(huì)自動(dòng)釋放,避免死鎖钢拧。
1
(3)delete
delete key:刪除key
實(shí)現(xiàn)思想:
(1)獲取鎖的時(shí)候蟹漓,使用setnx加鎖,并使用expire命令為鎖添加一個(gè)超時(shí)時(shí)間源内,超過(guò)該時(shí)間則自動(dòng)釋放鎖牧牢,鎖的value值為一個(gè)隨機(jī)生成的UUID,通過(guò)此在釋放鎖的時(shí)候進(jìn)行判斷姿锭。
(2)獲取鎖的時(shí)候還設(shè)置一個(gè)獲取的超時(shí)時(shí)間塔鳍,若超過(guò)這個(gè)時(shí)間則放棄獲取鎖。
(3)釋放鎖的時(shí)候呻此,通過(guò)UUID判斷是不是該鎖轮纫,若是該鎖,則執(zhí)行delete進(jìn)行鎖釋放焚鲜。
/**
* 分布式鎖的簡(jiǎn)單實(shí)現(xiàn)代碼
* Created by liuyang on 2017/4/20.
*/
public class DistributedLock {
private final JedisPool jedisPool;
public DistributedLock(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
/**
* 加鎖
* @param lockName 鎖的key
* @param acquireTimeout 獲取超時(shí)時(shí)間
* @param timeout 鎖的超時(shí)時(shí)間
* @return 鎖標(biāo)識(shí)
*/
public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
Jedis conn = null;
String retIdentifier = null;
try {
// 獲取連接
conn = jedisPool.getResource();
// 隨機(jī)生成一個(gè)value
String identifier = UUID.randomUUID().toString();
// 鎖名掌唾,即key值
String lockKey = "lock:" + lockName;
// 超時(shí)時(shí)間,上鎖后超過(guò)此時(shí)間則自動(dòng)釋放鎖
int lockExpire = (int) (timeout / 1000);
// 獲取鎖的超時(shí)時(shí)間忿磅,超過(guò)這個(gè)時(shí)間則放棄獲取鎖
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
if (conn.setnx(lockKey, identifier) == 1) {
conn.expire(lockKey, lockExpire);
// 返回value值糯彬,用于釋放鎖時(shí)間確認(rèn)
retIdentifier = identifier;
return retIdentifier;
}
// 返回-1代表key沒有設(shè)置超時(shí)時(shí)間,為key設(shè)置一個(gè)超時(shí)時(shí)間
if (conn.ttl(lockKey) == -1) {
conn.expire(lockKey, lockExpire);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retIdentifier;
}
/**
* 釋放鎖
* @param lockName 鎖的key
* @param identifier 釋放鎖的標(biāo)識(shí)
* @return
*/
public boolean releaseLock(String lockName, String identifier) {
Jedis conn = null;
String lockKey = "lock:" + lockName;
boolean retFlag = false;
try {
conn = jedisPool.getResource();
while (true) {
// 監(jiān)視lock葱她,準(zhǔn)備開始事務(wù)
conn.watch(lockKey);
// 通過(guò)前面返回的value值判斷是不是該鎖撩扒,若是該鎖,則刪除吨些,釋放鎖
if (identifier.equals(conn.get(lockKey))) {
Transaction transaction = conn.multi();
transaction.del(lockKey);
List<Object> results = transaction.exec();
if (results == null) {
continue;
}
retFlag = true;
}
conn.unwatch();
break;
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retFlag;
}
}
測(cè)試剛才實(shí)現(xiàn)的分布式鎖
例子中使用50個(gè)線程模擬秒殺一個(gè)商品搓谆,使用–運(yùn)算符來(lái)實(shí)現(xiàn)商品減少,從結(jié)果有序性就可以看出是否為加鎖狀態(tài)豪墅。
模擬秒殺服務(wù)泉手,在其中配置了jedis線程池,在初始化的時(shí)候傳給分布式鎖偶器,供其使用.
/**
* Created by liuyang on 2017/4/20.
*/
public class Service {
private static JedisPool pool = null;
private DistributedLock lock = new DistributedLock(pool);
int n = 500;
static {
JedisPoolConfig config = new JedisPoolConfig();
// 設(shè)置最大連接數(shù)
config.setMaxTotal(200);
// 設(shè)置最大空閑數(shù)
config.setMaxIdle(8);
// 設(shè)置最大等待時(shí)間
config.setMaxWaitMillis(1000 * 100);
// 在borrow一個(gè)jedis實(shí)例時(shí)斩萌,是否需要驗(yàn)證,若為true屏轰,則所有jedis實(shí)例均是可用的
config.setTestOnBorrow(true);
pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
}
public void seckill() {
// 返回鎖的value值颊郎,供釋放鎖時(shí)候進(jìn)行判斷
String identifier = lock.lockWithTimeout("resource", 5000, 1000);
System.out.println(Thread.currentThread().getName() + "獲得了鎖");
System.out.println(--n);
lock.releaseLock("resource", identifier);
}
}
模擬線程進(jìn)行秒殺服務(wù)
public class ThreadA extends Thread {
private Service service;
public ThreadA(Service service) {
this.service = service;
}
@Override
public void run() {
service.seckill();
}
}
//這里推薦使用 countDownLatch
public class Test {
public static void main(String[] args) {
Service service = new Service();
for (int i = 0; i < 50; i++) {
ThreadA threadA = new ThreadA(service);
threadA.start();
}
}
}
3.基于zookeeper
ZooKeeper是一個(gè)為分布式應(yīng)用提供一致性服務(wù)的開源組件,它內(nèi)部是一個(gè)分層的文件系統(tǒng)目錄樹結(jié)構(gòu)亭枷,規(guī)定同一個(gè)目錄下只能有一個(gè)唯一文件名袭艺。基于ZooKeeper實(shí)現(xiàn)分布式鎖的步驟如下:
(1)創(chuàng)建一個(gè)目錄mylock叨粘;
(2)線程A想獲取鎖就在mylock目錄下創(chuàng)建臨時(shí)順序節(jié)點(diǎn)猾编;
(3)獲取mylock目錄下所有的子節(jié)點(diǎn)瘤睹,然后獲取比自己小的兄弟節(jié)點(diǎn),如果不存在答倡,則說(shuō)明當(dāng)前線程順序號(hào)最小轰传,獲得鎖;
(4)線程B獲取所有節(jié)點(diǎn)瘪撇,判斷自己不是最小節(jié)點(diǎn)获茬,設(shè)置監(jiān)聽比自己次小的節(jié)點(diǎn);
(5)線程A處理完倔既,刪除自己的節(jié)點(diǎn)恕曲,線程B監(jiān)聽到變更事件,判斷自己是不是最小的節(jié)點(diǎn)渤涌,如果是則獲得鎖佩谣。
這里推薦一個(gè)Apache的開源庫(kù)Curator,它是一個(gè)ZooKeeper客戶端实蓬,Curator提供的InterProcessMutex是分布式鎖的實(shí)現(xiàn)茸俭,acquire方法用于獲取鎖,release方法用于釋放鎖安皱。
優(yōu)點(diǎn):具備高可用调鬓、可重入、阻塞鎖特性酌伊,可解決失效死鎖問(wèn)題腾窝。
缺點(diǎn):因?yàn)樾枰l繁的創(chuàng)建和刪除節(jié)點(diǎn),性能上不如Redis方式腺晾。
具體代碼可以看這篇文章:
https://blog.csdn.net/qiangcuo6087/article/details/79067136
自己設(shè)計(jì)一個(gè)分布式鎖
設(shè)計(jì)的目標(biāo)
- 強(qiáng)一致性
- 服務(wù)高可用燕锥、系統(tǒng)穩(wěn)健
- 鎖自動(dòng)續(xù)約及其自動(dòng)釋放
- 代碼高度抽象業(yè)務(wù)接入極簡(jiǎn)
- 可視化管理憑他辜贵、監(jiān)控及管理
對(duì)存儲(chǔ)模型進(jìn)行選型
N+1 代表部署奇數(shù)個(gè)
由于redis實(shí)現(xiàn)無(wú)法保證一致性悯蝉,zookeeper對(duì)鎖實(shí)現(xiàn)使用創(chuàng)建臨時(shí)節(jié)點(diǎn)和watch機(jī)制,執(zhí)行效率托慨,擴(kuò)展能力鼻由、社區(qū)活躍度等方面低于etcd,所以我們會(huì)選擇基于etcd實(shí)現(xiàn)厚棵。
etcd優(yōu)勢(shì)
- 簡(jiǎn)單KV(key Value)
- 強(qiáng)一致性
- 高可用
- 無(wú)單點(diǎn)
- 數(shù)據(jù)可靠性
- 持久化
整體方案
分布式Client + etcd
Client TTL模式
Server TTL模式
拿鎖的時(shí)候蕉世,選擇key,ttl是超時(shí)時(shí)間婆硬,value可以忽略狠轻,uuid為該鎖的唯一憑證,后面對(duì)鎖的操作都是對(duì)uuid做操作彬犯。需要uuid才能做操作向楼。etcd會(huì)保證只有一個(gè)線程能拿到鎖查吊。
使用場(chǎng)景1.申請(qǐng)鎖
使用場(chǎng)景2.申請(qǐng)鎖,鎖已經(jīng)被占用
使用場(chǎng)景3.鎖的清理
業(yè)務(wù)接入
JDK7以上湖蜕,建議9.
獲取鎖實(shí)例:
釋放鎖示例
兼容性考慮
ETCD恢復(fù)/版本
分布式鎖的特殊場(chǎng)景
特殊場(chǎng)景一:
分布式鎖只是在同一自然時(shí)間的互斥鎖逻卖,本省不解決冪等性問(wèn)題。
接入業(yè)務(wù)需要完善從獲得鎖到釋放鎖中間的數(shù)據(jù)冪等邏輯昭抒。
特殊場(chǎng)景二: 鎖沒有按照日期續(xù)約
心跳續(xù)約沒有成功
馬上啟動(dòng)GC评也,GCs時(shí)間太長(zhǎng)
特殊場(chǎng)景三: etcd內(nèi)部協(xié)調(diào)發(fā)生問(wèn)題
Leader節(jié)點(diǎn)掛了,選主中灭返,
raft日志數(shù)據(jù)同步發(fā)生錯(cuò)誤或者不一致問(wèn)題盗迟。
待續(xù)。熙含。诈乒。
部分摘自: