MYSQL · RELATIONAL DATABASE

MySQL
故障排查手册

THE OPS FIELD MANUAL
InnoDB · binlog · 主从 · 慢查询
mysql> SHOW PROCESSLIST;
slow query deadlock replication lag max connections innodb_buffer_pool binlog

MySQL 故障的 80% 集中在三个域:慢查询(性能)、锁(并发)、主从复制(高可用)。每个域都有自己的诊断工具——慢查询看 slow log,锁看 information_schema,复制看 SHOW SLAVE STATUS。掌握这三组命令,你就能 cover 大部分线上事故。

MySQL 诊断工具箱

每次排障前先想清楚要看哪类信息,选对工具。

SHOW PROCESSLIST
当前所有连接和执行的SQL
SHOW ENGINE INNODB STATUS
InnoDB 内部状态,死锁/锁等待
SHOW SLAVE STATUS
主从复制状态(必看Seconds_Behind_Master)
EXPLAIN
看 SQL 执行计划,定位慢查询
slow_query_log
慢查询日志
performance_schema
详细性能统计
pt-query-digest
慢日志分析神器
mysqldumpslow
自带的慢日志聚合
01

慢查询定位与优化

SLOW QUERY DIAGNOSIS
高频 必会
-- 看当前配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

-- 动态开启,无需重启
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;          -- 1 秒以上算慢查询
SET GLOBAL log_queries_not_using_indexes = 1;  -- 没用索引的也记
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
# my.cnf 持久化配置
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
log_slow_admin_statements = 1     # 记录管理类慢操作
min_examined_row_examined = 1000  # 扫描行数大于此才记录
# 1. 用 mysqldumpslow (自带)
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# -s t 按总执行时间排序  -t 10 取前 10

# 2. 用 pt-query-digest (推荐,更详细)
pt-query-digest /var/log/mysql/slow.log | less
# 按查询类型聚合,看 Query_time 和 Rows_examined
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 'paid';
type 值含义评级
system / const主键/唯一键单行查询★★★★★
eq_ref关联查询用主键★★★★
ref普通索引★★★
range索引范围扫描★★★
index全索引扫描★★
ALL全表扫描(警告!)★ 必须优化
-- 看当前正在执行,且时间超过 5 秒的查询
SELECT * FROM information_schema.processlist
WHERE command != 'Sleep' AND time > 5
ORDER BY time DESC;

-- 杀掉特定查询(谨慎!)
KILL <process_id>;
索引优化三板斧 · ① 看 WHERE/JOIN/ORDER BY 列是否有索引;② 看是否符合最左前缀原则;③ 避免函数/计算让索引失效(WHERE DATE(create_time) = ... 索引失效,改用范围查询)。
02

Too many connections

CONNECTION POOL EXHAUSTION
高频 致命
  • 应用报错 "Too many connections" 或 "Lost connection to MySQL server"
  • 新连接全部被拒绝
  • 已建立的连接还能正常工作
-- 1. 看当前配置和使用
SHOW VARIABLES LIKE 'max_connections';
SHOW STATUS LIKE 'Threads_connected';        -- 当前活跃
SHOW STATUS LIKE 'Max_used_connections';     -- 历史峰值
SHOW STATUS LIKE 'Threads_running';          -- 实际执行中

-- 2. 临时调大(无需重启)
SET GLOBAL max_connections = 2000;

-- 3. 看谁占用了连接
SELECT user, host, command, count(*)
FROM information_schema.processlist
GROUP BY user, host, command
ORDER BY count(*) DESC;
表现根因解决
大量 Sleep 连接应用连接池泄漏 / wait_timeout 太大修连接池 / 调小 wait_timeout
大量 Query 连接慢查询堆积 / 真实压力大优化慢查询 / 扩容
突然全部满应用 bug 死循环建连接找出来重启应用
# my.cnf 配置
[mysqld]
max_connections = 2000             # 总连接数
max_user_connections = 1800        # 单用户连接
wait_timeout = 600                 # 空闲多久断开(默认 28800,太长)
interactive_timeout = 600

# 应用端连接池(以 HikariCP 为例)
# maximum-pool-size = 20 (单实例)
# minimum-idle = 10
# idle-timeout = 300000 (5 分钟)
# max-lifetime = 1800000 (30 分钟,小于 wait_timeout)
max_lifetime 必须小于 wait_timeout · 否则连接池里的连接被 MySQL 单方面断开,应用拿到失效连接会报错。计算:max_lifetime < wait_timeout - 30s。
预留 super_connection · max_connections 满了后,管理员还有 1 个连接可登录(super 权限),用于救急。所以 root 一般连不上时,先确认是不是这个原因。
03

