单体下的 一人一单问题

1 基础版逻辑图

image

2 上图存在的问题

问题原因:出现这个问题的原因和前面库存为负数数的情况是一样的。

  • 线程1查询当前用户是否有订单,当前用户没有订单准备下单,
  • 此时线程2也查询当前用户是否有订单,由于线程1还没有完成下单操作,线程2同样发现当前用户未下单,也准备下单,
  • 这样明明一个用户只能下一单,结果下了两单,也就出现了超卖问题,导致不符合一人一单

3 解决

方案:悲观锁 / 乐观锁

3.1 乐观锁

  • 乐观锁需要判断数据是否修改,而当前是判断当前是否存在,所以无法像解决库存超卖一样使用CAS机制,但是可以使用版本号法,
  • 但是版本号法需要新增一个字段。

3.2 悲观锁(单体下)

加锁,用synchronized
image

@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尽量锁代码块,而不是方法,
  • 锁的范围越大性能越低

锁的对象一定要是一个不变的值:

  1. 我们不能直接锁 Long 类型的 userId,每请求一次都会创建一个新的 userId 对象,
  2. synchronized 要锁不变的值,所以我们要将 Long 类型的 userId 通过 toString()方法转成 String 类型的 userId,
  3. toString()方法底层(可以点击去看源码)是直接 new 一个新的String对象,显然还是在变,
  4. 所以我们要使用 intern() 方法从常量池中寻找与当前 字符串值一致的字符串对象,这就能够保障一个用户 发送多次请求,每次请求的 userId 都是不变的,从而能够完成锁的效果(并行变串行)

我们要锁住整个事务,而不是锁住事务内部的代码。

如果我们锁住事务内部的代码会导致其它线程能够进入事务,当我们事务还未提交,锁一旦释放,仍然会存在超卖问题

Spring的@Transactional注解要想事务生效,必须使用动态代理。

  • Service中一个方法中调用另一个方法,另一个方法使用了事务,此时会导致@Transactional失效,
  • 所以我们需要创建一个代理对象,使用代理对象来调用方法。
posted @ 2025-04-10 16:21  kuki'  阅读(45)  评论(0)    收藏  举报