自定義注解實(shí)現(xiàn)通用分布式鎖組件。
1 Redisson
Redisson官網(wǎng):https://redisson.org/
1.1介紹
Redisson是一個(gè)基于Redis的工具包,可以幫助開發(fā)人員更輕松地使用Redis,功能非常強(qiáng)大。將JDK中很多常見的隊(duì)列、鎖、對象都基于Redis實(shí)現(xiàn)了對應(yīng)的分布式版本并提供高級的分布式鎖,分布式集合,分布式對象,以及其他的高級Redis功能。
1.2 為什么要使用Redisson實(shí)現(xiàn)分布式鎖
1.2.1 鎖續(xù)期的問題
當(dāng)對業(yè)務(wù)進(jìn)行加鎖時(shí),鎖的過期時(shí)間,絕對不能想當(dāng)然的設(shè)置一個(gè)值。
假設(shè)線程A在執(zhí)行某個(gè)業(yè)務(wù)時(shí)加鎖成功并設(shè)置鎖過期時(shí)間。但該業(yè)務(wù)執(zhí)行時(shí)間過長,業(yè)務(wù)的執(zhí)行時(shí)間超過了鎖過期時(shí)間,那么在業(yè)務(wù)還沒執(zhí)行完時(shí),鎖就自動釋放了。
接著后續(xù)線程就可以獲取到鎖,又來執(zhí)行該業(yè)務(wù)。就會造成線程A還沒執(zhí)行完,后續(xù)線程又來執(zhí)行,導(dǎo)致同一個(gè)業(yè)務(wù)邏輯被重復(fù)執(zhí)行。因此對于鎖的超時(shí)時(shí)間,需要結(jié)合著業(yè)務(wù)執(zhí)行時(shí)間來判斷,讓鎖的過期時(shí)間大于業(yè)務(wù)執(zhí)行時(shí)間。
業(yè)務(wù)執(zhí)行時(shí)間的影響因素太多了,無法確定一個(gè)準(zhǔn)確值,只能是一個(gè)估值。無法百分百保證業(yè)務(wù)執(zhí)行期間,鎖只能被一個(gè)線程占有。
如想保證的話,可以在創(chuàng)建鎖的同時(shí)創(chuàng)建一個(gè)守護(hù)線程,同時(shí)定義一個(gè)定時(shí)任務(wù)每隔一段時(shí)間去為未釋放的鎖增加過期時(shí)間。當(dāng)業(yè)務(wù)執(zhí)行完,釋放鎖后,再關(guān)閉守護(hù)線程。 這種實(shí)現(xiàn)思想可以用來解決鎖續(xù)期。
1.2.2 獲取鎖嘗試的問題
在我們的項(xiàng)目中, 可能會有這樣的情況:
多個(gè)線程競爭獲得鎖, 同一時(shí)刻只有一個(gè)線程獲得到鎖, 其它線程應(yīng)該嘗試獲得鎖。而我們在使用Redis實(shí)現(xiàn)分布式鎖的時(shí)候,獲得不到鎖了,就不再嘗試獲得鎖了,而是直接放棄了。
如果要實(shí)現(xiàn),我們可以采取自旋的方式,同時(shí)設(shè)置一個(gè)超時(shí)時(shí)間。
1.2.3 可重入問題
當(dāng)一個(gè)線程擁有一個(gè)鎖時(shí),它可以重復(fù)獲取該鎖而不會被自己所持有的鎖阻塞??芍厝腈i通常用于高并發(fā)環(huán)境中,以保證線程安全性和避免死鎖的發(fā)生。而我們在使用Redis實(shí)現(xiàn)分布式鎖的時(shí)候,根本沒辦法重入。
像這樣的問題還有很多,如果要實(shí)現(xiàn)一個(gè)生產(chǎn)級別,比較完美的分布式鎖,是個(gè)很耗時(shí)耗力的工作。所以工作里面一般不會自己封裝分布式鎖,如果使用Redis實(shí)現(xiàn)分布式鎖,一般選擇Redisson來實(shí)現(xiàn)。
1.3 Wath Dog的自動延期機(jī)制
剛才提到過,自己實(shí)現(xiàn)的鎖可能存在鎖續(xù)期的問題,但是Redission就提供了一種自動延期機(jī)制解決了這個(gè)問題。
如果拿到分布式鎖的節(jié)點(diǎn)(微服務(wù))宕機(jī),且這個(gè)鎖正好處于鎖住的狀態(tài)時(shí),會出現(xiàn)鎖死的狀態(tài),為了避免這種情況的發(fā)生,鎖都會設(shè)置一個(gè)過期時(shí)間。這樣也存在一個(gè)問題,加入一個(gè)線程拿到了鎖設(shè)置了30s超時(shí),在30s后這個(gè)線程還沒有執(zhí)行完畢,鎖超時(shí)釋放了,就會導(dǎo)致問題,Redisson給出了自己的答案,就是 watch dog 自動延期機(jī)制。
Redisson提供了一個(gè)監(jiān)控鎖的看門狗,它的作用是在Redisson實(shí)例被關(guān)閉前,不斷的延長鎖的有效期,也就是說,如果一個(gè)拿到鎖的線程一直沒有完成邏輯,那么看門狗會幫助線程不斷的延長鎖超時(shí)時(shí)間,鎖不會因?yàn)槌瑫r(shí)而被釋放。
默認(rèn)情況下,看門狗的續(xù)期時(shí)間是30s,也可以通過修改config.lockWatchdogTimeout
來另行指定。
另外Redisson 還提供了可以指定leaseTime參數(shù)的加鎖方法來指定加鎖的時(shí)間。超過這個(gè)時(shí)間后鎖便自動解開了,不會延長鎖的有效期。
- watch dog 在當(dāng)前節(jié)點(diǎn)存活時(shí)每10s給分布式鎖的key續(xù)期 30s;
- watch dog 機(jī)制啟動,且代碼中沒有釋放鎖操作時(shí),watch dog 會不斷的給鎖續(xù)期;
1.4 快速了解
首先引入依賴:
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency>
然后是配置:
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient() {
// 配置類
Config config = new Config();
// 添加redis地址,這里添加了單點(diǎn)的地址,也可以使用config.useClusterServers()添加集群地址
config.useSingleServer()
.setAddress("redis://192.168.150.101:6379")
.setPassword("123456");
// 創(chuàng)建客戶端
return Redisson.create(config);
}
}
最后是基本用法:
@Autowired
private RedissonClient redissonClient;
@Test
void testRedisson() throws InterruptedException {
// 1.獲取鎖對象,指定鎖名稱
RLock lock = redissonClient.getLock("anyLock");
try {
// 2.嘗試獲取鎖,參數(shù):waitTime、leaseTime、時(shí)間單位
boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
if (!isLock) {
// 獲取鎖失敗處理 ..
} else {
// 獲取鎖成功處理
}
} finally {
// 4.釋放鎖
lock.unlock();
}
}
利用Redisson獲取鎖時(shí)可以傳3個(gè)參數(shù):
- waitTime:獲取鎖的等待時(shí)間。當(dāng)獲取鎖失敗后可以多次重試,直到waitTime時(shí)間耗盡。waitTime默認(rèn)-1,即失敗后立刻返回,不重試。
- leaseTime:鎖超時(shí)釋放時(shí)間。默認(rèn)是30,同時(shí)會利用WatchDog來不斷更新超時(shí)時(shí)間。需要注意的是,如果手動設(shè)置leaseTime值,會導(dǎo)致WatchDog失效。
- TimeUnit:時(shí)間單位
1.5 項(xiàng)目集成
關(guān)鍵基礎(chǔ)配置:
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.tianji.common.autoconfigure.redisson.aspect.LockAspect;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@ConditionalOnClass({RedissonClient.class, Redisson.class})
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class RedissonConfig {
private static final String REDIS_PROTOCOL_PREFIX = "redis://";
private static final String REDISS_PROTOCOL_PREFIX = "rediss://";
@Bean
@ConditionalOnMissingBean
public LockAspect lockAspect(RedissonClient redissonClient){
return new LockAspect(redissonClient);
}
@Bean
@ConditionalOnMissingBean
public RedissonClient redissonClient(RedisProperties properties){
log.debug("嘗試初始化RedissonClient");
// 1.讀取Redis配置
RedisProperties.Cluster cluster = properties.getCluster();
RedisProperties.Sentinel sentinel = properties.getSentinel();
String password = properties.getPassword();
int timeout = 3000;
Duration d = properties.getTimeout();
if(d != null){
timeout = Long.valueOf(d.toMillis()).intValue();
}
// 2.設(shè)置Redisson配置
Config config = new Config();
if(cluster != null && !CollectionUtil.isEmpty(cluster.getNodes())){
// 集群模式
config.useClusterServers()
.addNodeAddress(convert(cluster.getNodes()))
.setConnectTimeout(timeout)
.setPassword(password);
}else if(sentinel != null && !StrUtil.isEmpty(sentinel.getMaster())){
// 哨兵模式
config.useSentinelServers()
.setMasterName(sentinel.getMaster())
.addSentinelAddress(convert(sentinel.getNodes()))
.setConnectTimeout(timeout)
.setDatabase(0)
.setPassword(password);
}else{
// 單機(jī)模式
config.useSingleServer()
.setAddress(String.format("redis://%s:%d", properties.getHost(), properties.getPort()))
.setConnectTimeout(timeout)
.setDatabase(0)
.setPassword(password);
}
// 3.創(chuàng)建Redisson客戶端
return Redisson.create(config);
}
private String[] convert(List<String> nodesObject) {
List<String> nodes = new ArrayList<>(nodesObject.size());
for (String node : nodesObject) {
if (!node.startsWith(REDIS_PROTOCOL_PREFIX) && !node.startsWith(REDISS_PROTOCOL_PREFIX)) {
nodes.add(REDIS_PROTOCOL_PREFIX + node);
} else {
nodes.add(node);
}
}
return nodes.toArray(new String[0]);
}
}
幾個(gè)關(guān)鍵點(diǎn):
- 這個(gè)配置上添加了條件注解
@ConditionalOnClass({RedissonClient.class, Redisson.class})
也就是說,只要引用了配置所在模塊,并且引用了Redisson依賴,這套配置就會生效。不引入Redisson依賴,配置自然不會生效,從而實(shí)現(xiàn)按需引入。 - RedissonClient的配置無需自定義Redis地址,而是直接基于SpringBoot中的Redis配置即可。而且不管是Redis單機(jī)、Redis集群、Redis哨兵模式都可以支持
2 定義通用分布式鎖組件
Redisson的分布式鎖使用并不復(fù)雜,基本步驟包括:
- 1)創(chuàng)建鎖對象
- 2)嘗試獲取鎖
- 3)處理業(yè)務(wù)
- 4)釋放鎖
但是,除了第3步以外,其它都是非業(yè)務(wù)代碼,對業(yè)務(wù)的侵入較多:
可以發(fā)現(xiàn),非業(yè)務(wù)代碼格式固定,每次獲取鎖總是在重復(fù)編碼。我們可不可以對這部分代碼進(jìn)行抽取和簡化呢?
2.1 實(shí)現(xiàn)思路分析
要優(yōu)化這部分代碼,需要通過整個(gè)流程來分析:
可以發(fā)現(xiàn),只有紅框部分是業(yè)務(wù)功能,業(yè)務(wù)前、后都是固定的鎖操作。既然如此,我們完全可以基于AOP的思想,將業(yè)務(wù)部分作為切入點(diǎn),將業(yè)務(wù)前后的鎖操作作為環(huán)繞增強(qiáng)。
但是,我們該如何標(biāo)記這些切入點(diǎn)呢?
不是每一個(gè)service方法都需要加鎖,因此我們不能直接基于類來確定切入點(diǎn);另外,需要加鎖的方法可能也較多,我們不能基于方法名作為切入點(diǎn),這樣太麻煩。因此,最好的辦法是把加鎖的方法給標(biāo)記出來,利用標(biāo)記來確定切入點(diǎn)。如何標(biāo)記呢?
最常見的辦法就是基于注解來標(biāo)記了。同時(shí),加鎖時(shí)還有一些參數(shù),比如:鎖的key名稱、鎖的waitTime、releaseTime等等,都可以基于注解來傳參。
因此,注解的核心作用是兩個(gè):
- 標(biāo)記切入點(diǎn)
- 傳遞鎖參數(shù)
綜上,我們計(jì)劃利用注解來標(biāo)記切入點(diǎn),傳遞鎖參數(shù)。同時(shí)利用AOP環(huán)繞增強(qiáng)來實(shí)現(xiàn)加鎖、釋放鎖等操作。
2.2 定義注解
注解本身起到標(biāo)記作用,同時(shí)還要帶上鎖參數(shù):
- 鎖名稱
- 鎖等待時(shí)間
- 鎖超時(shí)時(shí)間
- 時(shí)間單位
- 方法結(jié)束是否釋放鎖
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLock {
/**
* 加鎖key的表達(dá)式,支持SPEL表達(dá)式
*/
String name();
/**
* 阻塞超時(shí)時(shí)長,不指定 waitTime 則按照Redisson默認(rèn)時(shí)長
*/
long waitTime() default 1;
/**
* 鎖自動釋放時(shí)長,默認(rèn)是-1,其實(shí)是30秒 + watchDog模式
*/
long leaseTime() default -1;
/**
* 時(shí)間單位,默認(rèn)為秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 如果設(shè)定了false,則方法結(jié)束不釋放鎖,而是等待leaseTime后自動釋放
*/
boolean autoUnlock() default true;
}
2.3 定義切面
接下來,我們定義一個(gè)環(huán)繞增強(qiáng)的切面,實(shí)現(xiàn)加鎖、釋放鎖:
package com.tianji.promotion.utils;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
@Component
@Aspect
@RequiredArgsConstructor
public class MyLockAspect implements Ordered{
private final RedissonClient redissonClient;
@Around("@annotation(myLock)")
public Object tryLock(ProceedingJoinPoint pjp, MyLock myLock) throws Throwable {
if (!myLock.autoUnlock() && myLock.leaseTime() <= 0) {
// 不手動釋放鎖時(shí),必須指定leaseTime時(shí)間
throw new BizIllegalException("leaseTime不能為空");
}
// 1.創(chuàng)建鎖對象
RLock lock = redissonClient.getLock(myLock.name());
// 2.嘗試獲取鎖
boolean isLock = lock.tryLock(myLock.waitTime(), myLock.leaseTime(), myLock.unit());
// 3.判斷是否成功
if(!isLock) {
// 3.1.失敗,快速結(jié)束
throw new BizIllegalException("請求太頻繁");
}
try {
// 3.2.成功,執(zhí)行業(yè)務(wù)
return pjp.proceed();
} finally {
// 4.釋放鎖
if (myLock.autoUnlock()) {
lock.unlock();
}
}
}
/**
* 指定切面注解的優(yōu)先執(zhí)行順序
* 這里設(shè)置鎖注解要優(yōu)先于其他注解執(zhí)行
* (先加鎖,再執(zhí)行事務(wù))
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
2.4 使用鎖
定義好了鎖注解和切面,接下來使用直接加上注解就行了:
可以看到,業(yè)務(wù)中無需手動編寫加鎖、釋放鎖的邏輯了,沒有任何業(yè)務(wù)侵入,使用起來也非常優(yōu)雅。
不過呢,現(xiàn)在還存在幾個(gè)問題:
- Redisson中鎖的種類有很多,目前的代碼中把鎖的類型寫死了
- Redisson中獲取鎖的邏輯有多種,比如獲取鎖失敗的重試策略,目前都沒有設(shè)置
- 鎖的名稱目前是寫死的,并不能根據(jù)方法參數(shù)動態(tài)變化
所以呢,我們接下來還要對鎖的實(shí)現(xiàn)進(jìn)行優(yōu)化,注意解決上述問題。
2.5.工廠模式切換鎖類型
Redisson中鎖的類型有多種,例如:
因此,我們不能在切面中把鎖的類型寫死,而是交給用戶自己選擇鎖類型。
那么問題來了,如何讓用戶選擇鎖類型呢?
鎖的類型雖然有多種,但類型是有限的幾種,完全可以通過枚舉定義出來。然后把這個(gè)枚舉作為MyLock
注解的參數(shù),交給用戶去選擇自己要用的類型。
而在切面中,我們則需要根據(jù)用戶選擇的鎖類型,創(chuàng)建對應(yīng)的鎖對象即可。但是這個(gè)邏輯不能通過if-else
來實(shí)現(xiàn),太low了。
這里我們的需求是根據(jù)用戶選擇的鎖類型,創(chuàng)建不同的鎖對象。有一種設(shè)計(jì)模式剛好可以解決這個(gè)問題:簡單工廠模式。
2.5.1 鎖類型枚舉
我們首先定義一個(gè)鎖類型枚舉:
public enum MyLockType {
RE_ENTRANT_LOCK, // 可重入鎖
FAIR_LOCK, // 公平鎖
READ_LOCK, // 讀鎖
WRITE_LOCK, // 寫鎖
;
}
然后在自定義注解中添加鎖類型這個(gè)參數(shù):
/**
* 使用的鎖類型,默認(rèn)可重入鎖
* @return
*/
MyLockType lockType() default MyLockType.RE_ENTRANT_LOCK;
2.5.2 鎖對象工廠
然后定義一個(gè)鎖工廠,用于根據(jù)鎖類型創(chuàng)建鎖對象:
import com.xxx.enums.MyLockType;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import java.util.EnumMap;
import java.util.Map;
import java.util.function.Function;
import static com.tianji.promotion.enums.MyLockType.*;
@Component
public class MyLockFactory {
//封裝的是方法引用
private final Map<MyLockType, Function<String, RLock>> lockHandlers;
public MyLockFactory(RedissonClient redissonClient) {
this.lockHandlers = new EnumMap<>(MyLockType.class);
this.lockHandlers.put(RE_ENTRANT_LOCK, redissonClient::getLock);
this.lockHandlers.put(FAIR_LOCK, redissonClient::getFairLock);
this.lockHandlers.put(READ_LOCK, name -> redissonClient.getReadWriteLock(name).readLock());
this.lockHandlers.put(WRITE_LOCK, name -> redissonClient.getReadWriteLock(name).writeLock());
}
public RLock getLock(MyLockType lockType, String name){
//.apply調(diào)用方法引用封裝的方法
return lockHandlers.get(lockType).apply(name);
}
}
說明:
- MyLockFactory內(nèi)部持有了一個(gè)Map,key是鎖類型枚舉,值是創(chuàng)建鎖對象的Function。注意這里不是存鎖對象,因?yàn)殒i對象必須是多例的,不同業(yè)務(wù)用不同鎖對象;同一個(gè)業(yè)務(wù)用相同鎖對象。
- MyLockFactory內(nèi)部的Map采用了
EnumMap
。只有當(dāng)Key是枚舉類型時(shí)可以使用EnumMap
,其底層不是hash表,而是簡單的數(shù)組。由于枚舉項(xiàng)數(shù)量固定,因此這個(gè)數(shù)組長度就等于枚舉項(xiàng)個(gè)數(shù),然后按照枚舉項(xiàng)序號作為角標(biāo)依次存入數(shù)組。這樣就能根據(jù)枚舉項(xiàng)序號作為角標(biāo)快速定位到數(shù)組中的數(shù)據(jù)。
2.5.3 改造切面代碼
我們將鎖對象工廠注入MyLockAspect,然后就可以利用工廠來獲取鎖對象了:
private final MyLockFactory myLockFactory;
RLock lock = myLockFactory.getLock(myLock.lockType(),myLock.name());
此時(shí),在業(yè)務(wù)中,就能通過注解來指定自己要用的鎖類型了:
2.6 鎖失敗策略
多線程爭搶鎖,大部分線程會獲取鎖失敗,而失敗后的處理方案和策略是多種多樣的。目前,我們獲取鎖失敗后就是直接拋出異常,沒有其它策略,這與實(shí)際需求不一定相符。
2.6.1 策略分析
接下來,我們就分析一下鎖失敗的處理策略有哪些。
大的方面來說,獲取鎖失敗要從兩方面來考慮:
- 獲取鎖失敗是否要重試?有三種策略:
- 不重試,對應(yīng)API:lock.tryLock(0, 10, SECONDS),也就是waitTime小于等于0
- 有限次數(shù)重試:對應(yīng)API:lock.tryLock(5, 10, SECONDS),也就是waitTime大于0,重試一定waitTime時(shí)間后結(jié)束
- 無限重試:對應(yīng)API lock.lock(10, SECONDS) , lock就是無限重試
- 重試失敗后怎么處理?有兩種策略:
- 直接結(jié)束
- 拋出異常
對應(yīng)的API和策略名如下:
重試策略 + 失敗策略組合,總共以下幾種情況:
那么該如何用代碼來表示這些失敗策略,并讓用戶自由選擇呢?
相信大家應(yīng)該能想到一種設(shè)計(jì)模式:策略模式。同時(shí),我們還需要定義一個(gè)失敗策略的枚舉。在MyLock注解中定義這個(gè)枚舉類型的參數(shù),供用戶選擇。
注意:
一般的策略模式大概是這樣:
- 定義策略接口
- 定義不同策略實(shí)現(xiàn)類
- 提供策略工廠,便于根據(jù)策略枚舉獲取不同策略實(shí)現(xiàn)
而在策略比較簡單的情況下,我們完全可以用枚舉代替策略工廠,簡化策略模式。
綜上,我們可以定義一個(gè)基于枚舉的策略模式,簡化開發(fā)。
2.6.2 策略實(shí)現(xiàn)
我們定義一個(gè)失敗策略枚舉,直接將失敗策略定義到枚舉中:
package com.xxx.utils;
import com.xxx.common.exceptions.BizIllegalException;//自定義業(yè)務(wù)異常
import org.redisson.api.RLock;
public enum MyLockStrategy {
SKIP_FAST(){
@Override
public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
return lock.tryLock(0, prop.leaseTime(), prop.unit());
}
},
FAIL_FAST(){
@Override
public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
boolean isLock = lock.tryLock(0, prop.leaseTime(), prop.unit());
if (!isLock) {
throw new BizIllegalException("請求太頻繁");
}
return true;
}
},
KEEP_TRYING(){
@Override
public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
lock.lock( prop.leaseTime(), prop.unit());
return true;
}
},
SKIP_AFTER_RETRY_TIMEOUT(){
@Override
public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
return lock.tryLock(prop.waitTime(), prop.leaseTime(), prop.unit());
}
},
FAIL_AFTER_RETRY_TIMEOUT(){
@Override
public boolean tryLock(RLock lock, MyLock prop) throws InterruptedException {
boolean isLock = lock.tryLock(prop.waitTime(), prop.leaseTime(), prop.unit());
if (!isLock) {
throw new BizIllegalException("請求太頻繁");
}
return true;
}
},
;
public abstract boolean tryLock(RLock lock, MyLock prop) throws InterruptedException;
}
然后,在MyLock注解中添加枚舉參數(shù):
/**
* 定義鎖失敗后的策略
* @return
*/
MyLockStrategy lockStrategy() default MyLockStrategy.FAIL_AFTER_RETRY_TIMEOUT;
最后,修改切面代碼,基于用戶選擇的策略來處理:
boolean isLock = myLock.lockStrategy().tryLock(lock, myLock);
最后,修改切面代碼,基于用戶選擇的策略來處理:
這個(gè)時(shí)候,我們就可以在使用鎖的時(shí)候自由選擇鎖類型、鎖策略了:
2.7 基于SPEL的動態(tài)鎖名
現(xiàn)在還剩下最后一個(gè)問題,就是鎖名稱的問題。
在當(dāng)前業(yè)務(wù)中,我們的鎖對象本來應(yīng)該是當(dāng)前登錄用戶,是動態(tài)獲取的。而加鎖是基于注解參數(shù)添加的,在編碼時(shí)就需要指定。怎么辦?
Spring中提供了一種表達(dá)式語法,稱為SPEL表達(dá)式,可以執(zhí)行java代碼,獲取任意參數(shù)。
思路:
我們可以讓用戶指定鎖名稱參數(shù)時(shí)不要寫死,而是基于SPEL表達(dá)式。在創(chuàng)建鎖對象時(shí),解析SPEL表達(dá)式,動態(tài)獲取鎖名稱。
思路很簡單,不過SPEL表達(dá)式的解析還是比較復(fù)雜的。不推薦自己編寫。
2.7.1 SPEL表達(dá)式
SPEL的表達(dá)式語法可以參考官網(wǎng)文檔:https://docs.spring.io/spring-framework/docs/3.0.x/reference/expressions.html
中文文檔:https://itmyhome.com/spring/expressions.html
首先,在使用鎖注解時(shí),鎖名稱可以利用SPEL表達(dá)式,例如我們指定鎖名稱中要包含參數(shù)中的用戶id,則可以這樣寫:
而如果是通過UserContext.getUser()獲取,則可以利用下面的語法:
@MyLock(name="lock:coupon:#{T(com.common.util.UserContext).getUser()}")
這里T(類名).方法名()
就是調(diào)用靜態(tài)方法。
2.7.2 解析SPEL
在切面中,我們需要基于注解中的鎖名稱做動態(tài)解析,而不是直接使用名稱:
其中獲取鎖名稱用的是getLockName()
這個(gè)方法:文章來源:http://www.zghlxwxcb.cn/news/detail-845019.html
/**
* SPEL的正則規(guī)則
*/
private static final Pattern pattern = Pattern.compile("\\#\\{([^\\}]*)\\}");
/**
* 方法參數(shù)解析器
*/
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* 解析鎖名稱
* @param name 原始鎖名稱
* @param pjp 切入點(diǎn)
* @return 解析后的鎖名稱
*/
private String getLockName(String name, ProceedingJoinPoint pjp) {
// 1.判斷是否存在spel表達(dá)式
if (StringUtils.isBlank(name) || !name.contains("#")) {
// 不存在,直接返回
return name;
}
// 2.構(gòu)建context,也就是SPEL表達(dá)式獲取參數(shù)的上下文環(huán)境,這里上下文就是切入點(diǎn)的參數(shù)列表
EvaluationContext context = new MethodBasedEvaluationContext(
TypedValue.NULL, resolveMethod(pjp), pjp.getArgs(), parameterNameDiscoverer);
// 3.構(gòu)建SPEL解析器
ExpressionParser parser = new SpelExpressionParser();
// 4.循環(huán)處理,因?yàn)楸磉_(dá)式中可以包含多個(gè)表達(dá)式
Matcher matcher = pattern.matcher(name);
while (matcher.find()) {
// 4.1.獲取表達(dá)式
String tmp = matcher.group();
String group = matcher.group(1);
// 4.2.這里要判斷表達(dá)式是否以 T字符開頭,這種屬于解析靜態(tài)方法,不走上下文
Expression expression = parser.parseExpression(group.charAt(0) == 'T' ? group : "#" + group);
// 4.3.解析出表達(dá)式對應(yīng)的值
Object value = expression.getValue(context);
// 4.4.用值替換鎖名稱中的SPEL表達(dá)式
name = name.replace(tmp, ObjectUtils.nullSafeToString(value));
}
return name;
}
private Method resolveMethod(ProceedingJoinPoint pjp) {
// 1.獲取方法簽名
MethodSignature signature = (MethodSignature)pjp.getSignature();
// 2.獲取字節(jié)碼
Class<?> clazz = pjp.getTarget().getClass();
// 3.方法名稱
String name = signature.getName();
// 4.方法參數(shù)列表
Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
return tryGetDeclaredMethod(clazz, name, parameterTypes);
}
private Method tryGetDeclaredMethod(Class<?> clazz, String name, Class<?> ... parameterTypes){
try {
// 5.反射獲取方法
return clazz.getDeclaredMethod(name, parameterTypes);
} catch (NoSuchMethodException e) {
Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
// 嘗試從父類尋找
return tryGetDeclaredMethod(superClass, name, parameterTypes);
}
}
return null;
}
2.8 完整代碼
MyLockAspect 經(jīng)過一步步修改與最開始在文章中出現(xiàn)有差異這里給出完整版。文章來源地址http://www.zghlxwxcb.cn/news/detail-845019.html
import com.common.utils.StringUtils;
import com.promotion.anno.MyLock;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.Ordered;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.lang.reflect.Method;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
@Aspect
@RequiredArgsConstructor
public class MyLockAspect implements Ordered {
// private final RedissonClient redissonClient;
private final MyLockFactory myLockFactory;
@Around("@annotation(myLock)")
public Object tryLock(ProceedingJoinPoint pjp, MyLock myLock) throws Throwable {
if (!myLock.autoUnlock() && myLock.leaseTime() <= 0) {
// 不手動釋放鎖時(shí),必須指定leaseTime時(shí)間
throw new BizIllegalException("leaseTime不能為空");
}
// 1.創(chuàng)建鎖對象
//RLock lock = redissonClient.getLock(myLock.name());//獲取可重入鎖
String lockName = getLockName(myLock.name(), pjp);
RLock lock = myLockFactory.getLock(myLock.lockType(),lockName);
// 2.嘗試獲取鎖
// boolean isLock = lock.tryLock(myLock.waitTime(), myLock.leaseTime(), myLock.unit());
//使用策略模式獲取鎖
boolean isLock = myLock.lockStrategy().tryLock(lock, myLock);
// 3.判斷是否成功
if (!isLock) {
// 3.1.失敗,快速結(jié)束(使用策略模式后內(nèi)部會自己拋異常)
return null;
}
try {
// 3.2.成功,執(zhí)行業(yè)務(wù)
return pjp.proceed();
} finally {
// 4.釋放鎖
if (myLock.autoUnlock()) {
lock.unlock();
}
}
}
/**
* 指定切面注解的優(yōu)先執(zhí)行順序
* 這里設(shè)置要高于其他注解
* @return
*/
@Override
public int getOrder() {
return 0;
}
/**
* SPEL的正則規(guī)則
*/
private static final Pattern pattern = Pattern.compile("\\#\\{([^\\}]*)\\}");
/**
* 方法參數(shù)解析器
*/
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* 解析鎖名稱
* @param name 原始鎖名稱
* @param pjp 切入點(diǎn)
* @return 解析后的鎖名稱
*/
private String getLockName(String name, ProceedingJoinPoint pjp) {
// 1.判斷是否存在spel表達(dá)式
if (StringUtils.isBlank(name) || !name.contains("#")) {
// 不存在,直接返回
return name;
}
// 2.構(gòu)建context,也就是SPEL表達(dá)式獲取參數(shù)的上下文環(huán)境,這里上下文就是切入點(diǎn)的參數(shù)列表
EvaluationContext context = new MethodBasedEvaluationContext(
TypedValue.NULL, resolveMethod(pjp), pjp.getArgs(), parameterNameDiscoverer);
// 3.構(gòu)建SPEL解析器
ExpressionParser parser = new SpelExpressionParser();
// 4.循環(huán)處理,因?yàn)楸磉_(dá)式中可以包含多個(gè)表達(dá)式
Matcher matcher = pattern.matcher(name);
while (matcher.find()) {
// 4.1.獲取表達(dá)式
String tmp = matcher.group();
String group = matcher.group(1);
// 4.2.這里要判斷表達(dá)式是否以 T字符開頭,這種屬于解析靜態(tài)方法,不走上下文
Expression expression = parser.parseExpression(group.charAt(0) == 'T' ? group : "#" + group);
// 4.3.解析出表達(dá)式對應(yīng)的值
Object value = expression.getValue(context);
// 4.4.用值替換鎖名稱中的SPEL表達(dá)式
name = name.replace(tmp, ObjectUtils.nullSafeToString(value));
}
return name;
}
private Method resolveMethod(ProceedingJoinPoint pjp) {
// 1.獲取方法簽名
MethodSignature signature = (MethodSignature)pjp.getSignature();
// 2.獲取字節(jié)碼
Class<?> clazz = pjp.getTarget().getClass();
// 3.方法名稱
String name = signature.getName();
// 4.方法參數(shù)列表
Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
return tryGetDeclaredMethod(clazz, name, parameterTypes);
}
private Method tryGetDeclaredMethod(Class<?> clazz, String name, Class<?> ... parameterTypes){
try {
// 5.反射獲取方法
return clazz.getDeclaredMethod(name, parameterTypes);
} catch (NoSuchMethodException e) {
Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
// 嘗試從父類尋找
return tryGetDeclaredMethod(superClass, name, parameterTypes);
}
}
return null;
}
}
到了這里,關(guān)于通用分布式鎖組件的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!