今天实现秒杀优惠券的一人一单功能时,出现了bug,原因是spring的事务没有控制好
源码如下
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
//查询数据库是否有此优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//判断优惠券抢购是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始");
}
//判断优惠券抢购是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束");
}
//判断优惠券库存是否充足
if (voucher.getStock() < 1) {
return Result.fail("优惠券已抢光");
}
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
//创建订单
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
//一人一单
//判断用户是否下过单
Long userId = UserHolder.getUser().getId();
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用户已经下过单");
}
//若库存充足,则stock-1
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();
if (!success) {
return Result.fail("优惠券已抢光");
}
//创建订单并返回订单id
VoucherOrder voucherOrder = new VoucherOrder();
//设置优惠券id
voucherOrder.setVoucherId(voucherId);
//生成订单id
Long orderId = redisIdProducer.produceId("order");
voucherOrder.setId(orderId);
// voucherOrder.setUserId(1L);
voucherOrder.setUserId(UserHolder.getUser().getId());
save(voucherOrder);
return Result.ok(orderId);
seckillVoucher 为controller调用的方法,createVoucherOrder为检验用户并且创建订单的方法,在使用jmeter携带token模拟同一个用户的恶意抢券行为时,发现并不能很好地实现一人一单。
原因在于spring的事务控制问题
@Override
@Transactional
public Result seckillVoucher(Long voucherId)
我在主体方法上加了@Transactional注解,那么事务的范围扩大到了seckillVoucher上。但是要修改数据库,实现事务控制的方法只有createVoucherOrder。而我加锁的范围是
synchronized (userId.toString().intern()) {
//创建订单
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
如果@Transactional加在方法上,那么spring事务提交的范围就在此方法结束后,并且事务的范围为seckillVoucher,即整个业务逻辑上。那么在创建完订单并返回订单id后,锁已经释放,但是此时spring的事务还未提交完成,即mysql中还没有订单数据,单此时其他携带同一token的线程能够获取锁并执行createVoucherOrder方法,导致线程问题。无法实现用户一人一单。
解决方法:将事务的粒度控制在createVoucherOrder上。即将seckillVoucher的事务注解删掉,
现在只有在createVoucherOrder订单事务提交后,锁才会释放。
@Override
// @Transactional
//注意事务控制!!!!!!!!!!!!!
public Result seckillVoucher(Long voucherId) {
//查询数据库是否有此优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
//判断优惠券抢购是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始");
}
//判断优惠券抢购是否结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已经结束");
}
//判断优惠券库存是否充足
if (voucher.getStock() < 1) {
return Result.fail("优惠券已抢光");
}
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
//创建订单
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
//一人一单
//判断用户是否下过单
Long userId = UserHolder.getUser().getId();
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
return Result.fail("用户已经下过单");
}
//若库存充足,则stock-1
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update();
if (!success) {
return Result.fail("优惠券已抢光");
}
//创建订单并返回订单id
VoucherOrder voucherOrder = new VoucherOrder();
//设置优惠券id
voucherOrder.setVoucherId(voucherId);
//生成订单id
Long orderId = redisIdProducer.produceId("order");
voucherOrder.setId(orderId);
// voucherOrder.setUserId(1L);
voucherOrder.setUserId(UserHolder.getUser().getId());
save(voucherOrder);
return Result.ok(orderId);
}