基础知识:
MySQL 默认隔离级别 RR(可重复读),依靠MVCC 多版本并发控制避免普通幻读;如果是当前读(select ... for update 锁查询),依靠间隙锁 + 临键锁彻底解决幻读。只有最高隔离级别:串行化 (Serializable) 完全杜绝所有幻读。
- 脏读
- 事务 B:更新 money=500,未 commit
- 事务 A:查询读到 money=500(读到未提交数据)
- 事务 B:执行 rollback 回滚,数据变回 1000
- 问题:A 拿到的 500 是脏数据,完全错误
- 不可重复读(已有行的值被修改)
- 事务 A:第一次查询 money=1000
- 事务 B:更新 money=600,执行 commit 提交
- 事务 A:再次查询,money=600
- 问题:A 同一个事务,两次读同一行结果不一样,不可重复读取
- 幻读(新增 / 删除了行)
- 事务 A:select * from account where money < 2000,查到 1 条数据
- 事务 B:插入一条 money=800 的新数据,commit
- 事务 A:相同条件再次查询,查到 2 条数据
- 问题:A 明明没操作,凭空多出一条数据,称为幻行
四种隔离级别:
1.读未提交(存在脏读、不可重复读、幻读)
2.读已提交(存在不可重复读、幻读)
3.可重复读(存在幻读)
4.串行化(三个都不存在)
mysql innodb默认可重复读,但是大厂为了提升并发性能,常设计为读已提交,由业务层都低处理重复读、幻读问题
MySQL RC 关闭间隙锁,只有记录锁,并发高,但无法靠数据库隔离级别规避上面不可重复读和幻读,全部交给业务层兜底,下面分通用方案 + 完整订单扣库存实战案例。
业务层通用兜底方案(大厂主流):
1.悲观锁方案(for update)
RC 下 select ... for update 加行排他锁,锁定当前行,其他事务阻塞更新/删除,规避不可重复读;
缺点:并发高容易锁等待,适合库存、订单扣减这类短事务。
2.乐观锁(版本号 version / 时间戳)
无锁,靠版本号控制更新,更新时校验版本不一致直接失败重试,互联网最常用。
3.唯一约束防幻读插入
幻读根源是并发插入,给关键字段加唯一索引,重复插入直接抛唯一冲突,捕获异常重试。
4.分布式锁(Redis/Zookeeper)
全局锁住资源,同一商品/订单同一时间只允许一个事务操作,彻底杜绝并发读写冲突。
5.最终一致性兜底(消息队列+对账)
业务操作+事务消息,定时对账补偿,即使并发出现数据不一致,后台定时任务修复。
6.避免长事务
RC 冲突概率和事务时长正相关,所有更新逻辑尽量缩小事务范围。
乐观锁示例:
- 查询当前商品库存、版本号
sql
SELECT stock, version FROM product_stock WHERE product_id = 1001; - 判断库存是否充足,不足直接返回失败;
- 执行扣减,带上版本号做条件更新:
sql
UPDATE product_stock
SET stock = stock - 1, version = version + 1
WHERE product_id = 1001 AND version = #{old_version}; - 判断更新影响行数:
- row > 0:扣减成功,提交事务;
- row = 0:说明中间被其他事务修改(不可重复读导致版本过期),回滚,重试 2~3 次。
解决了什么 - 不可重复读:版本号校验拦截中间更新;
- 库存超卖:多并发只会有一个事务更新成功;
- 幻读插入: product_id 唯一索引,不会重复创建库存行。
数据库唯一索引
插入防幻读
悲观锁示例:
sql
BEGIN;
-- 加行排他锁,锁定该行,其他事务阻塞读写
SELECT stock FROM product_stock WHERE product_id = 1001 FOR UPDATE;
if (stock < 1) {
ROLLBACK;
return "库存不足";
}
UPDATE product_stock SET stock = stock - 1 WHERE product_id = 1001;
COMMIT;
加锁后同一商品同一时间只能一个事务操作,不会出现两次读取库存不一致(不可重复读)。
分布式锁示例:
单纯数据库锁扛不住秒杀流量,Redis分布式锁前置拦截:
java
// 1. 先抢商品分布式锁
String lockKey = "lock:stock:1001";
boolean lock = redisTemplate.tryLock(lockKey, 3, TimeUnit.SECONDS);
if (!lock) return "抢购拥挤,请重试";
try {
// 2. 数据库RC事务扣库存(乐观锁)
boolean success = deductStockWithVersion(1001);
if (!success) throw new Exception("库存变动,重试");
} finally {
// 释放锁
redisTemplate.unlock(lockKey);
}
全局锁保证同一商品同时只有一个业务流程操作,从源头避免并发读写冲突,彻底屏蔽不可重复读、幻读。
无论乐观锁/分布式锁,网络、DB超时都可能出现不一致,大厂必加定时对账任务:
- 订单表已支付订单汇总扣减库存总量;
- 和商品实时库存做差值校验;
- 差值不为0则自动补发库存/生成异常工单人工处理;
这是最终兜底,隔离级别带来的一致性问题全部可以靠对账抹平。