死锁与锁等待

DEADLOCK AND LOCK WAIT TIMEOUT
高频 进阶
  • 应用报 Lock wait timeout exceeded
  • Deadlock found when trying to get lock
  • 更新操作变慢,事务超时
-- MySQL 5.7+ / 8.0 通用
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;

-- 查谁在阻塞谁
SELECT
  r.trx_id AS waiting_trx,
  r.trx_mysql_thread_id AS waiting_pid,
  r.trx_query AS waiting_sql,
  b.trx_id AS blocking_trx,
  b.trx_mysql_thread_id AS blocking_pid,
  b.trx_query AS blocking_sql
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id
JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id;

-- 杀掉阻塞源
KILL <blocking_pid>;
SHOW ENGINE INNODB STATUS\G
-- 找 "LATEST DETECTED DEADLOCK" 段,会列出两个事务的 SQL 和锁信息
-- 模式 A:不同顺序更新同样的行
-- 事务 1:
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;

-- 事务 2:
UPDATE account SET balance = balance - 100 WHERE id = 2;  -- 顺序反了
UPDATE account SET balance = balance + 100 WHERE id = 1;

-- 模式 B:间隙锁冲突(范围更新)
UPDATE orders SET status='paid' WHERE user_id BETWEEN 100 AND 200;
-- 同时插入 user_id=150 的订单,会被间隙锁阻塞
预防死锁三原则
统一访问顺序:所有事务按主键升序更新
事务尽量短:别在事务里做网络调用、文件IO
合适的隔离级别:大部分业务用 READ COMMITTED 比 REPEATABLE READ 更少死锁
-- 锁等待超时(默认 50 秒,根据业务调)
SET GLOBAL innodb_lock_wait_timeout = 30;

-- 死锁自动检测(默认开启,高并发时考虑关闭)
SET GLOBAL innodb_deadlock_detect = ON;
04

主从复制延迟

REPLICATION LAG DIAGNOSIS
高频 必会
-- 在从库执行,关键字段
SHOW SLAVE STATUS\G

-- 重点看这几个字段
-- Slave_IO_Running: Yes        (IO 线程,接收 binlog)
-- Slave_SQL_Running: Yes       (SQL 线程,应用 binlog)
-- Seconds_Behind_Master: 0     (延迟秒数,关键!)
-- Last_IO_Error / Last_SQL_Error
-- Step 1. 看延迟趋势
SHOW SLAVE STATUS\G
-- Seconds_Behind_Master 持续增加 → 从库追不上
-- 数值跳动但不大 → 正常网络抖动

-- Step 2. 找主从位点差
-- 主库 Position vs 从库 Exec_Master_Log_Pos
-- 差距大 → 从库 SQL 线程慢
-- 主库 Position vs 从库 Read_Master_Log_Pos
-- 差距大 → 主从网络慢或 IO 线程慢

-- Step 3. 看从库在执行什么
SELECT * FROM performance_schema.replication_applier_status_by_worker;
-- 找 LAST_APPLIED_TRANSACTION,看正在执行的事务
原因识别解决
大事务单个 binlog 事件巨大拆分事务,小批量提交
从库无索引主有索引从没检查表结构一致性
主从硬件差从库 IO/CPU 弱升级从库或加速 IO
单线程复制瓶颈5.6 之前默认单线程开启并行复制
从库被读压垮从库 CPU 100%扩容或读写分离调整
[mysqld]
slave_parallel_workers = 8                    # 并行线程数
slave_parallel_type = LOGICAL_CLOCK           # 基于组提交
slave_preserve_commit_order = ON              # 保持提交顺序
binlog_transaction_dependency_tracking = WRITESET  # 5.7.22+
Seconds_Behind_Master 不准 · 这个值是当前正在执行的事件的时间戳与主库当前时间的差,如果从库正好闲着,会显示 0,但实际可能还有积压。监控应同时看 binlog position 差距。
05

主从数据不一致

REPLICATION DATA INCONSISTENCY
中频 致命
  • 从库 SQL 线程报错停止: Slave_SQL_Running: No
  • Last_SQL_Error 显示具体错误
  • 用户反馈"主库能查到的数据,从库查不到"
  • 在从库执行了写操作(直接破坏一致性)
  • 主库执行了从库没有的库/表
  • 从库唯一键冲突(可能之前有人在从库插过数据)
  • binlog 损坏
