我們在單機服務(wù)器,出現(xiàn)資源的競爭,一般使用synchronized 就可以解決,但是在分布式的服務(wù)器上,synchronized 就無法解決這個問題,這就需要一個分布式事務(wù)鎖。
除此之外面試,基本會問springboot、Redis,然后都會一路再聊到分布式事務(wù)、分布式事務(wù)鎖的實現(xiàn)。
1、常見的分布式事務(wù)鎖
1、數(shù)據(jù)庫級別的鎖
- 樂觀鎖,基于加入版本號實現(xiàn)
- 悲觀鎖,基于數(shù)據(jù)庫的 for update 實現(xiàn)
2、Redis ,基于 SETNX、EXPIRE 實現(xiàn)
3、Zookeeper,基于InterProcessMutex 實現(xiàn)
4、Redisson,lcok、tryLock(背后原理也是Redis)
本文主要介紹一下Redis和Redisson的分布式事務(wù)鎖的原理。
2、Redis 搭建模式
Redis 的搭建方式:
- 單機
- 主從
- 哨兵
- 集群
單機,只要一臺Redis服務(wù)器,掛了就無法工作了
主從,是備份關(guān)系, 數(shù)據(jù)也會同步到從庫,還可以讀寫分離
哨兵:master掛了,哨兵就行選舉,選出新的master,作用是監(jiān)控主從,主從切換
集群:高可用,分散請求。目的是將數(shù)據(jù)分片存儲,節(jié)省內(nèi)存。
單機:

主從:

哨兵:

集群:

3、幾個概念
分布式:簡單來說就是將業(yè)務(wù)進行拆分,部署到不同的機器來協(xié)調(diào)處理。比如用戶在網(wǎng)上買東西,大致分為:訂單系統(tǒng)、庫存系統(tǒng)、支付系統(tǒng)、、、、這些系統(tǒng)共同來完成用戶買東西這個業(yè)務(wù)操作。
集群:同一個業(yè)務(wù),通過部署多個實例來完成,保證應(yīng)用的高可用,如果其中某個實例掛了,業(yè)務(wù)仍然可以正常進行,通常集群和分布式配合使用。來保證系統(tǒng)的高可用、高性能。
分布式事務(wù):按照傳統(tǒng)的系統(tǒng)架構(gòu),下單、扣庫存等等,這一系列的操作都是一在一個應(yīng)用一個數(shù)據(jù)庫中完成的,也就是說保證了事務(wù)的ACID特性。如果在分布式應(yīng)用中就會涉及到跨應(yīng)用、跨庫。這樣就涉及到了分布式事務(wù),就要考慮怎么保證這一系列的操作要么都成功要么都失敗。保證數(shù)據(jù)的一致性。
分布式鎖:因為資源有限,要通過互斥來保持一致性,引入分布式事務(wù)鎖。
4、Redis分布式鎖原理
簡單的來說,其實現(xiàn)原理如下:
互斥性
- 保證同一時間只有一個客戶端可以拿到鎖。
安全性
- 只有加鎖的服務(wù)才能有解鎖權(quán)限,也就是不能讓客戶端A加的鎖,客戶端B、C 都可以解鎖。
避免死鎖
保證加鎖與解鎖操作是原子性操作
- 這個其實屬于是實現(xiàn)分布式鎖的問題,假設(shè)a用redis實現(xiàn)分布式鎖
- 假設(shè)加鎖操作,操作步驟分為兩步:1,設(shè)置key set(key,value) 2,給key設(shè)置過期時間
- 假設(shè)現(xiàn)在a剛實現(xiàn)set后,程序崩了就導(dǎo)致了沒給key設(shè)置過期時間就導(dǎo)致key一直存在就發(fā)生了死鎖。
講了這么多,Redis實現(xiàn)分布式鎖的核心就是:
加鎖:
SET key value NX EX timeOut
參數(shù)解釋:
NX:只有這個key不存才的時候才會進行操作,即 if not exists;
EX:設(shè)置key的過期時間為秒,具體時間由第5個參數(shù)決定
timeOut:設(shè)置過期時間保證不會出現(xiàn)死鎖【避免宕機死鎖】
代碼實現(xiàn):
public Boolean lock(String key,String value,Long timeOut){
String var1 = jedis.set(key,value,"NX","EX",timeOut); //加鎖,設(shè)置超時時間 原子性操作
if(LOCK_SUCCESS.equals(var1)){
return true;
}
return false;
}
總的來說,執(zhí)行上面的set()方法就只會導(dǎo)致兩種結(jié)果:
- 當(dāng)前沒有鎖(key不存在),那么就進行加鎖操作,并對鎖設(shè)置個有效期,同時value表示加鎖的客戶端。
- 已有鎖存在,不做任何操作。
注:從2.6.12版本后, 就可以使用set來獲取鎖、Lua 腳本來釋放鎖。setnx是以前剛開始的實現(xiàn)方式,set命令nx、xx等參數(shù),,就是為了實現(xiàn) setnx 的功能。
解鎖:
代碼實現(xiàn):
public Boolean redisUnLock(String key, String value) {
String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Object var2 = jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value));
if (UNLOCK_SUCCESS == var2) {
return true;
}
return false;
}
這段lua代碼的意思:首先獲取鎖對應(yīng)的value值,檢查是否與輸入的value相等,如果相等則刪除鎖(解鎖)。
上面加鎖、解鎖,看著是挺麻煩的,所以就出現(xiàn)了Redisson。
5、Redisson 分布式鎖原理
官方介紹:
Redisson是一個在Redis的基礎(chǔ)上實現(xiàn)的Java駐內(nèi)存數(shù)據(jù)網(wǎng)格。
就是在Redis的基礎(chǔ)上封裝了很多功能,以便于我們更方便的使用。
只需要三行代碼:
RLock lock = redisson.getLock("myLock");
lock.lock(); //加鎖
lock.unlock(); //解鎖
(1)加鎖機制
加鎖流程:

