通過本文檔,你將會了解到
- 本地鎖不香么?你搞什么分布式鎖?
- 分布式鎖的產(chǎn)生背景?
- 分布式鎖怎么玩?以及玩起來的注意事項
1、 回顧
上次我們講到了,本地緩存的缺陷,所以嘍,直接用一個緩存中間件redis嘍。關(guān)于怎么使用,就不扯了,大家都會。接下來主要講的是我們項目中使用到的分布式鎖。
2、鎖
2.1、本地鎖

說到加鎖 我們有很多方式,比如synchronized,JUC包下的各種各樣的鎖,但是這種鎖只能鎖住當(dāng)前進(jìn)程,我們部署10臺機器,本地鎖就玩不轉(zhuǎn)了。所以分布式鎖說那我來吧,這也是我們項目中使用到的。我們所有的機器都先去占坑,如果占坑成功,那么你想怎么樣就怎么樣。
所以嘍要使用同一個鎖,我們使用redis分布式鎖,當(dāng)然也有ZK鎖。我們講我們項目中使用到的redis分布式鎖。我們可以同時去一個地方“占坑”,如果占到,就執(zhí)行邏輯。否則就必須等待,直到釋放鎖。 “占坑”可以去redis,可以去數(shù)據(jù)庫,可以去任何大家都能訪問的地方。 等待可以自旋的方式。
上次我們講到緩存擊穿的時候說到過:加鎖。大量并發(fā)只讓一個人去查,其他人等待,查到之后釋放鎖,其他人獲取到鎖,先查緩存,就會有數(shù)據(jù),不用去查數(shù)據(jù)庫
2.2、分布式鎖
2.2.1、介紹
分布式鎖的原理就是占坑唄,所有的人都想上廁所,那誰占到坑,那誰上嘍,分布式鎖大概就是這個原理搞的。

