Springboot基于Redisson實現(xiàn)Redis分布式可重入鎖源碼解析
一、前言
我們在實現(xiàn)使用Redis實現(xiàn)分布式鎖,最開始一般使用SET resource-name anystring NX EX max-lock-time進(jìn)行加鎖,使用Lua腳本保證原子性進(jìn)行實現(xiàn)釋放鎖。這樣手動實現(xiàn)比較麻煩,對此Redis官網(wǎng)也明確說Java版使用Redisson來實現(xiàn)。小編也是看了官網(wǎng)慢慢的摸索清楚,特寫此記錄一下。從官網(wǎng)到整合Springboot到源碼解讀,以單節(jié)點為例,小編的理解都在注釋里,希望可以幫助到大家??!
二、為什么使用Redisson
1. 我們打開官網(wǎng)
2. 我們可以看到官方讓我們?nèi)ナ褂闷渌?/h3>

3. 打開官方推薦

4. 找到文檔

5. Redisson結(jié)構(gòu)

三、Springboot整合Redisson
1. 導(dǎo)入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!--redis分布式鎖-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>2. 以官網(wǎng)為例查看如何配置

3. 編寫配置類
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author wangzhenjun
* @date 2022/2/9 9:57
*/
@Configuration
public class MyRedissonConfig {
/**
* 所有對redisson的使用都是通過RedissonClient來操作的
* @return
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson(){
// 1. 創(chuàng)建配置
Config config = new Config();
// 一定要加redis://
config.useSingleServer().setAddress("redis://192.168.17.130:6379");
// 2. 根據(jù)config創(chuàng)建出redissonClient實例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
4. 官網(wǎng)測試加鎖例子

5. 根據(jù)官網(wǎng)簡單Controller接口編寫
@ResponseBody
@GetMapping("/hello")
public String hello(){
// 1.獲取一把鎖,只要鎖名字一樣,就是同一把鎖
RLock lock = redisson.getLock("my-lock");
// 2. 加鎖
lock.lock();// 阻塞試等待 默認(rèn)加的都是30s
// 帶參數(shù)情況
// lock.lock(10, TimeUnit.SECONDS);// 10s自動解鎖,自動解鎖時間一定要大于業(yè)務(wù)的執(zhí)行時間。
try {
System.out.println("加鎖成功" + Thread.currentThread().getId());
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 3. 解鎖
System.out.println("解鎖成功:" + Thread.currentThread().getId());
lock.unlock();
}
return "hello";
}6. 測試

四、lock.lock()源碼分析
1. 打開RedissonLock實現(xiàn)類

2. 找到實現(xiàn)方法
@Override
public void lock() {
try {
// 我們發(fā)現(xiàn)不穿過期時間源碼默認(rèn)過期時間為-1
lock(-1, null, false);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}3. 按住Ctrl進(jìn)去lock方法
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
// 獲取線程的id,占有鎖的時候field的值為UUID:線程號id
long threadId = Thread.currentThread().getId();
// 嘗試獲得鎖
Long ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired 獲得鎖,返回
if (ttl == null) {
return;
}
// 這里說明獲取鎖失敗,就通過線程id訂閱這個鎖
RFuture<RedissonLockEntry> future = subscribe(threadId);
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
try {
// 這里進(jìn)行自旋,不斷嘗試獲取鎖
while (true) {
// 繼續(xù)嘗試獲取鎖
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired 獲取成功
if (ttl == null) {
// 直接返回,挑出自旋
break;
}
// waiting for message 繼續(xù)等待獲得鎖
if (ttl >= 0) {
try {
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
// 取消訂閱
unsubscribe(future, threadId);
}
// get(lockAsync(leaseTime, unit));
}4. 進(jìn)去嘗試獲取鎖方法

private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
// 直接進(jìn)入異步方法
return get(tryAcquireAsync(leaseTime, unit, threadId));
}
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
// 這里進(jìn)行判斷如果沒有設(shè)置參數(shù)leaseTime = -1
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
// 此方法進(jìn)行獲得鎖,過期時間為看門狗的默認(rèn)時間
// private long lockWatchdogTimeout = 30 * 1000;看門狗默認(rèn)過期時間為30s
// 加鎖和過期時間要保證原子性,這個方法后面肯定調(diào)用執(zhí)行了Lua腳本,我們下面在看
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
// 開啟一個定時任務(wù)進(jìn)行不斷刷新過期時間
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired 獲得鎖
if (ttlRemaining == null) {
// 刷新過期時間方法,我們下一步詳細(xì)說一下
scheduleExpirationRenewal(threadId);
});
return ttlRemainingFuture;5. 查看tryLockInnerAsync()方法
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
// 首先判斷鎖是否存在
"if (redis.call('exists', KEYS[1]) == 0) then " +
// 存在則獲取鎖
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
// 然后設(shè)置過期時間
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// hexists查看哈希表的指定字段是否存在,存在鎖并且是當(dāng)前線程持有鎖
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
// hincrby自增一
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
// 鎖的值大于1,說明是可重入鎖,重置過期時間
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// 鎖已存在,且不是本線程,則返回過期時間ttl
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}6. 進(jìn)入4留下的定時任務(wù)scheduleExpirationRenewal()方法
一步步往下找源碼:scheduleExpirationRenewal --->renewExpiration
根據(jù)下面源碼,定時任務(wù)刷新時間為:internalLockLeaseTime / 3,是看門狗的1/3,即為10s刷新一次
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
// reschedule itself
renewExpiration();
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}五、lock.lock(10, TimeUnit.SECONDS)源碼分析
1. 打開實現(xiàn)類
@Override
public void lock(long leaseTime, TimeUnit unit) {
try {
// 這里的過期時間為我們輸入的10
lock(leaseTime, unit, false);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}2. 方法lock()實現(xiàn)展示,同三.3源碼
3. 直接來到嘗試獲得鎖tryAcquireAsync()方法
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
// 這里進(jìn)行判斷如果沒有設(shè)置參數(shù)leaseTime = -1,此時我們?yōu)?0
if (leaseTime != -1) {
// 來到此方法
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
// 此處省略后面內(nèi)容,前面以詳細(xì)說明。。。。
}4. 打開tryLockInnerAsync()方法
我們不難發(fā)現(xiàn)和沒有傳過期時間的方法一樣,只不過leaseTime的值變了。
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
// 首先判斷鎖是否存在
"if (redis.call('exists', KEYS[1]) == 0) then " +
// 存在則獲取鎖
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
// 然后設(shè)置過期時間
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// hexists查看哈希表的指定字段是否存在,存在鎖并且是當(dāng)前線程持有鎖
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
// hincrby自增一
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
// 鎖的值大于1,說明是可重入鎖,重置過期時間
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
// 鎖已存在,且不是本線程,則返回過期時間ttl
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}六、lock.unlock()源碼分析
1. 打開方法實現(xiàn)
@Override
public void unlock() {
try {
// 點擊進(jìn)入釋放鎖方法
get(unlockAsync(Thread.currentThread().getId()));
} catch (RedisException e) {
if (e.getCause() instanceof IllegalMonitorStateException) {
throw (IllegalMonitorStateException) e.getCause();
} else {
throw e;
}
}
// Future<Void> future = unlockAsync();
// future.awaitUninterruptibly();
// if (future.isSuccess()) {
// return;
// }
// if (future.cause() instanceof IllegalMonitorStateException) {
// throw (IllegalMonitorStateException)future.cause();
// }
// throw commandExecutor.convertException(future);
}2. 打開unlockAsync()方法
@Override
public RFuture<Void> unlockAsync(long threadId) {
RPromise<Void> result = new RedissonPromise<Void>();
// 解鎖方法,后面展開說
RFuture<Boolean> future = unlockInnerAsync(threadId);
// 完成
future.onComplete((opStatus, e) -> {
if (e != null) {
// 取消到期續(xù)訂
cancelExpirationRenewal(threadId);
// 將這個未來標(biāo)記為失敗并通知所有人
result.tryFailure(e);
return;
}
// 狀態(tài)為空,說明解鎖的線程和當(dāng)前鎖不是同一個線程
if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + threadId);
result.tryFailure(cause);
return;
}
cancelExpirationRenewal(threadId);
result.trySuccess(null);
});
return result;
}
3. 打開unlockInnerAsync()方法
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 判斷釋放鎖的線程和已存在鎖的線程是不是同一個線程,不是返回空
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
// 釋放鎖后,加鎖次數(shù)減一
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
// 判斷剩余數(shù)量是否大于0
"if (counter > 0) then " +
// 大于0 ,則刷新過期時間
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
// 釋放鎖,刪除key并發(fā)布鎖釋放的消息
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}七、總結(jié)
這樣大家就跟著小編走完了一遍底層源碼,是不是感覺自己又行了,哈哈哈。小編走下來一遍覺得收貨還是蠻大的,以前不敢點進(jìn)去源碼,進(jìn)去就懵逼了,所以人要大膽的向前邁出第一步。
到此這篇關(guān)于Springboot基于Redisson實現(xiàn)Redis分布式可重入鎖【案例到源碼分析】的文章就介紹到這了,更多相關(guān)SpringbootRedis分布式可重入鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Hadoop環(huán)境配置之hive環(huán)境配置詳解
這篇文章主要介紹了Hadoop環(huán)境配置之hive環(huán)境配置,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-12-12
Spring Shell 命令行實現(xiàn)交互式Shell應(yīng)用開發(fā)
本文主要介紹了Spring Shell 命令行實現(xiàn)交互式Shell應(yīng)用開發(fā),能夠幫助開發(fā)者快速構(gòu)建功能豐富的命令行應(yīng)用程序,具有一定的參考價值,感興趣的可以了解一下2025-04-04
詳解Java8的forEach(...)如何提供index值
這篇文章主要介紹了詳解Java8的forEach(...)如何提供index值,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
Intellij Idea部署OpenCV 4.0.0環(huán)境
這篇文章主要為大家詳細(xì)介紹了Intellij Idea部署OpenCV 4.0.0環(huán)境,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-07-07
Java數(shù)據(jù)結(jié)構(gòu)之對象比較詳解
這篇文章主要為大家詳細(xì)介紹了Java中對象的比較、集合框架中PriorityQueue的比較方式以及PriorityQueue的模擬實現(xiàn),感興趣的可以了解一下2022-07-07
Spring?Boot項目抵御XSS攻擊實戰(zhàn)過程
XSS攻擊又稱跨站腳本攻擊,通常指利用網(wǎng)頁開發(fā)時留下的漏洞,通過巧妙的方法注入惡意指令代碼到網(wǎng)頁,使用戶加載并執(zhí)行攻擊者惡意制造的網(wǎng)頁程序,下面這篇文章主要給大家介紹了關(guān)于Spring?Boot項目抵御XSS攻擊的相關(guān)資料,需要的朋友可以參考下2022-11-11
SpringBoot注解@EnableScheduling定時任務(wù)詳細(xì)解析
這篇文章主要介紹了SpringBoot注解@EnableScheduling定時任務(wù)詳細(xì)解析,@EnableScheduling 開啟對定時任務(wù)的支持,啟動類里面使用@EnableScheduling 注解開啟功能,自動掃描,需要的朋友可以參考下2024-01-01
JavaScript 與 Java 區(qū)別介紹 學(xué)java怎么樣
JavaScript 是一種嵌入式腳本文件,直接插入網(wǎng)頁,有瀏覽器一邊解釋一邊執(zhí)行。而java 語言不一樣,他必須在JAVA虛擬機(jī)上運行。而且事先需要進(jìn)行編譯。接下來腳本之家小編給大家揭曉js與java區(qū)別,感興趣的朋友一起看看吧2016-09-09
基于RecyclerChart的KLine繪制Volume實現(xiàn)詳解
這篇文章主要為大家介紹了基于RecyclerChart的KLine繪制Volume實現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03

