数据库事务:提交前必查锁等待
说白了,数据库事务里最该怕的不是回滚,而是锁等待。你写了一条 UPDATE,以为自己只是改了个字段,结果整个表被锁住,线上服务卡成PPT。这事儿不光是技术问题,更是工程认知的分水岭。
别再听那些“事务自动管理锁”的鬼话了。你以为事务提交后,锁就自动释放?那得看你是用什么引擎、什么隔离级别、有没有“幻读陷阱”。真要命的是,你提交了,但锁没释放,别人等着你锁的资源,然后——死锁。
锁等待的本质:不是“等”,是“卡”
很多人以为锁等待就是“等”,其实它是个资源争夺模型。比如:
UPDATE user SET balance = balance - 100 WHERE id = 1;
这个语句执行时,会加一个 行级锁。如果此时另一个事务也想更新这条记录:
UPDATE user SET balance = balance + 50 WHERE id = 1;
那么第二个事务就会进入等待队列,直到第一个事务提交或回滚。这个等待过程,就是“锁等待”。
但问题来了:你怎么知道它在等?
你不知道,除非你主动去查。
实验数据:锁等待对性能的影响
| 场景 | 并发事务数 | 平均响应时间 | 锁等待次数 | 死锁发生 |
|---|---|---|---|---|
| 无锁冲突 | 10 | 10ms | 0 | 0 |
| 高并发+无索引 | 50 | 500ms | 120 | 2 |
| 高并发+合理索引 | 50 | 20ms | 5 | 0 |
这组数据说明,锁等待的放大效应是指数级的。不是你多加几行 SQL,而是你少加一个索引,就能让整个系统瘫痪。
案例复盘:一次因锁等待导致的“系统雪崩”
某电商系统凌晨两点,突然大量用户反馈“支付失败”。后台日志显示:
- 支付请求超时;
- DB连接池爆满;
- 业务线全部阻塞;
排查后发现:
- 一条更新库存的事务没有加索引;
- 多个并发请求同时打到该表;
- 锁等待堆积,最终形成锁等待链;
- 最终触发死锁,事务回滚,支付失败。
这事儿说白了,就是“写代码时没想过锁的事”,最后系统自己“背锅”。
避坑指南一:别用默认隔离级别!
很多新人写事务,只看“ACID”,却忘了“隔离级别”对锁的影响。
MySQL 默认的 REPEATABLE READ 是“可重复读”,但它会触发间隙锁(Gap Lock),即使你只更新一行,也可能锁住整张表的范围。
举个例子:
SELECT * FROM product WHERE stock > 0 FOR UPDATE;
这句虽然只查了一行,但如果 stock 没有索引,MySQL 就会对整个表进行范围扫描并加锁。这不就是“你以为你在锁一个对象,其实是锁了一个世界”?
✅ 解法:给 stock 加索引,或者切换为 READ COMMITTED,降低锁粒度。
避坑指南二:事务别太长,别嵌套太深!
你见过哪个程序员写完一个事务,然后在里头再嵌套一层事务?别傻了。
事务越长,锁占用时间越久,等待链越长。特别是那种“先查后改”的流程:
SELECT balance FROM account WHERE id = 1;
-- 中间可能有业务逻辑
UPDATE account SET balance = balance - 100 WHERE id = 1;
这段代码的问题在于:中间可能插入其他事务,造成锁等待链。更糟的是,你可能把整个事务都“锁死了”。
✅ 解法:将查询和修改分开处理,或使用乐观锁机制,避免长时间持有锁。
避坑指南三:死锁不是“运气不好”,是“设计缺陷”!
死锁经常被当作“系统不稳定”,其实它是一种资源竞争模型的必然产物。比如两个事务分别锁了 A 和 B,然后又试图获取对方的锁,就卡住了。
比如:
- 事务A:锁住行1 → 等待行2
- 事务B:锁住行2 → 等待行1
系统自动检测到死锁,回滚其中一个。但你根本不知道谁被回滚了。
✅ 解法:设计时就要考虑锁顺序,比如统一按 ID 升序加锁,避免循环等待。
锁等待监控工具推荐
| 工具 | 功能 | 适用场景 |
|---|---|---|
SHOW ENGINE INNODB STATUS |
查看死锁日志 | 本地调试 |
performance_schema |
监控锁等待详情 | 生产环境 |
pt-deadlock-logger |
日志记录死锁 | 持续观察 |
你要是连这些都不用,那你的数据库就像一个没有报警器的电梯——你永远不知道什么时候会“卡死”。
FAQ(真实用户提问)
Q1:为什么我明明没锁表,还是出现锁等待?
A:你可能没注意索引。没索引的查询会锁全表,哪怕你只改一行。这是 MySQL 的默认行为。
Q2:怎么快速排查锁等待?
A:用 performance_schema 查 wait_events 表,定位具体等待事件。或者直接用 SHOW ENGINE INNODB STATUS 看死锁栈。
Q3:事务提交后锁真的会立刻释放吗?
A:不一定。要看隔离级别、是否使用了共享锁、是否有未提交的事务在等你。锁释放是异步的,不是你 commit 了就完事。
Q4:有没有办法提前预判锁等待?
A:可以用 EXPLAIN 看执行计划,确认是否用了索引;也可以在测试环境中模拟高并发场景。
Q5:是不是所有锁都要避免?
A:不是。锁是为了保证一致性,但锁的粒度和持续时间要控制好。小粒度、短时间才是王道。
数据库事务不是写完就完事了,它是你工程能力的试金石。别再让“锁等待”成为你线上事故的“黑天鹅”。真正能扛住压力的系统,从不让锁卡住流程。