单体下的 一人一单问题
1 基础版逻辑图

2 上图存在的问题
问题原因:出现这个问题的原因和前面库存为负数数的情况是一样的。
- 线程1查询当前用户是否有订单,当前用户没有订单准备下单,
- 此时线程2也查询当前用户是否有订单,由于线程1还没有完成下单操作,线程2同样发现当前用户未下单,也准备下单,
- 这样明明一个用户只能下一单,结果下了两单,也就出现了超卖问题,导致不符合一人一单
3 解决
方案:悲观锁 / 乐观锁
3.1 乐观锁
- 乐观锁需要判断数据是否修改,而当前是判断当前是否存在,所以无法像解决库存超卖一样使用CAS机制,但是可以使用版本号法,
- 但是版本号法需要新增一个字段。
3.2 悲观锁(单体下)
加锁,用synchronized

@Override
public Result seckillVoucher(Long voucherId) {
// 单机模式下,使用synchronized实现锁
synchronized (userId.toString().intern()) {
// createVoucherOrder的事物不会生效,因为你调用的方法,其实是this.的方式调用的,事务想要生效,
// 还得利用代理来生效,所以这个地方,我们需要获得原始的事务对象, 来操作事务
return voucherOrderService.createVoucherOrder(voucherId);
}
}
@Transactional
public Result createVoucherOrder(Long voucherId) {
// 一人一单逻辑
Long userId = UserHolder.getUser().getId();
int count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();
if (count > 0) {
return Result.fail("你已经抢过优惠券了哦");
}
// 5. 扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0) // 加了CAS 乐观锁,Compare and swap
.update();
if (!success) {
return Result.fail("库存不足");
}
// 库存足且在时间范围内的,则创建新的订单
// 6. 创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 6.1 设置订单id,生成订单的全局id
long orderId = redisIdWorker.nextId("order");
// 6.2 设置用户id
Long id = UserHolder.getUser().getId();
// 6.3 设置代金券id
voucherOrder.setVoucherId(voucherId);
voucherOrder.setId(orderId);
voucherOrder.setUserId(id);
// 7. 将订单数据保存到表中
save(voucherOrder);
// 8. 返回订单id
return Result.ok(orderId);
}
3.2.1 注意点
锁的范围尽量小:
- synchronized尽量锁代码块,而不是方法,
- 锁的范围越大性能越低
锁的对象一定要是一个不变的值:
- 我们不能直接锁 Long 类型的 userId,每请求一次都会创建一个新的 userId 对象,
- synchronized 要锁不变的值,所以我们要将 Long 类型的 userId 通过 toString()方法转成 String 类型的 userId,
- toString()方法底层(可以点击去看源码)是直接 new 一个新的String对象,显然还是在变,
- 所以我们要使用 intern() 方法从常量池中寻找与当前 字符串值一致的字符串对象,这就能够保障一个用户 发送多次请求,每次请求的 userId 都是不变的,从而能够完成锁的效果(并行变串行)
我们要锁住整个事务,而不是锁住事务内部的代码。
如果我们锁住事务内部的代码会导致其它线程能够进入事务,当我们事务还未提交,锁一旦释放,仍然会存在超卖问题
Spring的@Transactional注解要想事务生效,必须使用动态代理。
- Service中一个方法中调用另一个方法,另一个方法使用了事务,此时会导致@Transactional失效,
- 所以我们需要创建一个代理对象,使用代理对象来调用方法。

浙公网安备 33010602011771号