如今基本上都是分布式、多節(jié)點(diǎn)時(shí)代,我們業(yè)務(wù)代碼中避免不了需要使用分布式鎖。admin4j-lock
為我們提供分布式鎖解決方案。支持redisson
和zookeeper
分布式鎖
功能
-
支持redisson分布式鎖和zookeeper 分布式鎖
-
支持可重入鎖
-
支持讀寫(xiě)鎖
-
支持紅鎖 redLock
-
支持一個(gè)注解解決分布式鎖問(wèn)題
-
支持一個(gè)注解解決接口冪等性問(wèn)題
-
支持編程式使用分布式鎖
-
鎖名稱支持 el表達(dá)式
使用方式
引入POM, 默認(rèn)使用redisson分布式鎖
<dependency>
<groupId>com.admin4j</groupId>
<artifactId>lock-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>
最新版查看 https://central.sonatype.com/artifact/com.admin4j/lock-spring-boot-starter/
注解方式使用
@DistributedLock(value = "'testDLock:'+#id", user = true)
public R testDLock(String name, Integer id) throws InterruptedException {
Thread.sleep(30000);
return R.ok();
}
DistributedLock 注解參數(shù)詳解
- prefix:鎖key的前綴
- lockModel:指定鎖模式 REENTRANT(可重入鎖),FAIR(公平鎖) ,REDLOCK(紅鎖),READ(讀鎖), WRITE(寫(xiě)鎖)
- key、value: 鎖名稱,支持el 表達(dá)式
- keyGenerator: 所名稱生成器。Spring注入基礎(chǔ)DLockKeyGenerator實(shí)現(xiàn)類即可
- tryLock:是否嘗試獲取鎖。成功獲取則進(jìn)入鎖;獲取失敗則拋出異常。 true 獲取不到鎖,會(huì)立即返回,不會(huì)阻塞。false(默認(rèn))
獲取不到鎖,會(huì)阻塞當(dāng)前線程 - tenant: 是否開(kāi)啟租戶(默認(rèn)false)。開(kāi)啟租戶會(huì)在可能后面拼接上租戶。 需要實(shí)現(xiàn) ILoginUserInfoService 接口告訴當(dāng)前登錄用戶的租戶信息
- user:是否開(kāi)啟用戶(默認(rèn)false)開(kāi)啟用戶模式 需要實(shí)現(xiàn) ILoginUserInfoService 接口告訴當(dāng)前登錄用戶的唯一ID標(biāo)識(shí)
- executor: 分布式鎖執(zhí)行器。指定使用 redisson 還是 zookeeper 分布式鎖
編程式使用
//使用工具類
DistributedLockUtil.tryLockWithError("DistributedLock:" + id, () -> {
System.out.println("i get the lock = " + name);
//doSomething
});
使用zookeeper 分布式鎖
<dependency>
<groupId>com.admin4j</groupId>
<artifactId>lock-spring-boot-starter</artifactId>
<version>0.2.0</version>
<exclusions>
<exclusion>
<artifactId>lock-redisson-spring-boot-starter</artifactId>
<groupId>com.admin4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.admin4j</groupId>
<artifactId>lock-zookeeper-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>
引入lock-zookeeper-spring-boot-starter
依賴,默認(rèn)使用zookeeper分布式鎖
指定分布式鎖執(zhí)行器
同時(shí)引入 zookeeper 和 redisson 。默認(rèn)使用redisson,可以指定 executor 執(zhí)行器來(lái)切換分布式類型
<dependency>
<groupId>com.admin4j</groupId>
<artifactId>lock-spring-boot-starter</artifactId>
<version>0.2.0</version>
<exclusions>
<exclusion>
<artifactId>lock-redisson-spring-boot-starter</artifactId>
<groupId>com.admin4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.admin4j</groupId>
<artifactId>lock-zookeeper-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>
指定 zookeeper分布式鎖 例子:
@DistributedLock( value = "'testDLock:'+#id", user = true, executor = ZookeeperLockExecutor.class)
public R testDLock(String name, Integer id) throws InterruptedException {
Thread.sleep(30000);
return R.ok();
}
指定 redisson分布式鎖 例子:
@DistributedLock( value = "'testDLock:'+#id", user = true, executor = RedissonLockExecutor.class)
public R testDLock(String name, Integer id) throws InterruptedException {
Thread.sleep(30000);
return R.ok();
}
使用示例代碼 https://github.com/admin4j/admin4j-example
一個(gè)注解搞定接口冪等性
@GetMapping("Idempotent")
@Idempotent(tryLock = true, key = "'Idempotent'+#id")
public R Idempotent(String name, Integer id) throws InterruptedException {
Thread.sleep(30000);
return R.ok();
}
需要實(shí)現(xiàn) ILoginUserInfoService 接口,返回當(dāng)前登錄用戶唯一ID
分布式鎖原理
1. redis 原生命令的不足
使用redis做分布式鎖相對(duì)于更簡(jiǎn)單和高效。但不是說(shuō)用了redis分布式鎖,就可以高枕無(wú)憂了,如果沒(méi)有用好或者用對(duì),也會(huì)引來(lái)一些意想不到的坑。
1.1 非原子性操作
if (jedis.setnx(lockKey, val) == 1) {
jedis.expire(lockKey, timeout);
}
改進(jìn)方式使用
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
return false;
1.2 釋放了別人的鎖
假如線程A和線程B,都使用lockKey加鎖。線程A加鎖成功了,但是由于業(yè)務(wù)功能耗時(shí)時(shí)間很長(zhǎng),超過(guò)了設(shè)置的超時(shí)時(shí)間。這時(shí)候,redis會(huì)自動(dòng)釋放lockKey鎖。此時(shí),線程B就能給lockKey加鎖成功了,接下來(lái)執(zhí)行它的業(yè)務(wù)操作。恰好這個(gè)時(shí)候,線程A執(zhí)行完了業(yè)務(wù)功能,接下來(lái),在finally方法中釋放了鎖lockKey。這不就出問(wèn)題了,線程B的鎖,被線程A釋放了。不知道你們注意到?jīng)]?在使用set
命令加鎖時(shí),除了使用lockKey鎖標(biāo)識(shí),還多設(shè)置了一個(gè)參數(shù):requestId
,為什么要需要記錄requestId呢?
答:requestId是在釋放鎖的時(shí)候用的。
在釋放鎖的時(shí)候,先獲取到該鎖的值(之前設(shè)置值就是requestId),然后判斷跟之前設(shè)置的值是否相同,如果相同才允許刪除鎖,返回成功。如果不同,則直接返回失敗。
此外,使用lua腳本,也能解決釋放了別人的鎖的問(wèn)題:
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
1.3 搶不到鎖的線程不會(huì)阻塞,大量失敗請(qǐng)求
當(dāng)大量請(qǐng)求進(jìn)入時(shí),只有一個(gè)會(huì)成功,其他的都是失敗。每1萬(wàn)個(gè)請(qǐng)求,有1個(gè)成功。再1萬(wàn)個(gè)請(qǐng)求,有1個(gè)成功。如此下去,直到庫(kù)存不足。這就變成均勻分布的秒殺了,跟我們想象中的不一樣。
1.4 不支持鎖重入問(wèn)題
1.5 鎖競(jìng)爭(zhēng)問(wèn)題,不支持讀寫(xiě)鎖,鎖顆粒度大
如果有大量需要寫(xiě)入數(shù)據(jù)的業(yè)務(wù)場(chǎng)景,使用普通的redis分布式鎖是沒(méi)有問(wèn)題的。
但如果有些業(yè)務(wù)場(chǎng)景,寫(xiě)入的操作比較少,反而有大量讀取的操作。這樣直接使用普通的redis分布式鎖,會(huì)不會(huì)有點(diǎn)浪費(fèi)性能?
我們都知道,鎖的粒度越粗,多個(gè)線程搶鎖時(shí)競(jìng)爭(zhēng)就越激烈,造成多個(gè)線程鎖等待的時(shí)間也就越長(zhǎng),性能也就越差。
所以,提升redis分布式鎖性能的第一步,就是要把鎖的粒度變細(xì)。添加讀寫(xiě)鎖
1.6 鎖超時(shí)問(wèn)題
如果線程A加鎖成功了,但是由于業(yè)務(wù)功能耗時(shí)時(shí)間很長(zhǎng),超過(guò)了設(shè)置的超時(shí)時(shí)間,這時(shí)候redis會(huì)自動(dòng)釋放線程A加的鎖。其他線程就會(huì)搶到鎖,但是A線程還未結(jié)束
1.7 主從復(fù)制的問(wèn)題
如果redis存在多個(gè)實(shí)例。比如:做了主從,或者使用了哨兵模式,由于redis 主從復(fù)制是異步的(AP模型) 就會(huì)出現(xiàn)問(wèn)題??梢酝ㄟ^(guò)redLock
解決
2. redisson 分布式鎖接口方案
使用原生的redis 會(huì)有各種問(wèn)題,我們來(lái)看看redisson框架給我的解決方法
2.1 看門狗原理
如果負(fù)責(zé)儲(chǔ)存這個(gè)分布式鎖的 Redisson
節(jié)點(diǎn)宕機(jī)以后,而且這個(gè)鎖正好處于鎖住的狀態(tài)時(shí),這個(gè)鎖會(huì)出現(xiàn)鎖死的狀態(tài)。為了避免這種情況的發(fā)生,Redisson內(nèi)部提供了一個(gè)監(jiān)控鎖的看門狗,它的作用是在Redisson實(shí)例被關(guān)閉前,不斷的延長(zhǎng)鎖的有效期。
默認(rèn)情況下,看門狗的檢查鎖的超時(shí)時(shí)間是30秒鐘,也可以通過(guò)修改Config.lockWatchdogTimeout來(lái)另行指定。
如果我們未制定 lock 的超時(shí)時(shí)間,就使用 30 秒作為看門狗的默認(rèn)時(shí)間。只要占鎖成功,就會(huì)啟動(dòng)一個(gè)定時(shí)任務(wù):每隔 10
秒重新給鎖設(shè)置過(guò)期的時(shí)間,過(guò)期時(shí)間為 30 秒。
2.2 主從復(fù)制的問(wèn)題
提供了一個(gè)專門的類:RedissonRedLock
,使用了Redlock算法。
Redisson
原理參考 https://blog.csdn.net/agonie201218/article/details/115339670
redisson
操作示例 https://blog.csdn.net/agonie201218/article/details/122084140
redis分布式鎖的坑 https://blog.csdn.net/agonie201218/article/details/121423212
3.Zookeeper 分布式鎖接口方案
zk 分布式鎖,其實(shí)可以做的比較簡(jiǎn)單,就是某個(gè)節(jié)點(diǎn)嘗試創(chuàng)建臨時(shí) znode,此時(shí)創(chuàng)建成功了就獲取了這個(gè)鎖;這個(gè)時(shí)候別的客戶端來(lái)創(chuàng)建鎖會(huì)失敗,只能
注冊(cè)個(gè)監(jiān)聽(tīng)器監(jiān)聽(tīng)這個(gè)鎖。釋放鎖就是刪除這個(gè) znode,一旦釋放掉就會(huì)通知客戶端,然后有一個(gè)等待著的客戶端就可以再次重新加鎖。
參考
java分布式鎖解決方案 redisson or
ZooKeeper https://blog.csdn.net/agonie201218/article/details/122446601
萬(wàn)字總結(jié)Zookeeper客戶端Curator操作Api https://andyoung.blog.csdn.net/article/details/130115913文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-425850.html
項(xiàng)目地址
https://github.com/admin4j/admin4j-framework/tree/master/admin4j-lock文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-425850.html
到了這里,關(guān)于一個(gè)注解解決分布式鎖和接口冪等性,springboot 實(shí)戰(zhàn) 。強(qiáng)到離大譜的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!