Java-乐观锁中的重试机制

使用场景

当一条记录存在并发修改的情况时,为了防止修改丢失,需要加锁来解决问题。在这里我们讨论乐观锁的用法。

在阿里巴巴Java手册中有这么一段话: 【强制】并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version作为更新依据。说明:如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3次。

一. 乐观锁的使用

  1. 在表中新增version字段

    version int
  2. 在实体类中增加字段映射

    private Integer version;
  3. 更新时判断当前version是否一样,不一样则无法修改成功

    update product_info
    set product_stock = product_stock - 1, version = #{product.version} + 1
    where product_id = #{product.productId}
    and (product_stock - 1) >= 0
    and version = #{product.version}

二. 使用Mybatis-plus中的乐观锁插件(可用可不用)

  1. 同上,需要在数据表中增加字段。

  2. 在实体类中增加字段映射

    /*
    * 标识该子弹是乐观锁版本字段
    */
    @Version
    private Integer version;
  3. 增加配置文件

    @Configuration
    public class MybatisPlusAutoConfiguration {
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
            return interceptor;
        }
    }
  4. 使用Mybatis-plus自带的修改方法会自动触发乐观锁机制。

三. 重试机制

使用乐观锁后,自然会导致有的请求修改失败;这时就需要使用重试机制。 这里浅谈两种实现方式:

  1. 使用自定义注解。

  2. 使用Spring-retry重试框架。

使用自定义注解

使用aop来解决问题,大体思路如下:

  1. 定义自定义注解

  2. 实现切面逻辑

  3. 新增自定义异常,在更新失败时抛出

  1. 定义自定义注解。 @Target标识声明来什么位置触发注解。 @Retention(RetentionPolicy.RUNTIME),表示注解的信息被保留在class文件

    /**
     * 重试注解
     * @author Lvdeyin
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface NeedTryAgain {
    ​
        /**
         * 重试次数
         * @return
         */
        int tryTimes() default 3;
    }
  2. 定义切面

    @Aspect
    @Component
    @Slf4j
    public class TryAgainAspect {
    ​
        /**
         * 定义切入点
         */
        @Pointcut("@annotation(com.example.designstudy.myannotation.NeedTryAgain)")
        public void retryOnOptFailure(){
    ​
        }
    ​
        /**
         * 定义通知 Around环绕通知
         */
        @Around("retryOnOptFailure() && @annotation(needTryAgain)")
        @Transactional(rollbackFor = Exception.class)
        public Object doConcurrentOperation(ProceedingJoinPoint joinPoint, NeedTryAgain needTryAgain) throws Throwable {
            int count = 0;
            do {
                count++;
                try {
                    //执行方法
                    return joinPoint.proceed();
                } catch (RetryException throwable) {
                    if (count > needTryAgain.tryTimes()){
                        //抛出异常
                        throw new RuntimeException("服务器繁忙,稍后再试");
                    }else{
                        //重试
                        log.info("正在重试>>>{}", count);
                    }
                }
            }while (true);
        }
    }
  3. 定义自定义异常

    /**
     * 自定义重试异常
     * @author Lv de yin
     * @date 2022/7/27 15:59
     */
    public class RetryException extends RuntimeException{
    ​
        private Integer status = 001;
    ​
        public RetryException(String message) {
            super(message);
        }
    ​
        public RetryException(String message, Integer status){
            super(message);
            this.status = status;
        }
    }
  4. 业务方法中增加注解,以下为测试demo

    /**
     * 使用乐观锁修改商品库存测试
     * @author Lv de yin
     * @date 2022/7/18 10:18
     */
    @NeedTryAgain(tryTimes = 3)
    @GetMapping
    public Object test1(){
        ProductInfo productInfo = productInfoMapper.selectById(1);
        if (productInfo.getProductStock() <= 0){
            log.error("库存不足");
            //抛出一个业务异常
        }
        //扣减库存
        Integer result = productInfoMapper.deductionStock(productInfo);
        if (result > 0){
            //新增订单记录
            OrderInfo orderInfo = new OrderInfo();
            orderInfo.setProductId(Integer.valueOf(productInfo.getProductId()));
            orderInfo.setCreateTime(LocalDateTime.now());
            orderInfo.setStock(1);
            orderInfoMapper.insert(orderInfo);
            return "修改成功";
        }else{
            throw new RetryException("修改失败重试", 100);
        }
    }
  5. mapper文件

    <update id="deductionStock" parameterType="com.example.designstudy.entity.ProductInfo">
        update product_info
        set product_stock = product_stock - 1, version = #{product.version} + 1
        where product_id = #{product.productId}
        and (product_stock - 1) >= 0
        and version = #{product.version}
    </update>

使用Spring-Retry框架

  1. 引入依赖

    <!--Spring-retry-->
    <dependency>
        <groupId>org.springframework.retry</groupId>
        <artifactId>spring-retry</artifactId>
    </dependency>
  2. 在业务方法上使用注解

    /**
     * @Retryable的参数说明: 
     * •value:抛出指定异常才会重试
     * •include:和value一样,默认为空,当exclude也为空时,默认所以异常
     * •exclude:指定不处理的异常
     * •maxAttempts:最大重试次数,默认3次
     * •backoff:重试等待策略,默认使用@Backoff,@Backoff的value默认为1000L,我们设置为2000L;
     * multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试
     */
    @Retryable(value = RetryException.class, maxAttempts = 5, backoff = @Backoff(delay = 2000L, multiplier = 1))

    只是简单使用,若需深入了解,还请查看文档。

posted @ 2022-07-28 15:46  EchoLv  阅读(2179)  评论(0编辑  收藏  举报