Redisson簡(jiǎn)明教程—你家的鎖芯該換了
1.簡(jiǎn)介
2.看門狗
2-1.為什么需要這個(gè)"狗子"?
2-2.工作原理
2-3.如何“擼狗子”
3.可重入鎖:你的分布式"萬(wàn)能鑰匙"
3-1.Redisson可重入鎖的魔法
3-2.使用姿勢(shì)大全
3-3.公平鎖
3-4.非公平鎖
3-5.可重入原理深扒
4.聯(lián)鎖:分布式鎖中的"全家桶套餐"
4-1.聯(lián)鎖是什么?——"要么全有,要么全無(wú)"的霸道總裁
4-2.為什么需要聯(lián)鎖?
4-3.聯(lián)鎖使用三件套
4-4.聯(lián)鎖的硬核原理
4-5.聯(lián)鎖的三大禁忌
5.Redisson讀寫鎖:分布式系統(tǒng)中的"讀寫分離"高手
5-1.使用姿勢(shì)
5-2.原理深扒
6.信號(hào)量(Semaphore):分布式系統(tǒng)中的"限量入場(chǎng)券"
6-1.使用姿勢(shì)大全
6-2.原理深扒
7.紅鎖(RedLock):分布式鎖界的“聯(lián)合國(guó)維和部隊(duì)”
7-1.核心原理
7-2.注意事項(xiàng)
7-3.使用姿勢(shì)
8.閉鎖(CountDownLatch):分布式系統(tǒng)中的"集結(jié)號(hào)"
8-1.使用姿勢(shì)
8-2.原理深扒
9.總結(jié)
1.簡(jiǎn)介
各位攻城獅們,你還在使用原生命令來(lái)上鎖么?看來(lái)你還是不夠懶,餃子都給你包好了,你非要吃大餅配炒韭菜,快點(diǎn)改善一下“伙食”吧,寫代碼也要來(lái)點(diǎn)幸福感。今天咱們就來(lái)聊聊Redisson提供的各種鎖,Redisson就像是Redis給Java程序員的一把瑞士軍刀,不僅能存數(shù)據(jù),還能玩出各種分布式花樣。
- Redis版本:Redis 2.8+,理想版本5.0+(支持 Stream、模塊化等高級(jí)特性,Redisson 能秀出全部技能)。
- 架構(gòu)模式:支持單機(jī)、哨兵和集群(集群模式可靠性更高)
2.看門狗
想象你上廁所(獲取鎖)時(shí)帶著一只忠心耿耿的阿黃(看門狗)。當(dāng)你蹲坑時(shí)間快到時(shí)(鎖快要過(guò)期),阿黃就會(huì)大叫:"主人你還沒(méi)完事嗎?我給你續(xù)時(shí)間啦!"(自動(dòng)續(xù)期)
2-1.為什么需要這個(gè)"狗子"?
- 防止業(yè)務(wù)沒(méi)執(zhí)行完鎖就過(guò)期:默認(rèn)鎖30秒過(guò)期,但萬(wàn)一你的業(yè)務(wù)要31秒呢?
- 避免鎖丟失:如果客戶端崩潰,看門狗停止續(xù)期,鎖最終會(huì)自動(dòng)釋放
- 不用手動(dòng)計(jì)算業(yè)務(wù)時(shí)間:再也不用戰(zhàn)戰(zhàn)兢兢估算業(yè)務(wù)執(zhí)行時(shí)間了
2-2.工作原理
- 首次加鎖:默認(rèn)設(shè)置鎖過(guò)期時(shí)間30秒
- 啟動(dòng)看門狗:加鎖成功后啟動(dòng)一個(gè)定時(shí)任務(wù)(后臺(tái)線程)
- 定期續(xù)期:每10秒(過(guò)期時(shí)間的1/3)檢查業(yè)務(wù)是否完成。未完成:執(zhí)行expire命令把鎖再續(xù)30秒;已完成:停止續(xù)期。
- 最終釋放:業(yè)務(wù)完成調(diào)用unlock或客戶端斷開(kāi)連接時(shí)釋放
2-3.如何“擼狗子”
Config config = new Config();
config.setLockWatchdogTimeout(30000L); // 單位毫秒,默認(rèn)就是30秒
// ...
RedissonClient redisson = Redisson.create(config);
// 你也可以在加鎖時(shí)指定(會(huì)覆蓋默認(rèn)值)
lock.lock(60, TimeUnit.SECONDS); // 這時(shí)看門狗會(huì)按60秒周期續(xù)期
3.可重入鎖:你的分布式"萬(wàn)能鑰匙"
我們都知道Java中ReentrantLock和synchronized都是可重入鎖,但都只能用于單機(jī)環(huán)境的,在分布式環(huán)境下,Redisson給我提供了類似體驗(yàn)的可重入鎖。
3-1.Redisson可重入鎖的魔法
- 線程安全:不同JVM的相同線程也可重入
- 自動(dòng)續(xù)期:看門狗機(jī)制?;睿J(rèn)30秒)
- 公平/非公平:兩種模式可選
- 超時(shí)機(jī)制:避免無(wú)限等待
3-2.使用姿勢(shì)大全
基礎(chǔ)款(阻塞式):
RLock lock = redisson.getLock("orderLock");
lock.lock(); // 一直等到天荒地老
try {
// 你的核心業(yè)務(wù)
} finally {
lock.unlock(); // 一定要放在finally!
}
高級(jí)款(嘗試獲?。?/span>
if (lock.tryLock(3, 30, TimeUnit.SECONDS)) { // 最多等3秒,鎖30秒自動(dòng)過(guò)期
try {
// 業(yè)務(wù)處理
} finally {
lock.unlock();
}
} else {
log.warn("獲取鎖失敗,換個(gè)姿勢(shì)再試一次");
}
騷操作款(異步獲取):
RFuture<Void> lockFuture = lock.lockAsync();
lockFuture.whenComplete((res, ex) -> {
if (ex == null) {
try {
// 異步業(yè)務(wù)處理
} finally {
lock.unlock();
}
}
});
3-3.公平鎖
- 排隊(duì)機(jī)制:使用Redis的List結(jié)構(gòu)維護(hù)等待隊(duì)列
- 訂閱發(fā)布:通過(guò)Redis的pub/sub通知下一個(gè)等待者
- 雙重檢查:獲取鎖時(shí)檢查自己是否在隊(duì)列頭部
RLock fairLock = redisson.getFairLock("myFairLock");
try {
fairLock.lock();
// 業(yè)務(wù)邏輯
} finally {
fairLock.unlock();
}
3-4.非公平鎖
- 直接競(jìng)爭(zhēng):所有線程同時(shí)嘗試CAS操作
- 效率優(yōu)先:沒(méi)有隊(duì)列維護(hù)開(kāi)銷
- 可能饑餓:運(yùn)氣差的線程可能長(zhǎng)期得不到鎖
RLock nonFairLock = redisson.getLock("hotItemLock");
if (nonFairLock.tryLock(50, TimeUnit.MILLISECONDS)) { // 拼手速!
try {
// 秒殺業(yè)務(wù)邏輯
} finally {
nonFairLock.unlock();
}
}
3-5.可重入原理深扒
加鎖Lua腳本偽代碼:
-- 參數(shù):鎖key、鎖超時(shí)時(shí)間、客戶端ID+線程ID
if (redis.call('exists', KEYS[1]) == 0) then
-- 鎖不存在,直接獲取
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
returnnil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 重入情況:計(jì)數(shù)器+1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
returnnil;
end;
-- 鎖被其他線程持有
return redis.call('pttl', KEYS[1]); -- 返回剩余過(guò)期時(shí)間
解鎖Lua腳本偽代碼:
-- 參數(shù):鎖key、客戶端ID+線程ID
if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
-- 壓根沒(méi)持有鎖
returnnil;
end;
-- 重入次數(shù)-1
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);
if (counter > 0) then
-- 還有重入次數(shù),更新過(guò)期時(shí)間
redis.call('pexpire', KEYS[1], 30000);
return0;
else
-- 最后一次解鎖,刪除key
redis.call('del', KEYS[1]);
-- 發(fā)布解鎖消息
redis.call('publish', KEYS[2], ARGV[2]);
return1;
end;
4.聯(lián)鎖:分布式鎖中的"全家桶套餐"
4-1.聯(lián)鎖是什么?——"要么全有,要么全無(wú)"的霸道總裁
想象你要同時(shí)約三個(gè)女神約會(huì):
- 女神A:周末有空 ?
- 女神B:周末有空 ?
- 女神C:周末要加班 ? Redisson聯(lián)鎖的做法是:只要有一個(gè)拒絕,就取消所有約會(huì)!這就是聯(lián)鎖的"All or Nothing"哲學(xué)。
4-2.為什么需要聯(lián)鎖?
典型場(chǎng)景:
- 跨資源事務(wù):需要同時(shí)鎖定訂單、庫(kù)存、優(yōu)惠券三個(gè)系統(tǒng)
- 數(shù)據(jù)一致性:確保多個(gè)關(guān)聯(lián)資源同時(shí)被保護(hù)
- 避免死鎖:防止交叉等待導(dǎo)致的死鎖情況
4-3.聯(lián)鎖使用三件套
基本用法:
// 準(zhǔn)備三把鎖(就像三個(gè)女神的聯(lián)系方式)
RLock lock1 = redisson.getLock("order_lock");
RLock lock2 = redisson.getLock("stock_lock");
RLock lock3 = redisson.getLock("coupon_lock");
// 創(chuàng)建聯(lián)鎖"約會(huì)套餐"
RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2, lock3);
try {
// 嘗試同時(shí)鎖定(約三位女神)
if (multiLock.tryLock(3, 30, TimeUnit.SECONDS)) {
// 三位都同意了!開(kāi)始你的表演
processOrder();
updateStock();
useCoupon();
} else {
log.warn("有個(gè)女神拒絕了你");
}
} finally {
multiLock.unlock(); // 記得送她們回家
}
高階技巧:
// 動(dòng)態(tài)構(gòu)造聯(lián)鎖(適合不確定數(shù)量的資源)
List<RLock> locks = resourceIds.stream()
.map(id -> redisson.getLock("resource_" + id))
.collect(Collectors.toList());
RedissonMultiLock dynamicLock = new RedissonMultiLock(locks.toArray(new RLock[0]));
4-4.聯(lián)鎖的硬核原理
加鎖流程:
- 順序加鎖:按傳入鎖的順序依次嘗試獲取
- 失敗回滾:任意一個(gè)鎖獲取失敗時(shí),釋放已獲得的所有鎖
- 統(tǒng)一過(guò)期時(shí)間:所有鎖使用相同的過(guò)期時(shí)間
底層Lua腳本(簡(jiǎn)化版):
-- 參數(shù):多個(gè)鎖的KEYS,統(tǒng)一過(guò)期時(shí)間,線程標(biāo)識(shí)
local failed = false
for i, key inipairs(KEYS) do
if redis.call('setnx', key, ARGV[2]) == 0then
failed = true
break
end
redis.call('expire', key, ARGV[1])
end
if failed then
-- 釋放已經(jīng)獲取的鎖
for j = 1, i-1do
redis.call('del', KEYS[j])
end
return0
end
return1
4-5.聯(lián)鎖的三大禁忌
亂序使用(導(dǎo)致死鎖):
// 線程1:
multiLock(lockA, lockB).lock();
// 線程2:
multiLock(lockB, lockA).lock(); // 危險(xiǎn)!可能死鎖
? 正確做法:全局統(tǒng)一加鎖順序
混合鎖類型:
// 混合普通鎖和公平鎖
new MultiLock(lock1, fairLock2); // 不推薦
? 正確做法:使用相同特性的鎖組合
忽略部分鎖失?。?/span>
if (!multiLock.tryLock()) {
// 直接返回,不處理部分獲取成功的情況
return; // 危險(xiǎn)!
}
? 正確做法:確保完全獲取或完全失敗
5.Redisson讀寫鎖:分布式系統(tǒng)中的"讀寫分離"高手
也許單機(jī)版的ReentrantReadWriteLock你聽(tīng)說(shuō)過(guò),但是分布式環(huán)境下的版本可能很少接觸到。
典型場(chǎng)景:
- 讀多寫少系統(tǒng):比如商品詳情頁(yè)(每秒上萬(wàn)次讀取,每分鐘幾次更新)
- 數(shù)據(jù)一致性要求:保證讀取時(shí)不會(huì)讀到半成品數(shù)據(jù)
- 系統(tǒng)性能優(yōu)化:避免讀操作被不必要的串行化
5-1.使用姿勢(shì)
RReadWriteLock rwLock = redisson.getReadWriteLock("libraryBook_123");
// 讀操作(多個(gè)線程可同時(shí)進(jìn)入)
rwLock.readLock().lock();
try {
// 查詢數(shù)據(jù)(安全讀?。? Book book = getBookFromDB(123);
} finally {
rwLock.readLock().unlock();
}
// 寫操作(獨(dú)占訪問(wèn))
rwLock.writeLock().lock();
try {
// 修改數(shù)據(jù)(安全寫入)
updateBookInDB(123, newVersion);
} finally {
rwLock.writeLock().unlock();
}
5-2.原理深扒
加讀鎖Lua腳本(簡(jiǎn)化):
-- 檢查是否可以加讀鎖(沒(méi)有寫鎖或當(dāng)前線程持有寫鎖)
if redis.call('hget', KEYS[1], 'mode') == 'write' then
-- 如果有寫鎖且不是當(dāng)前線程持有,則失敗
if redis.call('hexists', KEYS[1], ARGV[2]) == 0 then
return 0;
end;
end;
-- 增加讀鎖計(jì)數(shù)
redis.call('hincrby', KEYS[1], ARGV[1], 1);
redis.call('pexpire', KEYS[1], ARGV[3]);
return 1;
加寫鎖Lua腳本(簡(jiǎn)化):
-- 檢查是否已有鎖
if redis.call('exists', KEYS[1]) == 1 then
-- 如果是讀模式或有其他寫鎖
if redis.call('hget', KEYS[1], 'mode') == 'read' or
redis.call('hlen', KEYS[1]) > 1 then
return0;
end;
-- 如果是當(dāng)前線程持有的寫鎖(重入)
if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
return1;
end;
end;
-- 獲取寫鎖
redis.call('hset', KEYS[1], 'mode', 'write');
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[3]);
return1;
6.信號(hào)量(Semaphore):分布式系統(tǒng)中的"限量入場(chǎng)券"
同樣的味道,Java中Semaphore的分布式版本。
6-1.使用姿勢(shì)大全
基礎(chǔ)用法:
// 獲取信號(hào)量(初始10個(gè)許可)
RSemaphore semaphore = redisson.getSemaphore("apiLimit");
semaphore.trySetPermits(10); // 設(shè)置許可數(shù)量
// 獲取許可(阻塞直到可用)
semaphore.acquire();
try {
// 執(zhí)行業(yè)務(wù)(保證最多10個(gè)并發(fā))
callLimitedAPI();
} finally {
semaphore.release(); // 記得歸還!
}
// 嘗試獲取(非阻塞)
if (semaphore.tryAcquire()) {
try {
// 搶到許可了!
} finally {
semaphore.release();
}
} else {
log.warn("系統(tǒng)繁忙,請(qǐng)稍后再試");
}
高級(jí)技巧:
// 帶超時(shí)的嘗試獲取
if (semaphore.tryAcquire(3, 500, TimeUnit.MILLISECONDS)) {
try {
// 在500ms內(nèi)獲取到3個(gè)許可
batchProcess();
} finally {
semaphore.release(3); // 歸還多個(gè)許可
}
}
// 動(dòng)態(tài)調(diào)整許可數(shù)量
semaphore.addPermits(5); // 增加5個(gè)許可(擴(kuò)容)
semaphore.reducePermits(3); // 減少3個(gè)許可(縮容)
6-2.原理深扒
獲取許可的Lua腳本(簡(jiǎn)化):
-- 參數(shù):信號(hào)量key、請(qǐng)求許可數(shù)
local value = redis.call('get', KEYS[1])
if value >= ARGV[1] then
return redis.call('decrby', KEYS[1], ARGV[1])
else
return -1
end
釋放許可的Lua腳本(簡(jiǎn)化):
-- 參數(shù):信號(hào)量key、釋放許可數(shù)
return redis.call('incrby', KEYS[1], ARGV[1])
7.紅鎖(RedLock):分布式鎖界的“聯(lián)合國(guó)維和部隊(duì)”
RedLock 的誕生是為了對(duì)抗單點(diǎn) Redis 掛掉后鎖失效的問(wèn)題。它的目標(biāo)就是:“即使部分節(jié)點(diǎn)掛了,我也要穩(wěn)如老狗”。
7-1.核心原理
Redisson 的 RedLock 實(shí)現(xiàn)來(lái)源于 Redis 作者 antirez 提出的 Redlock 算法。流程如下:
- 準(zhǔn)備多個(gè)獨(dú)立的 Redis 節(jié)點(diǎn)(注意:是互相獨(dú)立的,不是主從復(fù)制結(jié)構(gòu))。
- 客戶端依次向這些節(jié)點(diǎn)嘗試加鎖(使用 SET NX PX 命令)。
- 記錄耗時(shí):加鎖操作總共不能超過(guò)鎖過(guò)期時(shí)間的 1/2(比如設(shè)置鎖有效期 10 秒,那就必須 5 秒內(nèi)搞定加鎖)。
- 加鎖成功節(jié)點(diǎn)超過(guò)半數(shù)(N/2 + 1)視為成功。
- 若失敗,立刻釋放所有加鎖成功的節(jié)點(diǎn),以避免資源死鎖。
- 釋放鎖時(shí),同樣要向所有節(jié)點(diǎn)發(fā)送 unlock 操作。
7-2.注意事項(xiàng)
- RedLock 是為多主 Redis 實(shí)例準(zhǔn)備的,不是給 Redis Cluster 用的。
- 你得維護(hù)多個(gè)彼此獨(dú)立的 Redis 實(shí)例,部署和運(yùn)維成本更高。
- RedLock 的“強(qiáng)一致性”并非線性一致性,它只是通過(guò)多點(diǎn)確認(rèn)提升“高可用性”。
7-3.使用姿勢(shì)
// 準(zhǔn)備多個(gè)獨(dú)立的RLock實(shí)例
RLock lock1 = redissonClient1.getLock("lock");
RLock lock2 = redissonClient2.getLock("lock");
RLock lock3 = redissonClient3.getLock("lock");
// 構(gòu)造紅鎖(建議奇數(shù)個(gè),通常3/5個(gè))
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
// 嘗試獲取鎖(等待時(shí)間100s,鎖持有時(shí)間30s)
boolean locked = redLock.tryLock(100, 30, TimeUnit.SECONDS);
if (locked) {
// 執(zhí)行業(yè)務(wù)邏輯
doCriticalWork();
}
} finally {
redLock.unlock();
}
8.閉鎖(CountDownLatch):分布式系統(tǒng)中的"集結(jié)號(hào)"
一樣是Java的CountDownLatch的分布式版本,用法也是基本一樣。
8-1.使用姿勢(shì)
// 主協(xié)調(diào)節(jié)點(diǎn)(教官)
RCountDownLatch latch = redisson.getCountDownLatch("batchTaskLatch");
latch.trySetCount(5); // 需要等待5個(gè)任務(wù)
// 工作節(jié)點(diǎn)(學(xué)員)
RCountDownLatch workerLatch = redisson.getCountDownLatch("batchTaskLatch");
workerLatch.countDown(); // 完成任務(wù)時(shí)調(diào)用
// 主節(jié)點(diǎn)等待(在另一個(gè)線程/JVM)
latch.await(); // 阻塞直到計(jì)數(shù)器歸零
System.out.println("所有任務(wù)已完成!");
8-2.原理深扒
關(guān)鍵操作偽代碼:
-- countDown操作
local remaining = redis.call('decr', KEYS[1])
if remaining <= 0then
redis.call('publish', KEYS[2], '0') -- 通知所有等待者
redis.call('del', KEYS[1]) -- 清理計(jì)數(shù)器
end
return remaining
-- await操作
local count = redis.call('get', KEYS[1])
if count == falseortonumber(count) <= 0then
return1-- 已經(jīng)完成
end
return0-- 需要繼續(xù)等待
9.總結(jié)
Redisson 提供了豐富的分布式鎖實(shí)現(xiàn),適用于各種分布式場(chǎng)景,使用體驗(yàn)更好,選擇鎖類型時(shí)應(yīng)根據(jù)具體業(yè)務(wù)場(chǎng)景和需求來(lái)決定,同時(shí)要注意鎖的粒度和持有時(shí)間,避免分布式死鎖和性能問(wèn)題。
關(guān)于作者,高宏杰,轉(zhuǎn)轉(zhuǎn)門店技術(shù)部研發(fā)工程師。