前言
在項目中,經常需要使用Redisson
分布式鎖來保證并發操作的安全性。在未引入基于注解的分布式鎖之前,我們需要手動編寫獲取鎖、判斷鎖、釋放鎖的邏輯,導致代碼重復且冗長。為了簡化這一過程,我們引入了基于注解的分布式鎖,通過一個注解就可以實現獲取鎖、判斷鎖、處理完成后釋放鎖的邏輯。這樣可以大大簡化代碼,提高開發效率。
目標
使用@DistributedLock
即可實現獲取鎖,判斷鎖,處理完成后釋放鎖的邏輯。
@RestController
public class HelloController {@DistributedLock@GetMapping("/helloWorld")public void helloWorld() throws InterruptedException {System.out.println("helloWorld");Thread.sleep(100000);}
}
涉及知識
- SpringBoot
- Spring AOP
- Redisson
- 自定義注解
- 統一異常處理
- SpEL表達式
代碼實現
引入依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.21.3</version>
</dependency>
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
</dependency>
注解類
/*** 分布式鎖注解* @author 只有影子*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {/*** 獲取鎖失敗時,默認的錯誤描述*/String errorDesc() default "任務正在處理中,請耐心等待";/*** SpEL表達式,用于獲取鎖的key* 示例:* "#name"則從方法參數中獲取name的值作為key* "#user.id"則從方法參數中獲取user對象中的id作為key*/String[] keys() default {};/*** key的前綴,為空時取類名+方法名*/String prefix() default "";
}
切面類
/*** 分布式鎖切面類* @author 只有影子*/
@Slf4j
@Aspect
@Component
public class DistributedLockAspect {@Resourceprivate RedissonClient redissonClient;private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();@Around("@annotation(distributedLock)")public Object around(ProceedingJoinPoint joinPoint,DistributedLock distributedLock) throws Throwable {String redisKey = getRedisKey(joinPoint, distributedLock);log.info("拼接后的redisKey為:" + redisKey);RLock lock = redissonClient.getLock(redisKey);if (!lock.tryLock()) {// 可以使用自己的異常類,演示用RuntimeExceptionthrow new RuntimeException(distributedLock.errorDesc());}// 執行被切面的方法try {return joinPoint.proceed();} finally {lock.unlock();}}/*** 動態解密參數,拼接redisKey* @param joinPoint* @param distributedLock 注解* @return*/private String getRedisKey(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) {MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();EvaluationContext context = new MethodBasedEvaluationContext(TypedValue.NULL, method, joinPoint.getArgs(), PARAMETER_NAME_DISCOVERER);StringBuilder redisKey = new StringBuilder();// 拼接redis前綴if (StringUtil.isNotBlank(distributedLock.prefix())) {redisKey.append(distributedLock.prefix()).append(":");} else {// 獲取類名String className = joinPoint.getTarget().getClass().getSimpleName();// 獲取方法名String methodName = joinPoint.getSignature().getName();redisKey.append(className).append(":").append(methodName).append(":");}ExpressionParser parser = new SpelExpressionParser();for (String key : distributedLock.keys()) {// keys是個SpEL表達式Expression expression = parser.parseExpression(key);Object value = expression.getValue(context);redisKey.append(ObjectUtils.nullSafeToString(value));}return redisKey.toString();}
}
統一異常處理類
/*** 全局異常處理類* @author 只有影子*/
@RestControllerAdvice
public class ExceptionHandle {@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public String sendErrorResponseSystem(Exception e) {// 這里只是模擬返回值,實際項目中一般都是返回封裝好的統一返回類return e.getMessage();}
}
還需要將redis配置讀入,這里就不體現
使用示例
1. 無參方法或者需要加方法級的鎖
@DistributedLock
@GetMapping("/helloWorld")
public void helloWorld() throws InterruptedException {System.out.println("helloWorld");Thread.sleep(100000);
}
調用接口:http://localhost:8080/helloWorld
拼接后的redisKey為:HelloController:helloWorld:
可以看到,無參方法的key為HelloController:helloWorld:
,其中HelloController
為類名,helloWorld
為方法名,因為是無參方法,所以沒有接下來的參數。
這時候,再次調用改接口,則不會再進去接口,會被切面類直接攔截,返回如下結果:
在實際生產使用中,這種情況一般被用來在自動任務上標注,因為在集群環境中自動任務同一時間一般只需要啟動一個。
2. 有參數方法,其中key從name中取值
@DistributedLock(keys = "#name")
@GetMapping("/hello1")
public String hello1(String name) throws InterruptedException {String s = "hello " + name;System.out.println(s);Thread.sleep(100000);return s;
}
調用接口為:http://localhost:8080/hello1?name=hurry
拼接后的redisKey為:HelloController:hello1:hurry
這時候,再通過hurry
這個名稱調用時,就不會再處理,而name換為zhangsan
時,則就能正常進入接口。
這時候redis中的key為
> 127.0.0.1@6379 connected!
> keys *
HelloController:hello2:zhangsan
HelloController:hello2:hurry
實際業務中,需要根據不同的參數值進行加鎖的場景。
3. 有參數方法,其中key需要從user對象中獲取name
@DistributedLock(keys = "#user.name")
@GetMapping("/hello2")
public String hello2(User user) throws InterruptedException {String s = "hello " + user.getName();System.out.println(s);Thread.sleep(100000);return s;
}
需要從某個對象中獲取指定屬性作為key的場景
4.有參數方法,其中key從name上取值并指定前綴
@DistributedLock(keys = "#name",prefix = "testPrefix")
@GetMapping("/hello3")
public String hello3(String name) throws InterruptedException {String s = "hello " + name;System.out.println(s);Thread.sleep(100000);return s;
}
需要指定key前綴的場景
最后
由于文章篇幅原因,很多東西沒有深入的講解,但是基于以上代碼基本實現了基于注解的分布式鎖,可以大大提到開發效率。如果還有其他需要拓展的功能,可以通過在注解類增加屬性及在切面類中通過不同的屬性進行不同的處理來實現。