一人一单问题 锁相关
一、最开始的问题(为什么不加锁不行?)
你最开始的代码是这样:
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(锁) {
查订单
扣库存
下单
}
// 方法结束 → 事务才提交!
}
执行顺序:
- 加锁
- 执行下单逻辑
- 释放锁
- 事务提交(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) {
// 一人一单判断
// 扣库存
// 下单
}
执行顺序终于对了!
- 加锁
- 进入
@Transactional方法 - 执行下单逻辑
- 方法结束 → 事务提交!
- 释放锁
完美!
事务提交完,才释放锁!
其他线程绝对查不到未提交的数据,不会重复下单!
五、还有一个 BUG!你代码里有个错误!
java
运行
// 错误!
.eq("stock", 0)
// 正确!
.gt("stock", 0)
你现在写的是:
stock = 0 才能扣减 → 永远扣不成功!
正确逻辑是:
stock > 0 才能扣减(乐观锁,防止超卖)
六、我用最简单的 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);
现在流程就对了:
- 加锁
- 代理对象调用方法
- 事务生效
- 执行下单逻辑
- 事务提交
- 释放锁
最关键的三句话(背会就懂)
- Spring 事务必须通过代理对象调用才能生效
- this. 方法名 () 是真实对象调用,不走代理,事务失效
- AopContext.currentProxy () 拿到代理对象,事务才生效
