为了账号安全,请及时绑定邮箱和手机立即绑定

MySQL事务隔离级别、锁机制与高并发场景优化:把数据库并发讲得清清楚楚

很多人一提到事务隔离级别、锁、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(最优)

  • 锁仅在数据库内部瞬间完成
  • 毫秒级释放,并发提升百倍

六、高并发优化终极口诀
  1. 能用UPDATE原子操作,别用SELECT+UPDATE
  2. 防重优先Redis/唯一索引,别用FOR UPDATE
  3. RR级别间隙锁易死锁,RC更稳(无幻读)
  4. 无索引别加FOR UPDATE,等于锁表
  5. 事务越小越好,别在事务里调外网
  6. 热点数据分片、读写分离、Redis挡枪
点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消