需求描述
- 控制某个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实现分布式锁——这才是实现分布式锁的正确姿势!