redisson的lock()、tryLock()方法 底層 其實是發(fā)送一段lua腳本到一臺服務(wù)器:
if (redis.call('exists' KEYS[1]) == 0) then + -- exists 判斷key是否存在
redis.call('hset' KEYS[1] ARGV[2] 1); + --如果不存在,hset存哈希表
redis.call('pexpire' KEYS[1] ARGV[1]); + --設(shè)置過期時間
return nil; + -- 返回null 就是加鎖成功
end; +
if (redis.call('hexists' KEYS[1] ARGV[2]) == 1) then + -- 如果key存在,查看哈希表中是否存在(當(dāng)前線程)
redis.call('hincrby' KEYS[1] ARGV[2] 1); + -- 給哈希中的key加1,代表重入1次,以此類推
redis.call('pexpire' KEYS[1] ARGV[1]); + -- 重設(shè)過期時間
return nil; +
end; +
return redis.call('pttl' KEYS[1]); --如果前面的if都沒進去,說明ARGV[2]的值不同,也就是不是同一線程的鎖,這時候直接返回該鎖的過期時間
參數(shù)解釋:
KEYS[1]:即加鎖的key,
RLock lock = redisson.getLock("myLock"); 中的myLockARGV[1]:即 TimeOut 鎖key的默認生存時間,默認30秒
ARGV[2]:代表的是加鎖的客戶端的ID,類似于這樣的:
99ead457-bd16-4ec0-81b6-9b7c73546469:1
其中l(wèi)ock()默認是30秒的生存時間。
(2)鎖互斥
假如客戶端A已經(jīng)拿到了 myLock,現(xiàn)在 有一客戶端(未知) 想進入:
1、第一個if判斷會執(zhí)行“exists myLock”,發(fā)現(xiàn)myLock這個鎖key已經(jīng)存在了。
2、第二個if判斷,判斷一下,myLock鎖key的hash數(shù)據(jù)結(jié)構(gòu)中, 如果是客戶端A重新請求,證明當(dāng)前是同一個客戶端同一個線程重新進入,所以可從入標(biāo)志+1,重新刷新生存時間(可重入); 否則進入下一個if。
3、第三個if判斷,客戶端B 會獲取到pttl myLock返回的一個數(shù)字,這個數(shù)字代表了myLock這個鎖key的剩余生存時間。比如還剩15000毫秒的生存時間。
此時客戶端B會進入一個while循環(huán),不停的嘗試加鎖。
(3)watch dog 看門狗自動延期機制
官方介紹:
lockWatchdogTimeout(監(jiān)控鎖的看門狗超時,單位:毫秒)
默認值:30000
監(jiān)控鎖的看門狗超時時間單位為毫秒。該參數(shù)只適用于分布式鎖的加鎖請求中未明確使用leaseTimeout參數(shù)的情況。(如果設(shè)置了leaseTimeout那就會自動失效了呀~)
看門狗的時間可以自定義設(shè)置:
config.setLockWatchdogTimeout(30000);
看門狗有什么用呢?
假如客戶端A在超時時間內(nèi)還沒執(zhí)行完畢怎么辦呢? redisson于是提供了這個看門狗,如果還沒執(zhí)行完畢,監(jiān)聽到這個客戶端A的線程還持有鎖,就去續(xù)期,默認是 LockWatchdogTimeout/ 3 即 10 秒監(jiān)聽一次,如果還持有,就不斷的延長鎖的有效期(重新給鎖設(shè)置過期時間,30s)
可以在lock的參數(shù)里面指定:
lock.lock(); //如果不設(shè)置,默認的生存時間是30s,啟動看門狗
lock.lock(10, TimeUnit.SECONDS);//10秒以后自動解鎖,不啟動看門狗,鎖到期不續(xù)
如果是使用了可重入鎖( leaseTimeout):
lock.tryLock(); //如果不設(shè)置,默認的生存時間是30s,啟動看門狗
lock.tryLock(100, 10, TimeUnit.SECONDS);//嘗試加鎖最多等待100秒,上鎖以后10秒自動解鎖,不啟動看門狗
這里的第二個參數(shù)leaseTimeout 設(shè)置為 10 就會覆蓋 看門狗的設(shè)置(看門狗無效),在10秒后鎖就自動失效,不會去續(xù)期;如果是 -1 ,就表示 使用看門狗的默認值。
(4)釋放鎖機制
lock.unlock(),就可以釋放分布式鎖。就是每次都對myLock數(shù)據(jù)結(jié)構(gòu)中的那個加鎖次數(shù)減1。
如果發(fā)現(xiàn)加鎖次數(shù)是0了,說明這個客戶端已經(jīng)不再持有鎖了,此時就會用:“del myLock”命令,從redis里刪除這個key。
為了安全,會先校驗是否持有鎖再釋放,防止
- 業(yè)務(wù)執(zhí)行還沒執(zhí)行完,鎖到期了。(此時沒占用鎖,再unlock就會報錯)
- 主線程異常退出、或者假死
finally {
if (rLock.isLocked()) {
if (rLock.isHeldByCurrentThread()) {
rLock.unlock();
}
}
}
(5) 缺點
如果是 主從、哨兵模式,當(dāng)客戶端A 把 myLock這個鎖 key 的value寫入了 master,此時會異步復(fù)制給slave實例。
萬一在這個主從復(fù)制的過程中 master 宕機了,主備切換,slave 變成了master。
那么這個時候 slave還沒來得及加鎖,此時 客戶端A的myLock的 值是沒有的,客戶端B在請求時,myLock卻成功為自己加了鎖。這時候分布式鎖就失效了,就會導(dǎo)致數(shù)據(jù)有問題。
所以說Redis分布式說最大的缺點就是宕機導(dǎo)致多個客戶端加鎖,導(dǎo)致臟數(shù)據(jù),不過這種幾率還是很小的。
參考: