SpringBoot基于自定义注解简洁方便地实现Redis分布式锁

需求描述

  • 控制某个service的方法,当有线程调用了该方法后,其他线程不可再调用该方法
  • 基于自定义注解实现,使用时只需要在被调用的方法上添加注解标记即可
  • redis的锁键(lockKey)通过自定义注解的属性传递进去
  • 客户端如果在当前时间不可执行该方法,可以得到及时的反馈
    转载请备注出处:SpringBoot基于自定义注解简洁方便地实现Redis分布式锁

技能点

  • 使用spring-integration-redisspring-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_ACTIONSpEL表达式中对常量的引用
  • 当切面中发现已经有线程正在执行业务代码时,会返回给客户端"已有任务正在执行,请勿重复请求"的提示

执行结果日志摘录

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-4http-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实现分布式锁——这才是实现分布式锁的正确姿势!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章