基于redisson的可动态定制切点的串行化请求切面

文章来源原创   作者:临窗旋墨   发布时间:2024-02-04   阅读:1525   标签:springboot 分类:我的编码 专题:编码随手记

基于redisson的可动态定制切点的串行化请求切面

应用场景

  1. 某些方法/请求需要串行化执行。比如针对某个资源(如用户),不能被同时编辑
  2. 支持动态设置切点
  3. 支持锁住某个方法,以及方法中的某个参数

编码实现

自定义注解

方便要串行化处理的地方传入一些定制化参数

  1. /**
  2. * 描述: 串行化请求注解
  3. * @author Vic.xu
  4. * @since 2024-02-02 10:53
  5. */
  6. @Retention(RetentionPolicy.RUNTIME)
  7. @Target(ElementType.METHOD)
  8. @Documented
  9. @Inherited
  10. public @interface SerialRequest {
  11. /**
  12. * 锁名称(前缀:如表名/业务实体名)
  13. */
  14. String name();
  15. /**
  16. * 锁住的参数:对应方法中的参数,通过spel表达式取值
  17. * 如:
  18. * 获取参数id: #id
  19. * 获取对象参数user中的id: #user.id
  20. * 获取map参数userMap中的id: #userMap['id']
  21. * ★ 不写此参数会锁住整个方法
  22. */
  23. String lockParameter() default "";
  24. /**
  25. * 获取锁的等待时间(秒) 默认10s, 10s后未能获取到锁,则直接抛出异常,
  26. */
  27. long waitTimeSeconds() default 10L;
  28. }

切面处理类 SerialRequestAdvice

  1. 此切面可以看成是在方法开始前加锁,方法结束后解锁,所以是一个环绕通知,所以实现org.aopalliance.intercept.MethodInterceptor接口。
    • 前置通知:org.springframework.aop.MethodBeforeAdvice
    • 后置通知:org.springframework.aop.AfterReturningAdvice
    • 异常通知:org.springframework.aop.ThrowsAdvice
  2. 通过自定义注解,以及spring的SPEL获取方法参数中对应参数的值

    • 注意此处需要获取编译后的方法参数名称,需要需要注意配置maven-compiler-plugin 开启此特性(springboot中应该默认启用了此特性)

      1. <plugin>
      2. <groupId>org.apache.maven.plugins</groupId>
      3. <artifactId>maven-compiler-plugin</artifactId>
      4. <configuration>
      5. <compilerArgs>
      6. <arg>-parameters</arg>
      7. </compilerArgs>
      8. </configuration>
      9. </plugin>
  3. 此处加锁不设置加锁时间,只设置获取锁的时间(从注解中获取),获取锁超时,则告知,当前资源正在被其他人处理。加锁后,默认使用redssion的看门狗机制延长锁的生命周期,直到业务处理结束后才释放锁。
  4. 代码参见:SerialRequestAdvice
  1. /**
  2. * 描述: 通过分布式锁串行化请求切面,根据请求中的业务标识添加锁
  3. * 如操作用户时候,针对同一个用户id,同一时刻只能被一个请求操作
  4. * @author Vic.xu
  5. * @since 2024-02-02 10:28
  6. */
  7. public class SerialRequestAdvice implements MethodInterceptor {
  8. private static final Logger LOGGER = LoggerFactory.getLogger(SerialRequestAdvice.class);
  9. private final SpelExpressionParser spelParser = new SpelExpressionParser();
  10. private final RedissonClient redissonClient;
  11. public SerialRequestAdvice(RedissonClient redissonClient) {
  12. this.redissonClient = redissonClient;
  13. }
  14. /**
  15. * 串行化切面的处理:加锁和释放锁
  16. * 通过redisson锁住资源,直到业务处理完毕后才释放
  17. */
  18. @Override
  19. public Object invoke(MethodInvocation invocation) throws Throwable {
  20. SerialInfo serialInfo = initSerialInfo(invocation);
  21. String lockName = serialInfo.lockName;
  22. RLock lock = redissonClient.getLock(lockName);
  23. try {
  24. //此处不设置加锁时间, 通过看门狗默认一直延期锁的生命周期,直到业务流程处理完毕
  25. boolean obtained = lock.tryLock(serialInfo.waitTime, TimeUnit.SECONDS);
  26. if (!obtained) {
  27. throw new RuntimeException("当前资源[" + lockName + "]正在被其他人操作,请稍后再试!");
  28. }
  29. LOGGER.info("SerialRequest 锁定 {}", lockName);
  30. return invocation.proceed();
  31. } finally {
  32. if (lock != null && lock.isHeldByCurrentThread()) {
  33. lock.unlock();
  34. LOGGER.info("SerialRequest 释放 {}", lockName);
  35. }
  36. }
  37. }
  38. /**
  39. * 初始化加锁的相关信息
  40. */
  41. private SerialInfo initSerialInfo(MethodInvocation invocation) {
  42. Method method = invocation.getMethod();
  43. SerialRequest serialRequest = method.getAnnotation(SerialRequest.class);
  44. String lockParameter = serialRequest.lockParameter();
  45. Parameter[] parameters = method.getParameters();
  46. String lockParameterValue = null;
  47. if (!ObjectUtils.isEmpty(parameters) && StringUtils.hasLength(lockParameter)) {
  48. //将方法的参数名和参数值一一对应的放入上下文中
  49. EvaluationContext ctx = new StandardEvaluationContext();
  50. Object[] arguments = invocation.getArguments();
  51. for (int i = 0; i < parameters.length; i++) {
  52. String name = parameters[i].getName();
  53. Object argument = arguments[i];
  54. ctx.setVariable(name, argument);
  55. }
  56. lockParameterValue = String.valueOf(spelParser.parseExpression(lockParameter).getValue(ctx));
  57. }
  58. return new SerialInfo(serialRequest, lockParameterValue);
  59. }
  60. /**
  61. * 串行化的相关数据
  62. */
  63. static class SerialInfo {
  64. String lockName;
  65. long waitTime;
  66. SerialInfo(SerialRequest serialRequest, String lockParameterValue) {
  67. if (!StringUtils.hasLength(lockParameterValue)) {
  68. lockParameterValue = "none";
  69. }
  70. this.lockName = "sr:" + serialRequest.name() + ":" + lockParameterValue;
  71. this.waitTime = serialRequest.waitTimeSeconds();
  72. }
  73. }
  74. }

