本文已参与「新人创作礼」活动,一起开启掘金创作之路
一个商城项目,用户下单时需要更新商品库存,在商品类增加了version字段,增加乐观锁,保证库存数据的线程安全,但是在多个用户同时下单更新库存时可能会导致库存更新失败,因此需要增加乐观锁失败重试机制
一、相关代码
(1)商品实体类
@Data
@ApiModel(value = "Goods对象", description = "Goods对象")
public class Goods {
@ApiModelProperty(value = "商品名称")
private String name;
@ApiModelProperty(value = "商品标题图")
private String tittleImg;
@ApiModelProperty(value = "商品数量")
private Integer quantity;
@ApiModelProperty(value = "展示价格")
private BigDecimal price;
@ApiModelProperty(value = "商品详情")
private String details;
@ApiModelProperty(value = "商品分类")
private String category;
@ApiModelProperty(value = "版本")
@Version
private Integer version;
}
(2)失败重试注解类
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FailRetry {
//重试次数,这里默认15
int value() default 15;
}
(3)重试切面类
import XXXX.exception.TryAgainException; //类的路径自行修改
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
@Aspect
@Configuration
@Slf4j
public class TryAgainAspect {
/**
* 定义切点
*/
@Pointcut("@annotation(com.ht.store.annotation.FailRetry)")
private void failRetryPointCut() {
}
@Around("failRetryPointCut() && @annotation(failRetry)")
@Transactional(rollbackFor = Exception.class)
public Object retry(ProceedingJoinPoint joinPoint, FailRetry failRetry) throws Throwable {
int count = 0;
do {
count++;
try {
log.info("重试次数:{}", count);
return joinPoint.proceed();
} catch (TryAgainException e) {
if(count >= failRetry.value()){
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw new TryAgainException("重试失败");
}
}
} while (true);
}
}
(3)更新失败异常类
public class TryAgainException extends RuntimeException {
public TryAgainException(String e) {
super(e);
}
}
(4)库存更新方法,注意方法上加上注解
@Override
@FailRetry
@Transactional(rollbackFor = TryAgainException.class)
public boolean updateGoodsQuantity(Long id,Integer number) {
Goods goods = this.getById(id);
goods.setQuantity(goods.getQuantity() - number);
if (this.updateById(goods)){
return true;
}else{
throw new TryAgainException("更新异常,版本号不一致");
}
}
二、执行测试
数据库商品目前库存976,version是23
调用接口更新库存 -1
更新成功了,查看数据库
修改代码,模拟一次更新失败
@Override
@FailRetry
@Transactional(rollbackFor = TryAgainException.class)
public boolean updateGoodsQuantity(Long id,Integer number) {
Goods goods = this.getById(id);
goods.setQuantity(goods.getQuantity() - number);
goods.setVersion(20); //此处修改为老版本,肯定更新失败
if (this.updateById(goods)){
return true;
}else{
throw new TryAgainException("更新异常,版本号不一致");
}
}
更新时version是20,而数据库的version是24,会判定为老版本,表示已经被更新过,会更新失败,触发异常,然后去执行重试机制
抛出TryAgainException异常,数据回滚,由于方法有@FailRetry注解,进入注解绑定的切面类
注解类FailRetry里面默认15次,最多重试15次,结束重试。
一般情况下,每次更新前读取最新的,然后减掉库存进行更新,失败的次数不会太多,可以满足日常使用。