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實現分佈式鎖——這纔是實現分佈式鎖的正確姿勢!

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