需求描述
- 控制某個service的方法,當有線程調用了該方法後,其他線程不可再調用該方法
- 基於自定義註解實現,使用時只需要在被調用的方法上添加註解標記即可
- redis的鎖鍵(lockKey)通過自定義註解的屬性傳遞進去
- 客戶端如果在當前時間不可執行該方法,可以得到及時的反饋
轉載請備註出處:SpringBoot基於自定義註解簡潔方便地實現Redis分佈式鎖
技能點
- 使用
spring-integration-redis
和spring-boot-starter-integration
實現基於redis的分佈式鎖 - 使用
@Aspect
實現面向切面編程 - 使用
SpEL
進行自定義註解的動態傳值
關鍵代碼
一、添加maven依賴
<!-- 分佈式鎖支持 start-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-redis</artifactId>
</dependency>
<!-- 分佈式鎖支持 end-->
二、配置RedisLockConfiguration
@Configuration
public class RedisLockConfiguration {
@Bean
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) {
return new RedisLockRegistry(redisConnectionFactory, "spring-cloud");
}
@Bean
public RedisTemplate<Object, Object> getRedisTemplate(RedisConnectionFactory jedisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory);
//使用Jackson2JsonRedisSerializer來序列化和反序列化redis的value值(默認使用JDK的序列化方式)
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
template.setValueSerializer(serializer);
//使用StringRedisSerializer來序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
實現自定義註解RedisLock
/**
* 用於標記redis鎖
* created at 2019-07-05 15:15
* @author lerry
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
/**
* 可使用SpEL傳方法參數
* @return
*/
String value() default "";
/**
* redis鎖的key值
* @return
*/
String lockKey() default "";
}
實現切面RedisLockAspect
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.redis.util.RedisLockRegistry;
import org.springframework.stereotype.Component;
/**
* redis分佈式鎖的切面
* created at 2019-07-31 11:05
* @author lerry
*/
@Aspect
@Component
public class RedisLockAspect {
@Autowired
private RedisLockRegistry redisLockRegistry;
@Around(value = "@annotation(redisLock)")
public Object redisLock(ProceedingJoinPoint joinPoint,
RedisLock redisLock) {
Object output = null;
try {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Object[] arguments = joinPoint.getArgs();
// 獲取鎖的key
Object lockKey = parseSpel(redisLock.lockKey(), method, arguments);
Lock lock = redisLockRegistry.obtain(lockKey);
try {
boolean ifLock = lock.tryLock(DEAL_ACTION_LOCK_WAITING_TIME, TimeUnit.SECONDS);
mLog.info("線程[{}]是否獲取到了鎖:{}", Thread.currentThread().getName(), ifLock);
/*
* 可以獲取到鎖,說明當前沒有線程在執行該方法
*/
if (ifLock) {
// 具體執行
output = joinPoint.proceed();
}
else {
arguments[1] = "已有任務正在執行,請勿重複請求";
output = joinPoint.proceed(arguments);
mLog.info("線程[{}]未獲取到鎖,目前鎖詳情信息爲:{}", Thread.currentThread().getName(), lock);
}
}
catch (Exception e) {
mLog.error("執行核心獎勵掃描時出錯:{}", e.getMessage());
}
finally {
mLog.info("嘗試解鎖[{}]", lockKey);
try {
lock.unlock();
mLog.info("[{}]解鎖成功", lockKey);
}
catch (Exception e) {
mLog.error("解鎖dealAction出錯:{}", e.getMessage());
}
}
}
catch (Throwable e) {
mLog.error("aop redis distributed lock error:{}", e.getLocalizedMessage());
}
return output;
}
}
獲取註解中EL表達式的值
/**
* 解析SpEL表達式
* @param key SpEL表達式
* @param method 反射得到的方法
* @param args 反射得到的方法參數
* @return 解析後SpEL表達式對應的值
*/
public static Object parseSpel(String key, Method method, Object[] args) {
// 創建解析器
ExpressionParser parser = new SpelExpressionParser();
// 通過Spring的LocalVariableTableParameterNameDiscoverer獲取方法參數名列表
LocalVariableTableParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
// 構造上下文
EvaluationContext context = new StandardEvaluationContext();
if (args.length == parameterNames.length) {
for (int i = 0, len = args.length; i < len; i++) {
// 使用setVariable方法來註冊自定義變量
context.setVariable(parameterNames[i], args[i]);
}
}
return parser.parseExpression(key).getValue(context);
}
切面的備註說明
@Around(value = "@annotation(redisLock)")
中的參數redisLock應該與增強處理方法中的參數名保持一致- 獲取註解中的
SpEL
表達式的值可以使用parseSpel
方法 DEAL_ACTION_LOCK_WAITING_TIME
是自定義的常量,表示獲取鎖時願意等待的時長,秒爲單位output = joinPoint.proceed(arguments);
可以改變調用執行方法時的方法參數
業務代碼
@RedisLock(lockKey = "T(com.wondersgroup.healthaccount.common.constant.AccountConstant).REDIS_KEY_DEAL_ACTION")
public String dealAction(String scanType, String errorMsg) {
// 對應切面中的 arguments[1] = "已有任務正在執行,請勿重複請求";
if (StringTools.isNotBlank(errorMsg)) {
return errorMsg;
}
// 模擬方法執行耗時
ThreadTools.sleepS(5);
// xxx 具體執行代碼 省略
}
業務模塊的備註說明
T(XX).REDIS_KEY_DEAL_ACTION
是SpEL
表達式中對常量的引用- 當切面中發現已經有線程正在執行業務代碼時,會返回給客戶端"已有任務正在執行,請勿重複請求"的提示
執行結果日誌摘錄
2019-07-31 16:19:49.761 [http-nio-8095-exec-2] - 線程[http-nio-8095-exec-2]是否獲取到了鎖:true
2019-07-31 16:19:51.930 [http-nio-8095-exec-4] - 線程[http-nio-8095-exec-4]是否獲取到了鎖:false
2019-07-31 16:19:51.930 [http-nio-8095-exec-4] - 線程[http-nio-8095-exec-4]未獲取到鎖,目前鎖詳情信息爲:RedisLock [lockKey=health-account:lock:deal-action,lockedAt=2019-07-31@16:19:49.761, clientId=9ea56e92-20a7-4297-b5f1-d5974f8b4a8d]
2019-07-31 16:19:51.930 [http-nio-8095-exec-4] - 嘗試解鎖[lock:deal-action]
2019-07-31 16:19:51.930 [http-nio-8095-exec-4] - 解鎖dealAction出錯:You do not own lock at health-account:lock:deal-action
2019-07-31 16:19:51.931 [http-nio-8095-exec-4] - method [ActionDealService.dealAction()] execution time:1.006s
2019-07-31 16:19:52.834 [http-nio-8095-exec-5] - 線程[http-nio-8095-exec-5]是否獲取到了鎖:false
2019-07-31 16:19:52.834 [http-nio-8095-exec-5] - 線程[http-nio-8095-exec-5]未獲取到鎖,目前鎖詳情信息爲:RedisLock [lockKey=health-account:lock:deal-action,lockedAt=2019-07-31@16:19:49.761, clientId=9ea56e92-20a7-4297-b5f1-d5974f8b4a8d]
2019-07-31 16:19:52.834 [http-nio-8095-exec-5] - 嘗試解鎖[lock:deal-action]
2019-07-31 16:19:52.834 [http-nio-8095-exec-5] - 解鎖dealAction出錯:You do not own lock at health-account:lock:deal-action
2019-07-31 16:19:52.834 [http-nio-8095-exec-5] - method [ActionDealService.dealAction()] execution time:1.005s
2019-07-31 16:19:54.765 [http-nio-8095-exec-2] - -------------------- deal action job start --------------------
2019-07-31 16:19:54.765 [http-nio-8095-exec-2] - -------------------- whole-table --------------------
2019-07-31 16:19:54.775 [http-nio-8095-exec-2] - -------------------- 需要處理的數據條數爲:0--------------------
2019-07-31 16:19:54.776 [http-nio-8095-exec-2] - -------------------- deal action job end --------------------
2019-07-31 16:19:54.776 [http-nio-8095-exec-2] - 嘗試解鎖[lock:deal-action]
2019-07-31 16:19:54.782 [http-nio-8095-exec-2] - [lock:deal-action]解鎖成功
2019-07-31 16:19:54.782 [http-nio-8095-exec-2] - method [ActionDealService.dealAction()] execution time:5.032s
日誌分析
我們可以看到,線程http-nio-8095-exec-2
首先獲取到了鎖,
隨後http-nio-8095-exec-4
和http-nio-8095-exec-5
先後嘗試獲取鎖,結果獲取失敗。
此時顯示當前的鎖信息爲lockedAt=2019-07-31@16:19:49.761
。
最後,http-nio-8095-exec-2
執行完畢,解鎖成功,方法執行完畢。
參考
【Spring-AOP-學習筆記-7】@Around增強處理簡單示例
利用SpringAOP+自定義註解+SpEL實現日誌操作記錄
spring 分佈式鎖-RedisLockRegistry和ZookeeperLockRegistry
spring中的SpEL表達式
Spring Boot 2實現分佈式鎖——這纔是實現分佈式鎖的正確姿勢!