一人一单问题 锁相关

一、最开始的问题(为什么不加锁不行?)

你最开始的代码是这样:

java

运行

public Result createVoucherOrder(Long voucherId) {
    // 查询是否买过
    int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
    if (count > 0) {
        return 已买过;
    }
    // 扣库存
    // 创建订单
}

高并发下会发生什么灾难?

两个线程同时进来

  • 线程 A:查 → 没买过
  • 线程 B:查 → 没买过
  • 两个线程同时通过判断
  • 最后都下单成功一人买了多单!BUG!

所以必须加锁,让同一个用户的请求排队执行

二、第一次改错:给方法加 synchronized(锁太大了!)

java

运行

@Transactional
public synchronized Result createVoucherOrder(Long voucherId) {

错在哪?

锁的是整个对象、整个方法!

  • 用户 1 来下单 → 锁住整个方法
  • 用户 2、用户 3、用户 1000 全都要等!
  • 秒杀直接变龟速

我们要的是:

同一个用户排队,不同用户互不影响!

所以不能锁整个方法!

三、第二次改错:锁 userId(锁粒度变小,但事务出问题)

java

运行

synchronized (userId.toString().intern()) {

为什么要 intern ()?(超级关键)

  • userId.toString() 每次都是新对象
  • 不同线程,哪怕 userId 一样,锁对象不一样!
  • 锁不住!

intern() → 去字符串常量池拿,保证同一个 userId 用同一把锁!

但这版本还是错!因为 事务没提交,锁就放了

java

运行

@Transactional
public Result createVoucherOrder(...) {
    synchronized(锁) {
        查订单
        扣库存
        下单
    }
    // 方法结束 → 事务才提交!
}

执行顺序:

  1. 加锁
  2. 执行下单逻辑
  3. 释放锁
  4. 事务提交(Spring 事务在方法结束才提交)

问题:

锁释放了 → 但数据库订单还没保存!

第二个线程进来 → 查不到订单 → 又下单!

还是一人多单!

这是全段最难的坑

四、最终正确方案:锁包着事务(先加锁 → 再执行事务 → 事务提交 → 再释放锁)

java

运行

@Override
public Result seckillVoucher(Long voucherId) {
    // ... 秒杀时间、库存判断

    Long userId = UserHolder.getUser().getId();

    // 锁 放 在 事 务 外 面!
    synchronized (userId.toString().intern()) {
        // 调用带有 @Transactional 的方法
        return createVoucherOrder(voucherId);
    }
}

@Transactional
public Result createVoucherOrder(Long voucherId) {
    // 一人一单判断
    // 扣库存
    // 下单
}

执行顺序终于对了!

  1. 加锁
  2. 进入 @Transactional 方法
  3. 执行下单逻辑
  4. 方法结束 → 事务提交!
  5. 释放锁

完美!

事务提交完,才释放锁!

其他线程绝对查不到未提交的数据,不会重复下单!

五、还有一个 BUG!你代码里有个错误!

java

运行

// 错误!
.eq("stock", 0)

// 正确!
.gt("stock", 0)

你现在写的是:

stock = 0 才能扣减 → 永远扣不成功!

正确逻辑是:

stock > 0 才能扣减(乐观锁,防止超卖)

六、我用最简单的 3 句话总结整个逻辑

  1. 高并发下,不加锁 → 一人多单
  2. 锁方法 → 锁太大,性能极差
  3. 锁粒度太小 + 事务内释放锁 → 事务未提交,依旧重复下单

最终正确方案:

锁放在事务外面

保证:

先加锁 → 执行事务 → 事务提交 → 释放锁

意思:

不要用 this(自己)调用事务方法,

要用 Spring 生成的【代理对象】来调用,

否则 @Transactional 事务会失效!

一步一步讲,你马上懂

1. 先搞懂:Spring 事务是怎么生效的?

Spring 事务不是靠魔法生效的!

它是靠【代理对象】生效的!

你写的:

java

运行

@Transactional
public void a(){}

Spring 会偷偷给你生成一个代理类

plaintext

代理对象.a() {
    开启事务
    执行你的代码
    提交/回滚事务
}

只有通过【代理对象】调用方法,事务才会生效!

直接用 this 调用,事务直接失效!

2. 你原来的代码错在哪?(this 调用)

你之前写的:

java

运行

synchronized (userId.toString().intern()) {
    // this 调用!
    return this.createVoucherOrder(voucherId);
}

这里的 this当前真实对象,不是代理对象!

Spring 事务直接失效!

→ 不会开启事务!

→ 不会回滚!

→ 数据会出错!

画个图你就懂

plaintext

真实对象(this) → 调用方法 → 无事务
代理对象(proxy) → 调用方法 → 有事务

3. 正确做法:拿到代理对象来调用

所以必须写:

java

运行

// 拿到 Spring 代理对象
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();

// 用代理对象调用事务方法 → 事务生效!
return proxy.createVoucherOrder(voucherId);

现在流程就对了:

  1. 加锁
  2. 代理对象调用方法
  3. 事务生效
  4. 执行下单逻辑
  5. 事务提交
  6. 释放锁

最关键的三句话(背会就懂)

  1. Spring 事务必须通过代理对象调用才能生效
  2. this. 方法名 () 是真实对象调用,不走代理,事务失效
  3. AopContext.currentProxy () 拿到代理对象,事务才生效
全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务