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