基于自定义注解的Redisson分布式锁实现
前言
在项目中,经常需要使用Redisson分布式锁来保证并发操作的安全性。在未引入基于注解的分布式锁之前,我们需要手动编写获取锁、判断锁、释放锁的逻辑,导致代码重复且冗长。为了简化这一过程,我们引入了基于注解的分布式锁,通过一个注解就可以实现获取锁、判断锁、处理完成后释放锁的逻辑。这样可以大大简化代码,提高开发效率。
目标
使用@DistributedLock即可实现获取锁,判断锁,处理完成后释放锁的逻辑。
▼java复制代码@RestController public class HelloController { @DistributedLock @GetMapping("/helloWorld") public void helloWorld() throws InterruptedException { System.out.println("helloWorld"); Thread.sleep(100000); } }
涉及知识
- SpringBoot
- Spring AOP
- Redisson
- 自定义注解
- 统一异常处理
- SpEL表达式
代码实现
引入依赖
▼xml复制代码<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>
注解类
▼java复制代码/** * 分布式锁注解 * @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 ""; }
切面类
▼java复制代码/** * 分布式锁切面类 * @author 只有影子 */ @Slf4j @Aspect @Component public class DistributedLockAspect { @Resource private 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()) { // 可以使用自己的异常类,演示用RuntimeException throw 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(); } }
统一异常处理类
▼java复制代码/** * 全局异常处理类 * @author 只有影子 */ @RestControllerAdvice public class ExceptionHandle { @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public String sendErrorResponseSystem(Exception e) { // 这里只是模拟返回值,实际项目中一般都是返回封装好的统一返回类 return e.getMessage(); } }
还需要将redis配置读入,这里就不体现
使用示例
1. 无参方法或者需要加方法级的锁
▼java复制代码@DistributedLock @GetMapping("/helloWorld") public void helloWorld() throws InterruptedException { System.out.println("helloWorld"); Thread.sleep(100000); }
调用接口:http://localhost:8080/helloWorld
▼text复制代码拼接后的redisKey为:HelloController:helloWorld:
可以看到,无参方法的key为HelloController:helloWorld:,其中HelloController为类名,helloWorld为方法名,因为是无参方法,所以没有接下来的参数。
这时候,再次调用改接口,则不会再进去接口,会被切面类直接拦截,返回如下结果:

在实际生产使用中,这种情况一般被用来在自动任务上标注,因为在集群环境中自动任务同一时间一般只需要启动一个。
2. 有参数方法,其中key从name中取值
▼java复制代码@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
▼text复制代码拼接后的redisKey为:HelloController:hello1:hurry
这时候,再通过hurry这个名称调用时,就不会再处理,而name换为zhangsan时,则就能正常进入接口。
这时候redis中的key为
▼bash复制代码> 127.0.0.1@6379 connected! > keys * HelloController:hello2:zhangsan HelloController:hello2:hurry
实际业务中,需要根据不同的参数值进行加锁的场景。
3. 有参数方法,其中key需要从user对象中获取name
▼java复制代码@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上取值并指定前缀
▼java复制代码@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前缀的场景
最后
由于文章篇幅原因,很多东西没有深入的讲解,但是基于以上代码基本实现了基于注解的分布式锁,可以大大提到开发效率。如果还有其他需要拓展的功能,可以通过在注解类增加属性及在切面类中通过不同的属性进行不同的处理来实现。
评论
问答助学
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
作者分享
工作快5年的总结(关于调薪,跳槽和转型大模型应用开发)
48
1秒将本地SpringBoot项目jar包部署到Linux环境
10
基于自定义注解的Redisson分布式锁实现
17
#技术# #Java# linux
分享一篇自己写的广受好评的文章
关于根据自己所掌握的技术(Linux、SpringBoot、Maven、IDEA插件),融会贯通、逐步迭代的SpringBoot项目快速部署到Linux环境方式。
迭代步骤:
1. 在windows环境使用命令启动SpringBoot项目
2. 在Linux环境使用命令启动项目
3. 在Linux环境使用脚本启动项目
4. 优化脚本,每次启动时先关闭旧项目
5. 分离依赖,每次上传Linux环境只上传几百k的代码包
6. 搭配IDEA插件,实现在IDEA中一键秒级部署启动
文章在线地址:http://t.csdn.cn/Ggbg9
如果文章对你有帮助,请帮忙点个赞,谢谢!
32
分享新写的文章,在linux环境部署SpringBoot项目,一步一步优化部署方式。
从上传完整jar包,使用命令;到使用脚本启动;再到分离依赖jar,秒级上传;最后搭配插件,实现一键秒级部署
https://blog.csdn.net/weixin_43811294/article/details/128810233
8