廢話不多少,直接上菜唄,Redis里面那個是占坑的?說到學(xué)習(xí)一門新的知識,那么權(quán)威肯定是官網(wǎng)嘍。
redis官網(wǎng)
http://www.redis.cn/
http://www.redis.cn/commands/set.html
有個命令比較有特色就是set命令 這個就是占坑的命令。這個坑占起來還有講究,hahah
SET 里面放個key 一個value 這個后面可是有可選參數(shù)哦,有的是過期時間,有一個是NX,所以綜上所述
占坑命令就是
set lock haha NX
SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds – Set the specified expire time, in seconds.
PX milliseconds – Set the specified expire time, in milliseconds.
NX – Only set the key if it does not already exist.
XX – Only set the key if it already exist.
EX seconds – 設(shè)置鍵key的過期時間,單位時秒
PX milliseconds – 設(shè)置鍵key的過期時間,單位時毫秒
NX – 只有鍵key不存在的時候才會設(shè)置key的值
XX – 只有鍵key存在的時候才會設(shè)置key的值
注意: 由于SET命令加上選項已經(jīng)可以完全取代SETNX, SETEX, PSETEX的功能,所以在將來的版本中,redis可能會不推薦使用并且最終拋棄這幾個命令。
2.2.2、分布式鎖的演進(jìn)過程
巧了,我懂了,直接用set lock haha NX 分布式鎖搞的,分享結(jié)束。你覺得會是這么簡單么?不要太天真,到時候發(fā)現(xiàn)小丑竟是我自己hahaha。
2.2.2.1、青銅玩家
來上菜。我們先看青銅玩家怎么玩?
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "11111");
if (lock) {
try {
//加鎖成功...執(zhí)行業(yè)務(wù)
XXXXXXX
//刪除
stringRedisTemplate.delete(XXXXXX);
}else{
//重試 自旋
1.自己調(diào)取自己
2.可以休眠1S
}
你看看青銅玩家,一看就是新手。一眼就看出來的錯誤就是異常了沒人解鎖了。
解決:
- 設(shè)置鎖的自動過期,即使沒有刪除,會自動刪除。
- try 包裹 finally解鎖
2.2.2.2、白銀玩家
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if (lock) {
try {
//加鎖成功...執(zhí)行業(yè)務(wù)
}else{
//重試 自旋
1.自己調(diào)取自己
2.可以休眠1S
} finally{
//刪除
stringRedisTemplate.delete(XXXXXX);
}
你看看白銀玩家,有點那么個意思了是吧?那白銀玩家有沒有問題呢?階段才是白銀了,那肯定是有問題的,關(guān)鍵是問題在哪里?
1、刪除鎖直接刪除???
如果由于業(yè)務(wù)時間很長,鎖自己過期了,我們直接刪除,有可能把別人正在持有的鎖刪除了。
2、解決:
- 占鎖的時候,值指定為uuid,每個人匹配是自己的鎖才刪除。。
2.2.2.2、黃金玩家

關(guān)鍵是問題在哪里?
問題:
1、如果正好判斷是當(dāng)前值,正要刪除鎖的時候,鎖已經(jīng)過期, 別人已經(jīng)設(shè)置到了新的值。那么我們刪除的是別人的鎖。
2、解決:
- 刪除鎖必須保證原子性。使用redis+Lua腳本完成
2.2.2.2、鉆石玩家
//1、占分布式鎖。去redis占坑 設(shè)置過期時間必須和加鎖是同步的,保證原子性(避免死鎖)
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if (lock) {
System.out.println("獲取分布式鎖成功...");
Map<String, List<Catelog2Vo>> dataFromDb = null;
try {
//加鎖成功...執(zhí)行業(yè)務(wù)
dataFromDb = getDataFromDb();
} finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//刪除鎖
stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
當(dāng)然線上你可以這么寫,但是畢竟是鉆石玩家,我們還有更好的玩法。
2.3、Redisson 作為分布式鎖
這個也是我們項目中使用到的。廢話不多說,第一步打開官網(wǎng),開啟尋寶之路。
https://www.redis.io/topics/distlock
分布式鎖的介紹頁面

2.3.1 Redisson 是什么
一種 可重入、持續(xù)阻塞、獨占式的 分布式鎖協(xié)調(diào)框架。
Redisson的宗旨是促進(jìn)使用者對Redis的關(guān)注分離(Separation of Concern),從而讓使用者能夠?qū)⒕Ω械胤旁谔幚順I(yè)務(wù)邏輯上。
特點:
- 可重入
拿到鎖的線程后續(xù)拿鎖可跳過獲取鎖的步驟,只進(jìn)行value+1的步驟。 - 持續(xù)阻塞
獲取不到鎖的線程,會在一定時間內(nèi)等待鎖。 - 獨占式
同一環(huán)境下理論上只能有一個線程可以獲取到鎖
比較爽的一點是什么?
基于Redis的Redisson分布式可重入鎖RLock Java對象實現(xiàn)了java.util.concurrent.locks.Lock接口。
這句話什么意思?如果你已經(jīng)了解了JUC包下的各種鎖,什么ReentrantLock,信號量什么的。恭喜你無縫切換。就像你換手機一樣,iPhone12換iPhone13 像流水切換一樣自然。以前怎么用,現(xiàn)在還怎么用。
關(guān)于并發(fā)編程JUC的知識不在本次分享范圍內(nèi)。
//1、獲取一把鎖,只要鎖的名字一樣,就是同一把鎖
RLock myLock = redisson.getLock("my-lock");
//2、加鎖
myLock.lock(); //阻塞式等待。默認(rèn)加的鎖都是30s
//1)、鎖的自動續(xù)期,如果業(yè)務(wù)超長,運行期間自動鎖上新的30s。不用擔(dān)心業(yè)務(wù)時間長,鎖自動過期被刪掉
//2)、加鎖的業(yè)務(wù)只要運行完成,就不會給當(dāng)前鎖續(xù)期,即使不手動解鎖,鎖默認(rèn)會在30s內(nèi)自動過期,不會產(chǎn)生死鎖問題
// myLock.lock(10,TimeUnit.SECONDS); //10秒鐘自動解鎖,自動解鎖時間一定要大于業(yè)務(wù)執(zhí)行時間
//問題:在鎖時間到了以后,不會自動續(xù)期
//1、如果我們傳遞了鎖的超時時間,就發(fā)送給redis執(zhí)行腳本,進(jìn)行占鎖,默認(rèn)超時就是 我們制定的時間
//2、如果我們指定鎖的超時時間,就使用 lockWatchdogTimeout = 30 * 1000 【看門狗默認(rèn)時間】
//只要占鎖成功,就會啟動一個定時任務(wù)【重新給鎖設(shè)置過期時間,新的過期時間就是看門狗的默認(rèn)時間】,每隔10秒都會自動的再次續(xù)期,續(xù)成30秒
// internalLockLeaseTime 【看門狗時間】 / 3, 10s
try {
System.out.println("加鎖成功,執(zhí)行業(yè)務(wù)..." + Thread.currentThread().getId());
try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception ex) {
ex.printStackTrace();
} finally {
//3、解鎖 假設(shè)解鎖代碼沒有運行,Redisson會不會出現(xiàn)死鎖
System.out.println("釋放鎖..." + Thread.currentThread().getId());
myLock.unlock();
}
關(guān)于看門狗,就是業(yè)務(wù)執(zhí)行需要10秒,還沒執(zhí)行完就解鎖了?看門狗自動幫你續(xù)命時間(這個是一個定時任務(wù),沒10秒執(zhí)行一次看看要不要續(xù)命)當(dāng)前線程活著就續(xù)命,線程(而key的組成應(yīng)該是:{uuid}:{threadid})只有沒有指定過去時間才會啟動。
最佳實踐:我們指定過期時間,性能會有一點增加,指定3秒過期時間,3秒都沒執(zhí)行完,那早就超時了什么的。