-- 1. 先看具体错误
SHOW SLAVE STATUS\G
-- Last_SQL_Errno: 1062 (Duplicate entry)
-- Last_SQL_Error: Error 'Duplicate entry...' on query 'INSERT INTO...'

-- 2. 临时跳过错误(仅紧急救急,谨慎!)
STOP SLAVE;
SET GLOBAL sql_slave_skip_counter = 1;
START SLAVE;
-- 跳过一个事件,然后看是否继续
# 主库执行,自动对比所有表的校验值
pt-table-checksum --host=master \
  --user=root --password=xxx \
  --databases=mydb \
  --replicate=test.checksums

# 检查不一致的行
pt-table-sync --print \
  --replicate=test.checksums \
  h=master,u=root,p=xxx
# 输出修复 SQL,人工确认后再 --execute
从库不要写 · 生产环境从库必须配置 read_only = 1super_read_only = 1,防止误操作。但 super 权限的用户仍能写,所以应用账号一定不要给 super。
新搭从库 · 用 xtrabackup 物理备份恢复,比 mysqldump 快几十倍,适合 GB/TB 级数据库。
06

MySQL CPU 飙高 / 性能下降

CPU SPIKE INVESTIGATION
高频 进阶
-- Step 1. 看当前在跑啥
SELECT id, user, host, db, command, time, state, info
FROM information_schema.processlist
WHERE command != 'Sleep'
ORDER BY time DESC
LIMIT 20;

-- Step 2. 看哪些 SQL 在频繁执行
SELECT digest_text, count_star, sum_timer_wait/1e12 AS total_sec
FROM performance_schema.events_statements_summary_by_digest
ORDER BY count_star DESC
LIMIT 10;

-- Step 3. 看锁等待
SELECT count(*) FROM information_schema.innodb_lock_waits;
# MySQL 进程的 CPU 分布
top -H -p $(pidof mysqld)
# 看是哪个线程在猛吃 CPU

# 把线程 ID 转 16 进制(用于关联 MySQL 内部线程)
printf "%x\n" <thread_id>
# mysqladmin 看实时状态
mysqladmin -uroot -p extended-status -i1 | \
  grep -E "Questions|Com_select|Com_insert|Com_update|Com_delete|Threads_running"

# 计算 QPS
mysqladmin -uroot -p extended-status -i1 -r | grep Questions
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
-- 应为物理内存的 50-70%(独占数据库的机器)

SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
-- 计算命中率 = 1 - reads / read_requests
-- 低于 99% 说明 buffer pool 不够大

SHOW STATUS LIKE 'Innodb_row_lock_waits';
SHOW STATUS LIKE 'Innodb_row_lock_time_avg';
CPU 高的三大主因 · ① 慢查询(看 processlist) ② 全表扫描(看 EXPLAIN) ③ 锁竞争(看 lock_waits)。处理顺序也是这个,先杀慢查询止血,再优化 SQL,最后排查锁。
07

binlog 暴增,磁盘告急

BINLOG GROWTH AND CLEANUP
中频 必会
-- 1. 看 binlog 列表和大小
SHOW BINARY LOGS;

-- 2. 看 binlog 保留天数
SHOW VARIABLES LIKE 'expire_logs_days';        -- 5.7
SHOW VARIABLES LIKE 'binlog_expire_logs_seconds'; -- 8.0
-- 方法 A:删除指定时间之前的(推荐)
PURGE BINARY LOGS BEFORE '2026-04-01 00:00:00';

-- 方法 B:删除指定文件之前的
PURGE BINARY LOGS TO 'mysql-bin.000100';

-- 方法 C:设置自动过期(永久生效)
SET GLOBAL binlog_expire_logs_seconds = 604800;  -- 7 天
有从库时不能乱删 binlog · 删除从库还没拉取的 binlog 会导致主从中断。删除前必须确认:
SHOW SLAVE STATUS 中 Master_Log_File 之前的才能安全删。
[mysqld]
# 8.0+
binlog_expire_logs_seconds = 604800     # 7 天自动清理

# 5.7
expire_logs_days = 7

# 单个 binlog 文件大小(默认 1G)
max_binlog_size = 256M                  # 调小便于清理

