📖 本篇简介:死锁与主从延迟是 MySQL 高并发场景下的两大隐形杀手。死锁表面上只是"偶尔报错",但风暴来时会把业务线程全部卡住;主从延迟表面上只是"数据晚几秒",但切换时可能丢数据、读一致性出问题。本章复盘 10 个典型事故,每个按"事故经过 → 根因 → 处理 → 预防"四要素展开。所有 SQL 示例基于 MySQL 8.0 语法。读完你能在死锁爆发的第一时间定位热点表,在主从延迟告警时知道该拆哪个大事务。
死锁和主从延迟有一个共同特点:平时不声不响,爆发时直接上 P0。因为死锁是偶发的、延迟是渐变的,不像慢查询那样立刻有告警。所以平时的监控和巡检比事后救火更重要。
本章 10 个复盘点,前 4 个讲死锁,后 6 个讲主从延迟。建议你读完每个案例后,先自己想"如果是我值班会怎么处理",再对照文中的做法。
本章适合已经理解锁机制与主从复制原理、但缺少线上排障经验的工程师。建议配合第 5 章(锁机制)、第 6 章(MVCC)、第 15 章(主从复制)、第 22 章(死锁现场处理)一起阅读。
目录
- 坑 1:并发秒杀扣库存引发死锁风暴
- 坑 2:多表更新顺序不一致导致互相等待
- 坑 3:间隙锁范围过大引发死锁
- 坑 4:长事务持锁导致锁等待雪崩
- 坑 5:大事务组提交造成主从延迟暴涨
- 坑 6:大表 DDL 期间主从延迟几十分钟
- 坑 7:从库单线程回放跟不上主库写入
- 坑 8:超大 binlog 事件导致从库回放卡住
- 坑 9:强制走全表索引打满从库 IO
- 坑 10:半同步超时退化为异步切换后丢数据
- 死锁排查标准流程
- 主从延迟排查标准流程
- 新手常见误区
- 死锁与主从延迟监控指标速记
- 事故复盘后的改进项清单
- 速查表:死锁与主从延迟应急命令
- 自测:死锁与延迟排查
- 压测时必须覆盖死锁与主从延迟
- 定期演练:不要等真出事才第一次处理
- 一句话总结
坑 1:并发秒杀扣库存引发死锁风暴
事故经过:大促零点秒杀,10 万用户同时抢 1000 件商品。应用日志里突然刷出大量 "Deadlock found when trying to get lock" 报错,成功率从 100% 跌到 30%。客服收到大量"下单失败"反馈。值班同学一开始以为是代码 bug,回滚版本后问题依旧,才意识到是数据库死锁。
-- 事务 A(用户 1)
BEGIN;
UPDATE stock SET count = count - 1 WHERE sku_id = 100 AND bucket = 0;
UPDATE stock SET count = count - 1 WHERE sku_id = 100 AND bucket = 1;
COMMIT;
-- 事务 B(用户 2)顺序相反
BEGIN;
UPDATE stock SET count = count - 1 WHERE sku_id = 100 AND bucket = 1;
UPDATE stock SET count = count - 1 WHERE sku_id = 100 AND bucket = 0;
COMMIT;
根因:两个事务以相反顺序更新同一组行,形成循环等待。A 等 B 持有的 bucket=1 锁,B 等 A 持有的 bucket=0 锁,InnoDB 检测到死锁后主动回滚其中一个。
处理:紧急把扣库存 SQL 改成统一顺序(先 bucket=0 再 bucket=1),死锁立刻消失;同时把扣库存逻辑迁到 Redis 预扣,MySQL 只异步落库。
-- 修复后:所有事务按相同顺序加锁
UPDATE stock SET count = count - 1 WHERE sku_id = 100 AND bucket = 0;
UPDATE stock SET count = count - 1 WHERE sku_id = 100 AND bucket = 1;
预防:所有多表/多行更新必须统一加锁顺序;高并发写热点行优先用 Redis 预扣 + 异步落库;死锁日志 innodb_print_all_deadlocks=ON 必开。
坑 2:多表更新顺序不一致导致互相等待
事故经过:订单服务上线新功能,同时更新 order 和 inventory 两张表。高峰期每天出现几十次死锁,错误率 0.5%,客诉不断。因为错误率不高,一开始没重视,直到运营周报里看到客诉趋势上涨才立项排查。
-- 应用 A:先更订单再扣库存
BEGIN;
UPDATE trade_order SET status = 1 WHERE id = 1001;
UPDATE inventory SET locked = locked + 1 WHERE sku_id = 2001;
COMMIT;
-- 应用 B(另一个入口):先扣库存再更订单
BEGIN;
UPDATE inventory SET locked = locked + 1 WHERE sku_id = 2001;
UPDATE trade_order SET status = 1 WHERE id = 1001;
COMMIT;
根因:两个业务入口以相反顺序访问相同的两张表,交叉点上形成死锁。这是典型的"多表加锁顺序不一致"问题。
处理:统一团队规范:任何事务涉及 order + inventory,必须先更 order 再更 inventory。统一后死锁消失。
-- 规范后的模板(本示例基于 MySQL 8.0)
BEGIN;
UPDATE trade_order SET status = ? WHERE id = ?;
UPDATE inventory SET locked = locked + ? WHERE sku_id = ?;
COMMIT;
预防:团队制定"资源访问顺序表",多表事务必须按统一顺序;Code Review 专门盯多表 UPDATE 的顺序;死锁监控接到告警。
坑 3:间隙锁范围过大引发死锁
事故经过:用户表上有 (tenant_id, status) 联合索引,业务按 tenant_id 查询待处理订单。高峰期出现大量死锁,EXPLAIN 一看走了范围扫描。这个问题在测试环境从来没复现过,一上生产高并发就炸。
-- 事务 A
UPDATE orders SET status = 10
WHERE tenant_id = 5 AND status = 1;
-- 走 (tenant_id, status) 索引,锁住 status=1 的记录 + 间隙
-- 事务 B
INSERT INTO orders(tenant_id, status, ...) VALUES (5, 1, ...);
-- 想插入 status=1 的新行,被 A 的间隙锁挡住
根因:RR 隔离级别下,范围查询会加 next-key lock(记录锁 + 间隙锁)。间隙锁之间不兼容,两个事务同时持有相邻间隙锁时,互相等待对方的插入意图锁,形成死锁。
处理:把隔离级别从 RR 降到 RC(RC 没有间隙锁),死锁立刻消失;业务上 RC 足够,因为已经有唯一索引兜底。
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 调整为 RC(本示例基于 MySQL 8.0)
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
预防:不需要 RR 的业务库直接用 RC,减少间隙锁;范围查询尽量用等值查询;唯一索引插入前先 SELECT 判断,减少插入冲突。
坑 4:长事务持锁导致锁等待雪崩
事故经过:财务月底跑批量对账脚本,一个事务跑了 20 分钟,期间锁住了大量订单行。在线下单接口全部超时,错误率冲到 30%。值班同学一开始没意识到是对账脚本,查了半天应用日志才定位到数据库。
-- 对账脚本:一个大事务扫全表
BEGIN;
SELECT * FROM orders WHERE created_at < '2026-10-01' FOR UPDATE;
-- 处理数据,耗时 20 分钟
UPDATE orders SET checked = 1 WHERE id = ?;
COMMIT;
根因:SELECT ... FOR UPDATE 持锁时间随脚本执行时长线性增长,期间所有想更新这些行的事务全部排队。长事务是锁等待雪崩的根源。
处理:立刻 KILL 对账脚本;改成小批量循环,每批 500 行提交一次。
-- 修复后:分批小事务
-- Python 伪代码
for batch_id in batches:
cur.execute("""
SELECT id FROM orders
WHERE created_at < '2026-10-01' AND checked = 0
LIMIT 500
""")
ids = [row[0] for row in cur.fetchall()]
cur.execute("UPDATE orders SET checked=1 WHERE id IN (%s)" % ','.join(ids))
conn.commit()
预防:SELECT ... FOR UPDATE 必须短事务;监控 information_schema.innodb_trx,事务时长超过 5 秒就告警;批处理脚本必须分批提交。
坑 5:大事务组提交造成主从延迟暴涨
事故经过:运营凌晨跑数据订正脚本,一个大事务更新了 500 万行。提交后 5 分钟,主从延迟从 0 涨到 20 分钟。在线读从库的接口全部读到旧数据。第二天早上运营上班,发现前一天的修改在从库上看不到,才发现延迟问题。
根因:主库提交大事务时,binlog 一次性写入一个巨大事件;从库单线程回放这个事件要花很长时间,期间所有其他更新都被堵住。组提交(group commit)对超大事务无效。
处理:等从库回放完,延迟慢慢追平;事后把订正脚本改成每 1000 行提交一次。
-- 查看主从延迟(本示例基于 MySQL 8.0)
SHOW REPLICA STATUS\G
-- 重点看 Seconds_Behind_Source
预防:任何单事务更新超过 1 万行就要警惕;大事务拆分小事务;从库开启多线程回放(binlog_transaction_dependency_tracking)。
坑 6:大表 DDL 期间主从延迟几十分钟
事故经过:给订单表加一个索引,主库上执行了 10 分钟(INPLACE 算法),但从库回放这个 DDL 花了 40 分钟,期间主从延迟一路涨到 30 分钟。业务方读从库查到的是 30 分钟前的旧数据。当时正好赶上早高峰,业务方投诉说"数据怎么都是旧的"。
根因:DDL 在从库上是单线程回放的,即使主库用了 INPLACE,从库还是要串行执行。大表 DDL 期间从库无法处理其他事件,延迟持续累积。
处理:DDL 期间临时把读流量切回主库,等从库追平后再切回;事后大表 DDL 改用 gh-ost,对主从都更友好。
预防:大表 DDL 走 gh-ost / pt-osc;DDL 期间监控主从延迟,超过阈值就切主;从库开启并行回放。
坑 7:从库单线程回放跟不上主库写入
事故经过:业务高峰期主库 QPS 8000,从库延迟持续 5~10 秒,偶尔飙到 30 秒。读从库的商品列表页经常看到刚下架的商品。这个问题持续了一周,一直被当作"正常波动",直到产品经理提出来才重视。
根因:MySQL 5.7 默认从库单线程回放,主库每秒 8000 个事务,从库一个线程根本追不上。8.0 虽然支持基于 WRITESET 的并行回放,但默认配置没开。
处理:开启基于 WRITESET 的并行回放,延迟立刻降到 1 秒以内。
-- 8.0 并行回放配置(本示例基于 MySQL 8.0)
SET GLOBAL binlog_transaction_dependency_tracking = WRITESET;
SET GLOBAL replica_parallel_workers = 8;
SET GLOBAL replica_parallel_type = LOGICAL_CLOCK;
预防:8.0 从库默认开 WRITESET 并行;监控 Seconds_Behind_Source 持续 > 5 秒就要查原因;写入 QPS 预估超过 5000 就提前调优。
坑 8:超大 binlog 事件导致从库回放卡住
事故经过:批量导入任务一次 INSERT 了 100 万行,binlog 事件大小 2GB。从库回放这个事件时,IO 打满,延迟涨到 1 小时。期间从库无法处理任何其他更新。导入任务是半夜跑的,没人发现,直到第二天早高峰读从库报错才知道。
根因:ROW 格式 binlog 会记录每一行的变更,100 万行就是 100 万条 row event,binlog 文件巨大。从库回放时要把这些 event 一条条应用,单线程下就是慢。
处理:等回放完;事后把批量导入改成每 1000 行提交一次,binlog 事件切碎。
-- 查看 binlog 大小
SHOW BINARY LOGS;
-- 大事务拆分
-- Python 伪代码:每 1000 行 commit 一次
预防:单事务影响行数不超过 1 万;binlog 用 ROW 格式但要注意大事务;批量导入走专门的导入窗口,避开高峰期。
坑 9:强制走全表索引打满从库 IO
事故经过:数据分析平台在从库上跑一个全表扫描的统计 SQL,从库 IO 100%,主从延迟从 0 涨到 15 分钟。在线读从库的接口全部超时。数据分析师觉得"从库就是用来查的",不知道会影响在线业务。
-- 分析 SQL:全表扫 1 亿行做聚合
SELECT customer_id, SUM(amount)
FROM orders
GROUP BY customer_id;
-- 这条 SQL 在从库上跑了 10 分钟,IO 打满
根因:从库虽然是只读,但同样的硬件资源被大查询占满后,正常的复制回放和在线查询都跑不动。从库不是"无限资源池"。
处理:KILL 这条分析 SQL;把分析任务迁到独立的备份从库,跟在线只读从库物理隔离。
预防:从库分级——在线只读从库、报表从库、备份从库,各司其职;分析 SQL 必须走报表从库;从库也配慢查询告警。
坑 10:半同步超时退化为异步切换后丢数据
事故经过:主库故障自动切换到从库,切换后发现丢了 5 秒的订单数据。复盘发现半同步复制因为网络抖动超时,自动退化成异步,主库最后几个事务没同步到从库就挂了。这 5 秒的数据是当天的支付订单,直接造成了客诉和资金损失。
根因:半同步复制(semi-sync)有超时参数,超时后退化为异步。故障发生时正好处于"异步窗口期",从库没有最新数据。
处理:丢失的数据通过 binlog 补回;调整半同步参数,延长超时、降低退化为异步的概率;配合 MHA 等高可用方案,切换前确认数据一致性。
-- 半同步配置(本示例基于 MySQL 8.0)
SET GLOBAL rpl_semi_sync_master_timeout = 10000; -- 10 秒超时,别太短
SET GLOBAL rpl_semi_sync_master_wait_for_sync_count = 1; -- 至少 1 个从库 ACK
预防:半同步超时不要设太短(建议 5~10 秒);高可用切换前强制等待从库追平;重要业务考虑 MGR 组复制,强一致。
死锁排查标准流程
死锁不是玄学,有标准排查路径。按下面五步走,90% 的死锁 30 分钟内能定位。
| 步骤 | 动作 | 命令 / 工具 |
|---|---|---|
| 1 | 开启死锁日志 | SET GLOBAL innodb_print_all_deadlocks = ON |
| 2 | 复现时抓取现场 | SHOW ENGINE INNODB STATUS\G |
| 3 | 分析死锁日志 | 看 LATEST DETECTED DEADLOCK 段,两个事务各自持有什么锁、等什么锁 |
| 4 | 定位业务代码 | 根据 SQL 反向找到应用入口 |
| 5 | 改加锁顺序 / 降隔离级别 | 统一顺序 / RC |
-- 死锁日志关键看三段:
-- 1. *** (1) TRANSACTION:事务 1 在做什么、持有哪些锁
-- 2. *** (2) TRANSACTION:事务 2 在做什么、持有哪些锁
-- 3. *** WE ROLL BACK TRANSACTION (1):MySQL 回滚了谁
-- 两边的 SQL 都要拿到,回去找业务代码
死锁 vs 锁等待:死锁是循环等待,MySQL 会自动回滚一个;锁等待是单向等待,事务会一直等到 innodb_lock_wait_timeout(默认 50 秒)。两者排障思路不同,不要混。
死锁日志开了之后,会写到错误日志里。生产环境建议每天巡检一次死锁日志,不要等业务报错了才去看。一周死锁超过 10 次,就要立项优化。
死锁日志里两个事务的 SQL 都要拿到,回去找对应的业务代码。重点看两个事务的加锁顺序是否相反、是否有范围查询导致间隙锁。
主从延迟排查标准流程
主从延迟告警时,按下面三步定位,不要上来就重启从库。
| 步骤 | 检查项 | 正常 | 异常处理 |
|---|---|---|---|
| 1 | 复制线程状态 | Replica_IO/SQL_Running 都是 Yes | 一个 No 就看 Last_Error |
| 2 | 延迟指标 | Seconds_Behind_Source < 5s | 持续涨就继续查 |
| 3 | 主库大事务 | SHOW PROCESSLIST 看长事务 | KILL 或等它跑完 |
-- 完整排查命令序列(本示例基于 MySQL 8.0)
-- 在从库上执行
SHOW REPLICA STATUS\G
-- 重点看:
-- Replica_IO_Running: Yes
-- Replica_SQL_Running: Yes
-- Seconds_Behind_Source: 0
-- Last_Error: 空
-- 在主库上看有没有大事务
SELECT trx_id, trx_started, trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started LIMIT 5;
常见延迟原因排序:大事务 > 大 DDL > 从库跑大查询 > 从库硬件差 > 网络问题。前三个占了 80%。
排查时按这个顺序来:先在主库找大事务(innodb_trx),再看主库有没有在跑 DDL,再看从库 IO 是否打满。不要一上来就重启从库,那只会让情况更糟。
如果三个都查了还是延迟,再看网络和从库硬件。但 90% 的情况都是前三个原因。
新手常见误区
| 误区 | 正确做法 |
|---|---|
| 死锁是数据库的问题,改不了 | 90% 的死锁是应用加锁顺序不对,能改 |
| 主从延迟是网络问题 | 80% 是大事务或大查询,先查主库 |
| 从库随便查,反正不影响主库 | 从库 IO 打满会导致回放卡住,延迟暴涨 |
| 半同步就不会丢数据 | 半同步超时会退化为异步,有窗口期 |
| 开了并行回放就万事大吉 | 大事务和热点行照样串行 |
新手最容易犯的错是"死锁就报错重试呗"。重试确实能解决偶发死锁,但风暴来了重试只会让系统更忙。根因还是要找加锁顺序。
另一个常见误区是"主从延迟是网络问题,等一等就好"。如果延迟持续涨,一定有原因,不要干等。等到延迟 1 小时再处理,业务早就炸了。
还有一个误区是"从库延迟了就把读切回主库"。主库压力本来就大,全部切过去可能主库也挂了。正确做法是先定位延迟原因,能解决就解决,不能解决再考虑切主。
死锁与主从延迟监控指标速记
| 指标 | 正常范围 | 告警阈值 | 说明 |
|---|---|---|---|
| InnoDB_row_lock_waits | < 10/s | > 100/s | 锁等待次数 |
| InnoDB_row_lock_time_avg | < 50ms | > 500ms | 平均锁等待时长 |
| Deadlock count | 0/天 | > 10/天 | 死锁次数 |
| Threads_running | < 20 | > 50 | 正在执行的查询 |
| Seconds_Behind_Source | < 1s | > 10s | 主从延迟 |
| Replica_IO_Running | Yes | No | IO 线程状态 |
| Replica_SQL_Running | Yes | No | SQL 线程状态 |
| Binlog disk usage | < 50% | > 80% | binlog 磁盘占用 |
这些指标要接到 Grafana,死锁次数、主从延迟这两个必须告警到值班手机。死锁不是"正常现象",每天超过 10 次就要立项排查。
告警分级:预警(群消息)→ 严重(打电话)→ 紧急(电话 + 短信)。死锁率和延迟阈值对应不同级别,不要全部打成紧急电话,值班会麻木。
事故复盘后的改进项清单
每次死锁或主从延迟事故处理完,改进项必须落到这张清单上,每条都要有 owner 和 deadline。
| 改进方向 | 具体动作 | 负责方 |
|---|---|---|
| 代码层 | 统一多表加锁顺序、事务内禁 RPC、大改分批 | 业务开发 |
| 配置层 | 开死锁日志、开并行回放、调半同步超时 | DBA |
| 监控层 | 死锁告警、延迟告警、长事务告警 | DBA/SRE |
| 架构层 | 热点行 Redis 预扣、从库分级、MGR | 架构组 |
没有 owner 和 deadline 的改进项等于没写。复盘会结束前,必须逐条确认谁来做、什么时候做完。
改进项做完后,还要验证:下次压测或演练时,问题是否真的消失了。不验证的改进项等于没做。
改进项超过 3 个的话,建一个专项跟踪,每周过一次进度。
速查表:死锁与主从延迟应急命令
| 场景 | 命令 | 说明 |
|---|---|---|
| 查死锁日志 | SHOW ENGINE INNODB STATUS\G | LATEST DETECTED DEADLOCK 段 |
| 查当前锁等待 | SELECT * FROM information_schema.innodb_lock_waits | 谁等谁 |
| 查长事务 | SELECT * FROM information_schema.innodb_trx ORDER BY trx_started | 事务时长 |
| 杀会话 | KILL <processlist_id> | 杀连接 |
| 查主从延迟 | SHOW REPLICA STATUS\G | Seconds_Behind_Source |
| 查复制线程 | SHOW REPLICA STATUS\G | Replica_IO_Running / Replica_SQL_Running |
| 跳过复制错误 | SET GLOBAL sql_replica_skip_counter = 1 | 谨慎使用 |
| 开并行回放 | SET GLOBAL replica_parallel_workers = 8 | 8.0 WRITESET |
| 查死锁详情 | SELECT * FROM performance_schema.data_lock_waits | 8.0 新表 |
| 查持锁事务 | SELECT * FROM performance_schema.data_locks | 谁持锁 |
| 看 binlog 事件 | SHOW BINLOG EVENTS IN 'mysql-bin.000123' LIMIT 10 | 排查大事务 |
| 跳过复制错误 | STOP REPLICA; SET GLOBAL sql_replica_skip_counter=1; START REPLICA | 谨慎 |
| 查看半同步状态 | SHOW STATUS LIKE 'Rpl_semi_sync%' | 是否在用半同步 |
自测:死锁与延迟排查
下面 10 道题模拟值班时可能遇到的死锁与主从延迟场景,先自己想答案,再展开参考答案。
题 1:两个事务以相反顺序更新两行,为什么会死锁?
<details>
<summary>参考答案</summary>
事务 A 持有行 1 的锁,等行 2 的锁;事务 B 持有行 2 的锁,等行 1 的锁,形成循环等待。InnoDB 检测到死锁后主动回滚其中一个事务。解决:所有事务按相同顺序加锁。
</details>
题 2:RR 隔离级别下间隙锁为什么会引发死锁?
<details>
<summary>参考答案</summary>
间隙锁之间不兼容,两个事务同时持有相邻间隙锁时,互相等待对方的插入意图锁。范围查询 + 并发插入是典型场景。解决:业务允许就降到 RC,RC 没有间隙锁。
</details>
题 3:主从延迟突然涨到 10 分钟,你的第一步是什么?
<details>
<summary>参考答案</summary>
先 SHOW REPLICA STATUS 看 Replica_SQL_Running 是否正常、Seconds_Behind_Source 是多少;再查主库上有没有大事务、大 DDL、批量导入;最后看从库 IO 是否被大查询打满。不要一上来就重启从库。
</details>
题 4:为什么大事务会导致主从延迟?
<details>
<summary>参考答案</summary>
主库提交大事务时 binlog 一次性写入巨大事件,从库回放这个事件要花很长时间,期间所有其他更新被堵住。解决:大事务拆小事务,单事务影响行数控制在 1 万以内。
</details>
题 5:半同步复制为什么会丢数据?
<details>
<summary>参考答案</summary>
半同步有超时参数,超时后自动退化为异步。故障发生时正好处于异步窗口期,从库没收到最新事务。解决:超时设长一点(5~10 秒),切换前强制等从库追平,重要业务用 MGR。
</details>
题 6:死锁和锁等待有什么区别?
<details>
<summary>参考答案</summary>
死锁是两个事务循环等待,MySQL 检测到后主动回滚一个,报错给应用;锁等待是单向等待,事务会一直等到 innodb_lock_wait_timeout(默认 50 秒)才超时。死锁是"你死我活",锁等待是"干等"。排障思路不同:死锁看加锁顺序,锁等待看谁持有长事务。
</details>
题 7:为什么从库开了并行回放还是延迟?
<details>
<summary>参考答案</summary>
并行回放不是万能的。如果主库全是大事务,或者全是更新同一行的热点事务,并行度再高也没用。还要看从库硬件是否够、IO 是否被大查询打满、网络是否稳定。先排除大事务和大查询,再调并行度。
</details>
题 8:主从延迟期间,在线业务应该读主还是读从?
<details>
<summary>参考答案</summary>
读自己刚写的数据(read-after-write)必须读主;其他查询读从也可以接受旧数据。工程上做法:写操作后短时间内(比如 5 秒内)同用户的请求强制读主,过期后再切回从。这个叫"写后读一致性"策略。
</details>
题 9:怎么预防大事务?
<details>
<summary>参考答案</summary>
四件事:1)单事务影响行数控制在 1 万以内;2)事务内禁止 RPC/HTTP;3)批处理脚本分批提交;4)监控 innodb_trx,长事务告警。写代码时养成习惯:一个事务只做一件事,做完就 commit。
</details>
题 10:MGR 组复制相比半同步有什么优势?
<details>
<summary>参考答案</summary>
MGR 是基于 Paxos 的强一致复制,多数派确认才算成功,不存在"半同步超时退化为异步"的窗口期。主从切换时不会丢数据。代价是性能比异步低一些,部署更复杂。核心交易业务建议 MGR,一般业务半同步够用。
</details>
自测评分:10 道题答对 8 道以上,说明你已经能独立处理死锁与主从延迟事故;答对 5~7 道,建议再复习一遍第 5、6、15 章;答对 5 道以下,先跟一次真实事故再说。
压测时必须覆盖死锁与主从延迟
很多问题平时测不出来,一上大促就炸。压测时必须专门覆盖这两类场景。
| 压测项 | 方法 | 通过标准 |
|---|---|---|
| 死锁 | 并发跑多表更新,观察死锁率 | 死锁率 < 0.1% |
| 主从延迟 | 压测期间监控 Seconds_Behind_Source | 延迟 < 5s |
| 热点行 | 单 SKU 1000 QPS 扣库存 | 无死锁,无超时 |
| 大事务 | 跑一个 1 万行更新 | 不阻塞在线业务 |
压测报告里必须包含死锁次数和主从延迟这两项指标。很多团队压测只看 QPS 和 RT,不看这两项,结果上线就出事。
压测环境要尽量模拟生产:同样的隔离级别、同样的索引、同样的数据量。拿空表压测出来的结果没有意义。
压测时间也要够长,至少跑 30 分钟,让长事务和延迟问题暴露出来。
定期演练:不要等真出事才第一次处理
死锁和主从延迟问题,必须在测试环境演练过,真出事时才不会手忙脚乱。
| 演练项 | 怎么做 | 目标 |
|---|---|---|
| 死锁注入 | 故意构造两个事务反向更新 | 能 5 分钟内从日志定位 |
| 主从延迟 | 从库上跑大查询 | 能识别是大查询导致延迟 |
| 主库切换 | 手动 failover | RTO < 1 分钟,不丢数据 |
| 半同步退化为异步 | 故意断网 | 知道怎么发现、怎么恢复 |
建议每季度做一次故障演练,把值班同学轮流拉进来。演练不考核,但能暴露很多平时注意不到的流程问题。
演练完也要复盘:哪里卡住了、哪条命令找不到、哪个告警没收到。这些流程问题不演练永远发现不了。
一句话总结
死锁事故的根因 90% 是"加锁顺序不一致"或"间隙锁范围过大";主从延迟事故的根因 90% 是"大事务"或"从库被大查询打满"。记住这两个 90%,遇到问题先往这两个方向查,效率最高。
死锁不可怕,可怕的是不知道为什么死锁;主从延迟不可怕,可怕的是不知道延迟从哪来。把本章的排查命令存成脚本,告警来了一键跑,比手忙脚乱强得多。
最后送一句话:数据库的问题,99% 都能通过 SHOW PROCESSLIST + SHOW ENGINE INNODB STATUS + SHOW REPLICA STATUS 这三条命令定位。把这三条命令背下来,值班时就不会慌。
祝你值班顺利,永远用不上本章的应急命令。
—— 75 章完 ——
下一章讲误删数据与恢复,那是最让人手心出汗的事故类型。
敬请期待。
下一篇预告:第 76 章 事故复盘:误删数据与恢复
文章回复
0 条公开回复