Java-乐观锁中的重试机制
当一条记录存在并发修改的情况时,为了防止修改丢失,需要加锁来解决问题。在这里我们讨论乐观锁的用法。
在阿里巴巴Java手册中有这么一段话: 【强制】并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version作为更新依据。说明:如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3次。
-
在表中新增version字段
version int
-
在实体类中增加字段映射
private Integer version;
-
更新时判断当前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中的乐观锁插件(可用可不用)
-
同上,需要在数据表中增加字段。
-
在实体类中增加字段映射
/* * 标识该子弹是乐观锁版本字段 */ @Version private Integer version;
-
增加配置文件
@Configuration public class MybatisPlusAutoConfiguration { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }
-
使用Mybatis-plus自带的修改方法会自动触发乐观锁机制。
三. 重试机制
使用乐观锁后,自然会导致有的请求修改失败;这时就需要使用重试机制。 这里浅谈两种实现方式:
使用自定义注解。
使用Spring-retry重试框架。
使用自定义注解
使用aop来解决问题,大体思路如下:
定义自定义注解
实现切面逻辑
新增自定义异常,在更新失败时抛出
-
定义自定义注解。 @Target标识声明来什么位置触发注解。 @Retention(RetentionPolicy.RUNTIME),表示注解的信息被保留在class文件
/** * 重试注解 * @author Lvdeyin */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface NeedTryAgain { /** * 重试次数 * @return */ int tryTimes() default 3; }
-
定义切面
@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); } }
-
定义自定义异常
/** * 自定义重试异常 * @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; } }
-
业务方法中增加注解,以下为测试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); } }
-
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框架
-
引入依赖
<!--Spring-retry--> <dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency>
-
在业务方法上使用注解
/** * @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))
本文来自博客园,作者:EchoLv,转载请注明原文链接:https://www.cnblogs.com/lvdeyinBlog/p/16528879.html