# 推荐配置(数据安全 + 性能平衡)
sync_binlog = 1                         # 每次事务都刷盘(最安全)
binlog_format = ROW                     # 行级,推荐
binlog_row_image = MINIMAL              # 节省空间
# 解析 binlog 为可读 SQL
mysqlbinlog --base64-output=DECODE-ROWS -v mysql-bin.000123

# 找出某段时间的所有变更
mysqlbinlog --start-datetime="2026-05-01 10:00:00" \
            --stop-datetime="2026-05-01 11:00:00" \
            mysql-bin.000123

# 找出哪个表被改最多(诊断 binlog 暴增)
mysqlbinlog --base64-output=DECODE-ROWS -v mysql-bin.000123 | \
  grep -oP '### (INSERT INTO|UPDATE|DELETE FROM) \K\S+' | sort | uniq -c | sort -rn
08

字符集乱码 — 一次性根治

CHARSET / COLLATION ISSUES
中频 入门
-- 看当前所有字符集设置
SHOW VARIABLES LIKE 'character%';
SHOW VARIABLES LIKE 'collation%';
层级变量说明
服务器character_set_server默认字符集
数据库character_set_database建库时定
连接character_set_connection客户端连接时定
客户端character_set_client客户端发送数据的字符集
# my.cnf,记住:必须用 utf8mb4 不是 utf8
[mysqld]
character_set_server = utf8mb4
collation_server = utf8mb4_unicode_ci
init_connect = 'SET NAMES utf8mb4'

[client]
default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4
utf8 ≠ UTF-8 · MySQL 的 utf8 是阉割版,只支持 3 字节字符,存不了 emoji 和部分中文(如 𠀀)。必须用 utf8mb4,这才是真正的 UTF-8。
-- 看表的字符集
SHOW CREATE TABLE users;

-- 转换整张表(数据量大时慎用,会锁表)
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 只改默认值,不动现有数据(快,但旧数据字符集不变)
ALTER TABLE users DEFAULT CHARACTER SET utf8mb4;
connection 不对的临时解决 · 应用连接后立即执行 SET NAMES utf8mb4,一次性设置 client / connection / results 三个字符集。
09

大表 DDL 不锁库 — 在线 DDL 方案

ONLINE SCHEMA CHANGE
中频 高级
  • 需要给上亿行的表加字段 / 加索引
  • 直接 ALTER 会锁表几分钟到几小时
  • 业务不能停机
-- 加索引(默认就是 in-place,不锁表读写)
ALTER TABLE users ADD INDEX idx_phone(phone),
  ALGORITHM=INPLACE, LOCK=NONE;

-- 加字段(8.0+ 支持 INSTANT 算法,瞬间完成)
ALTER TABLE users ADD COLUMN nickname VARCHAR(50),
  ALGORITHM=INSTANT;

-- 算法选择
-- INSTANT  (8.0)  瞬时完成,只改元数据
-- INPLACE  原地修改,无需复制表,但要时间
-- COPY     复制表,最慢,会锁表
# 1. 加字段不锁表
pt-online-schema-change \
  --alter="ADD COLUMN age INT" \
  D=mydb,t=users \
  --execute

# 原理:
# 1. 创建影子表 _users_new (结构含新字段)
# 2. 在原表加触发器,记录后续变更
# 3. 分批从原表复制数据到新表
# 4. 触发器持续同步增量
# 5. 一瞬间 rename 完成切换

# 2. 主从延迟自适应
pt-online-schema-change \
  --alter="ADD INDEX idx_name(name)" \
  D=mydb,t=users \
  --max-lag=2 \
  --check-interval=2 \
  --execute
# 不用触发器,基于 binlog 同步
gh-ost \
  --user=root --password=xxx \
  --host=replica.host \
  --database="mydb" \
  --table="users" \
  --alter="ADD COLUMN age INT" \
  --execute

# 优势:可暂停可继续、读从库 binlog 不影响主库性能
切换瞬间还是会锁 · 即使是 Online DDL,最后 rename 的瞬间仍需要短暂的元数据锁(MDL)。这一刻如果有长事务,会一起卡住。所以重大变更建议在低峰期 + 提前 kill 长事务。
选型建议 · 简单加字段/加索引 → 原生 Online DDL 即可;复杂变更(改字段类型、重命名等)→ pt-osc 或 gh-ost。新业务尽量用 gh-ost,避免触发器副作用。
CONTINUE TO
Redis 故障排查手册 →