串行化请求切面 SerialRequestAdvisor

  1. 定义串行化请求的Advisor:
    • 告知spring对哪里进行切面处理(也即通知切入点Pointcut),被方法中的切点为:自定义切面(可为空) + SerialRequest注解的方法
    • 告知切面的处理逻辑:也即·SerialRequestAdvice
  2. 如何使用 SerialRequestAdvisor
    • 通过@Bean把SerialRequestAdvisor注册到spring容器,并传入切点与redissonClient
      • 在需要串行化处理的方法上加上SerialRequest注解
  3. 代码:SerialRequestAdvisor
  1. /**
  2. * 描述: 串行化请求切面
  3. * 使用方法:
  4. * 1. 通过@Bean把SerialRequestAdvisor注册到spring容器,并传入切点与redissonClient
  5. * 2. 在需要串行化处理的方法上加上SerialRequest注解
  6. * @see SerialRequest
  7. * @author Vic.xu
  8. * @since 2024-02-02 11:15
  9. */
  10. public class SerialRequestAdvisor extends AbstractPointcutAdvisor {
  11. private static final Logger LOGGER = LoggerFactory.getLogger(SerialRequestAdvisor.class);
  12. private static final long serialVersionUID = 1L;
  13. /**
  14. * 切入点表达式
  15. */
  16. private final String pointcutExpression;
  17. private final RedissonClient redissonClient;
  18. private Pointcut serialRequestPointcut;
  19. private SerialRequestAdvice serialRequestAdvice;
  20. public SerialRequestAdvisor(@Nullable String pointcutExpression,
  21. @NotNull RedissonClient redissonClient) {
  22. LOGGER.info("SerialRequestAdvisor initialization!");
  23. this.pointcutExpression = pointcutExpression;
  24. this.redissonClient = redissonClient;
  25. initPointcut();
  26. initAdvice();
  27. }
  28. @Override
  29. public Pointcut getPointcut() {
  30. return serialRequestPointcut;
  31. }
  32. @Override
  33. public Advice getAdvice() {
  34. return serialRequestAdvice;
  35. }
  36. /**
  37. * 初始化 串行化请求的通知 切入点:
  38. * 如果没有自定义切入点,则只需要 SerialRequest注解
  39. */
  40. public void initPointcut() {
  41. AnnotationMatchingPointcut annotationMethodMatcher = AnnotationMatchingPointcut.forMethodAnnotation(SerialRequest.class);
  42. if (!StringUtils.hasText(pointcutExpression)) {
  43. this.serialRequestPointcut = annotationMethodMatcher;
  44. return;
  45. }
  46. AspectJExpressionPointcut expressionPointcut = new AspectJExpressionPointcut();
  47. expressionPointcut.setExpression(pointcutExpression);
  48. //复合切入点
  49. ComposablePointcut pointcut = new ComposablePointcut();
  50. pointcut.intersection(annotationMethodMatcher).intersection(pointcut);
  51. this.serialRequestPointcut = pointcut;
  52. }
  53. /**
  54. * 初始化串行话处理逻辑
  55. */
  56. public void initAdvice() {
  57. this.serialRequestAdvice = new SerialRequestAdvice(redissonClient);
  58. }
  59. }

单元测试

相关依赖

  1. <!-- AOP -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-aop</artifactId>
  5. </dependency>
  6. <!--redisson
  7. 注意官网的版本提示: https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter
  8. redisson-spring-data Spring Boot
  9. redisson-spring-data-18 1.5.y
  10. redisson-spring-data-2x 2.x.y
  11. redisson-spring-data-3x 3.x.y
  12. -->
  13. <dependency>
  14. <groupId>org.redisson</groupId>
  15. <artifactId>redisson-spring-boot-starter</artifactId>
  16. <version>2.15.2</version>
  17. </dependency>

参考


发表评论

目录