很多人一提到事务隔离级别、锁、MVCC、死锁,就觉得头大,满脑子都是一堆专业术语,不知道到底在解决什么问题、什么时候该用哪一种、怎么避免线上事故。
其实很简单:隔离级别是目标,锁是手段,MVCC是提速方案,死锁是事故结果。
下面我用通俗方式,把整个逻辑讲透,再结合电商、支付、订单、库存、优惠券、报表导出等高频场景,讲清楚为什么会翻车、怎么改、背后锁在干什么。
一、事务隔离级别:数据库给并发设的四道防线
数据库在多人同时读写数据时,最怕三件事:
- 脏读:读到别人没提交、随时可能回滚的数据
- 不可重复读:同一事务两次读同一行,结果不一样
- 幻读:范围查询时,凭空多出别人新插入的数据
为了控制这三种情况,SQL标准定义了四种隔离级别,从宽松到严格依次是:
1)读未提交(Read Uncommitted)
什么都不防,别人改一半你也能看到。
- 会脏读、不可重复读、幻读
- 生产环境基本不用,仅极限吞吐场景
2)读已提交(Read Committed,RC)
别人提交后你才能看到。
- 解决:脏读
- 没解决:不可重复读、幻读
- Oracle、PostgreSQL默认级别,并发高、死锁少
3)可重复读(Repeatable Read,RR)
同一事务内,你看到的数据永远不变,像戴上VR眼镜。
- 解决:脏读、不可重复读
- MySQL(InnoDB)通过间隙锁,事实上防住了幻读
- MySQL默认级别,但间隙锁易死锁
4)串行化(Serializable)
所有事务排队执行,读写互斥。
- 解决:脏读、不可重复读、幻读
- 并发性能几乎归零,只用于金融资金绝对强一致场景
一句话总结:
RC:快、易不一致;RR:稳、易死锁;SR:绝对安全、极慢。
二、锁:隔离级别背后真正干活的武器
1)锁的两种性质
- 共享锁(S锁/读锁):大家一起读,谁都别改
- 排他锁(X锁/写锁):我在写,别人都别读别改
2)锁的三种范围(最关键)
- 记录锁(Record Lock):只锁某一行
- 条件:主键/唯一索引精准命中
- 间隙锁(Gap Lock):锁空白区间,防别人插新数据(防幻读)
- 条件:RR级别 + 范围查询/查不到数据
- 临键锁(Next-Key Lock):记录锁 + 间隙锁,RR默认
3)意向锁(IS/IX):表级“打招呼”
不用记细节,理解一句话:
加行锁前先打个招呼,避免锁表时遍历全表,性能灾难。
三、锁与索引:最致命的坑
核心铁律:MySQL行锁,锁的是索引,不是数据!
1)有索引:只锁命中行/间隙(安全、并发高)
SELECT * FROM user WHERE id=10 FOR UPDATE;
- id是主键索引 → 只锁id=10一行
2)查不到数据:RR级别自动加间隙锁(防幻读)
SELECT * FROM user WHERE id=99 FOR UPDATE; -- id=99不存在
- RR级别 → 锁住相邻两个索引之间的区间
- 别人插id=99 → 阻塞
3)无索引:直接锁全表(线上大忌)
SELECT * FROM user WHERE name='张三' FOR UPDATE; -- name无索引
- 全表扫描 → 整张表所有行全部上锁
- 别人下单、改数据 → 全部卡死
一句话:没索引 + FOR UPDATE = 锁表、崩库。
四、电商高并发六大典型翻车场景 + 最优解法
场景1:秒杀扣库存(热点行竞争)
翻车写法(悲观锁长事务)
BEGIN;
SELECT stock FROM sku WHERE id=1001 FOR UPDATE;
if(stock>0) UPDATE sku SET stock=stock-1 WHERE id=1001;
COMMIT;
- 锁时间长(含网络+业务判断)
- 万人排队,连接超时
最优:原子UPDATE
UPDATE sku SET stock=stock-1 WHERE id=1001 AND stock>0;
- 锁只在数据库内部瞬间完成
- 无网络延迟、并发飙升
场景2:订单防重提交(间隙锁死锁)
翻车写法
SELECT * FROM order WHERE no='20260520' FOR UPDATE;
if(不存在) INSERT ...;
- RR级别 + 查不到 → 间隙锁
- 两个并发请求 → 互相等 → 死锁
最优:唯一索引 + 异常捕获
INSERT INTO order(no,user_id) VALUES('20260520',1001);
- 重复插入 → 直接抛DuplicateKeyException
- 无锁、无死锁、性能最高
场景3:批量改价(大事务锁死)
翻车写法
- 一个事务改5000条商品
- 事务内调用第三方接口(网络I/O)
- 锁长时间不释放 → 前台下单卡死
最优:分批小事务 + 异步调用
- 每100条一个独立事务
- 事务外异步通知第三方
- 快进快出,锁秒释放
场景4:热点账户(单一行扛不住)
翻车写法
UPDATE account SET balance=balance+100 WHERE merchant_id=8888;
- 大主播/商户,几万并发改同一行 → CPU拉满、超时
最优:账户分片
- 拆成10个子账户:slice_id 0~9
- 随机打到其中一个子账户
- 锁冲突降为1/10
场景5:优惠券防刷(间隙锁死锁)
翻车写法
SELECT * FROM user_coupon WHERE user_id=1001 AND cid=99 FOR UPDATE;
if(无记录) INSERT ...;
- 查不到 → 间隙锁
- 并发点击 → 死锁
最优:Redis分布式锁拦截
- 进入数据库前,Redis SetNX 抢锁
- 抢锁成功 → 数据库INSERT(无需锁)
- Redis扛高并发,数据库干净无锁
场景6:报表导出(读锁阻塞写)
翻车写法
- 大范围SELECT导出历史订单
- RR级别 + 无索引 → 大量间隙锁
- 前台下单、改状态 → 卡死
最优:读写分离 + MVCC快照读
- 报表查从库/数仓,不影响主库
- 主库普通SELECT不加锁(MVCC快照)
五、锁时长对比:长事务 vs 原子操作
长事务(FOR UPDATE + 业务判断)
- 锁覆盖:网络传输 + Java逻辑 + 数据库
- 并发极低,极易超时
原子UPDATE(最优)
- 锁仅在数据库内部瞬间完成
- 毫秒级释放,并发提升百倍
六、高并发优化终极口诀
- 能用UPDATE原子操作,别用SELECT+UPDATE
- 防重优先Redis/唯一索引,别用FOR UPDATE
- RR级别间隙锁易死锁,RC更稳(无幻读)
- 无索引别加FOR UPDATE,等于锁表
- 事务越小越好,别在事务里调外网
- 热点数据分片、读写分离、Redis挡枪
随时随地看视频