MySQL 原理 + 运维急救指南:从一条 SQL 看透数据库,特殊故障不再慌
前言
做运维和后端开发,你大概率经历过这些"惊魂时刻":
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
凌晨 3 点,告警群里弹出:
"生产数据库 CPU 100%!"
早上 9 点,你发现:
"为什么一条 SELECT 跑了一分钟?"
下午 2 点,业务反馈:
"订单状态不一致!"
深夜 11 点,老板来电:
"数据库主从挂了!"
你看着屏幕上满屏的 SQL 语句,想找一个准确的排查方向,
却发现自己对 MySQL 的原理"似懂非懂"。
|
MySQL 是怎么执行一条 SQL 的?事务是怎么保证不丢数据的?主从复制是怎么同步的?
不懂原理 → 永远是"哪里报错查哪里",永远是被动救火;
懂了原理 → 任何故障都能从根上推导出原因。
这篇文章,我会用一个比喻 + 实战故障两条线,让你彻底搞懂 MySQL:
1
2
3
4
5
6
7
8
|
第一条线(原理):
一条 SELECT 怎么从你敲下回车那一刻,
一路穿过连接器、分析器、优化器、执行器、InnoDB,
最终把数据交到你手里。
第二条线(实战):
运维中 10 类最常见的"诡异故障",
每一种告诉你:现象、原因、排查思路、解决方案。
|
读完后,你不仅能看懂 MySQL 文档,还能从容应对深夜告警。
第一部分:MySQL 原理
一、一条 SELECT 语句是怎么跑起来的
1.1 一个比喻:MySQL 是个大型快递站
为了不让你被一堆概念劝退,先把 MySQL 想成一个大型快递站:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
你(客户端)
│
│ 寄一个包裹:"SELECT * FROM user WHERE id=1"
│
▼
┌─────────────────────────────────────────┐
│ 快递站:MySQL │
│ │
│ 大门口:连接器(验证身份证) │
│ │ │
│ ▼ │
│ 客服台:分析器("你这个包裹要寄到哪?")│
│ │ │
│ ▼ │
│ 路线规划:优化器("走哪条路最快?") │
│ │ │
│ ▼ │
│ 仓库工人:执行器("去货架取货!") │
│ │ │
│ ▼ │
│ 货架本身:InnoDB("数据存放在哪") │
└─────────────────────────────────────────┘
│
▼
你收到包裹:"id=1, name=小明"
|
接下来我们一关一关走。
1.2 第一关:连接器(Connector)
1
2
3
|
你:SELECT * FROM user WHERE id=1
↑
建立连接(Connection)
|
连接器负责:
1
2
3
|
1. 验证身份(用户名 + 密码)
2. 验证权限(这个用户能 SELECT user 表吗?)
3. 建立连接(分配线程、内存)
|
关键点:每次连接都用一个独立线程,所以 MySQL 长连接用多了会内存爆炸。
1
2
3
4
5
6
7
8
9
|
# 查看当前连接
mysql> SHOW PROCESSLIST;
+----+------+-----------+------+---------+------+-------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+------+-----------+------+---------+------+-------+------------------+
| 5 | app | 10.0.0.1 | shop | Query | 0 | init | SHOW PROCESSLIST |
| 8 | app | 10.0.0.1 | shop | Sleep | 120 | | |
| 10 | app | 10.0.0.1 | shop | Query | 3 | | SELECT * FROM... |
+----+------+-----------+------+---------+------+-------+------------------+
|
实战建议:连接池大小 = (CPU 核心数 × 2) + 有效硬盘数。不是越大越好!
1.3 第二关:分析器(Parser)
连接成功后,MySQL 看到 SELECT * FROM user WHERE id=1,先要"读懂"这句话。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
分析器做的事情:
1. 词法分析
SELECT → 关键字
* → 通配符
FROM → 关键字
user → 表名
WHERE → 关键字
id=1 → 条件表达式
2. 语法分析
检查语法对不对(你写的 SQL 是否符合 MySQL 语法规范)
如果写错了,会报错:
ERROR 1064 (42000): You have an error in your SQL syntax
|
记住:MySQL 的"语法错误"就是这一关抛出的。
1.4 第三关:优化器(Optimizer)
1
2
3
4
5
6
7
8
|
SQL 写好了,但"怎么执行最快"还没定。
比如 SELECT * FROM user WHERE id=1 AND name='张三'
可能的执行方式:
A. 先用 id=1 找到用户(id 有索引),再判断 name='张三'
B. 先用 name='张三' 找到用户(name 有索引),再判断 id=1
哪个更快?优化器决定。
|
优化器干的事:
1
2
3
4
5
|
1. 决定用哪个索引
2. 决定表的连接顺序(多表 JOIN 时)
3. 决定 SQL 的执行计划
→ 优化器输出一个"执行计划"(EXPLAIN)
|
1
2
3
4
5
6
7
8
|
-- 看 SQL 的执行计划
EXPLAIN SELECT * FROM user WHERE id=1;
+----+-------------+-------+-------+-------------+---------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+-------------+---------+---------+-------+------+-------+
| 1 | SIMPLE | user | const | PRIMARY | PRIMARY | 4 | const | 1 | |
+----+-------------+-------+-------+-------------+---------+---------+-------+------+-------+
|
type = const 表示直接通过主键查到,最高效。
1.5 第四关:执行器(Executor)
1
2
3
4
5
6
|
拿到执行计划后,执行器"真正执行":
1. 打开表
2. 调用 InnoDB 引擎读数据
3. 逐行判断 WHERE 条件
4. 返回结果给客户端
|
1
2
3
4
5
6
7
8
9
10
11
12
|
-- 查看执行器工作状态
mysql> SHOW STATUS LIKE 'Handler%';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| Handler_read_first | 1 |
| Handler_read_key | 1 |
| Handler_read_next | 0 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 1 |
+----------------------------+-------+
|
1.6 第五关:InnoDB 存储引擎
InnoDB 才是 MySQL 真正存数据的地方。
1
2
3
4
5
6
7
|
InnoDB 的核心组件:
├── Buffer Pool(缓冲池):内存中的"数据货架"
├── Undo Log(回滚日志):事务回滚用
├── Redo Log(重做日志):崩溃恢复用
├── Binlog(归档日志):主从复制用
├── B+ 树索引:数据组织方式
└── 行锁 / 间隙锁:并发控制
|
二、InnoDB:MySQL 的心脏
2.1 数据是怎么存进 InnoDB 的
1
2
3
4
5
|
表空间 (Tablespace)
└── 段 (Segment)
└── 区 (Extent) :1MB = 64 个页
└── 页 (Page) :16KB(InnoDB 最小 IO 单位)
└── 行 (Row) :真正存数据
|
关键点:InnoDB 以 16KB 的页为单位读写数据,不是以"行"为单位。
1
2
3
4
5
6
7
|
# 查看 innodb_page_size
mysql> SHOW VARIABLES LIKE 'innodb_page_size';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| innodb_page_size | 16384 |
+---------------+-------+
|
2.2 B+ 树索引:为什么查询快
假设表里有 100 万行数据,没有索引,查询一条记录需要 O(n)?
1
2
3
4
5
6
7
8
9
10
11
12
13
|
SELECT * FROM user WHERE id=1;
没有索引:
→ 全表扫描,逐行检查
→ 最坏情况读 100 万行(每次读 16KB 一页)
→ 共读 ~16GB 的 IO
→ 慢!
有索引(主键索引):
→ B+ 树查找
→ 3 层 B+ 树(约 100 万行)只需 3 次 IO
→ 共读 48KB
→ 快!
|
B+ 树结构(InnoDB 默认索引):
1
2
3
4
5
6
7
8
9
|
根节点(Root):存 key 和指针
│
├── 内部节点(Internal):存 key 和指针
│ │
│ ├── 叶子节点(Leaf):存数据
│ ├── 叶子节点(Leaf):存数据
│ └── 叶子节点(Leaf):存数据
│
叶子节点之间通过指针连起来(这就是"范围查询快"的原因)
|
索引分类:
1
2
3
4
5
6
7
8
9
|
聚簇索引(Clustered Index):
- 主键索引就是聚簇索引
- 数据行就存在叶子节点
- 一张表只有一个聚簇索引
非聚簇索引(Secondary Index):
- 普通索引(KEY index_name)
- 叶子节点存主键值,不存完整数据
- 查询需要"回表":先查非聚簇索引拿到主键,再去聚簇索引查完整数据
|
这就是为什么"主键查询快、普通索引查询慢":
1
2
3
4
5
|
SELECT * FROM user WHERE id=1
→ 主键索引,叶子节点就是数据 → 不需要回表
SELECT * FROM user WHERE name='小明'
→ 普通索引,叶子节点是 id=1 → 回表去聚簇索引拿完整数据
|
2.3 Buffer Pool:内存里的数据货架
1
2
|
InnoDB 把磁盘上的"页(16KB)"读到内存的 Buffer Pool 里缓存。
下次再读同一页,直接从内存读,速度提升 ~1000 倍(内存 vs 磁盘)。
|
1
2
3
4
5
6
7
8
9
10
11
|
Buffer Pool 的工作机制:
读取数据:
1. 先查 Buffer Pool
2. 命中 → 直接返回(速度极快)
3. 未命中 → 从磁盘加载到 Buffer Pool,再返回
修改数据:
1. 修改 Buffer Pool 中的页(内存中修改)
2. 标记为"脏页"(dirty page)
3. 后台线程异步刷回磁盘
|
1
2
3
4
5
6
7
8
9
10
|
-- 查看 Buffer Pool 状态
mysql> SHOW ENGINE INNODB STATUS\G
BUFFER POOL AND MEMORY
----------------------
Buffer pool size 8192 ← 页数(每页 16KB = 128MB)
Free buffers 1024
Database pages 7000
Old database pages 2580
Modified db pages 15 ← 脏页数
|
Buffer Pool 配置建议:
1
2
3
4
5
6
7
8
9
|
# my.cnf 推荐配置
[mysqld]
# 服务器内存的 50-70% 分配给 Buffer Pool
innodb_buffer_pool_size = 8G
# 多实例(高并发用)
innodb_buffer_pool_instances = 4
# 预热(重启时自动加载热点数据)
innodb_buffer_pool_load_at_startup = 1
innodb_buffer_pool_dump_at_shutdown = 1
|
2.4 三个关键日志:Undo Log / Redo Log / Binlog
这是 InnoDB 的核心机制,理解了它们就理解了事务和复制。
Undo Log(回滚日志)
1
2
3
4
5
6
|
事务回滚用:
BEGIN;
UPDATE user SET name='李四' WHERE id=1;
-- 此时 InnoDB 在 Undo Log 里记下:id=1 原来 name='张三'
ROLLBACK;
-- 事务回滚时,从 Undo Log 恢复 name='张三'
|
Undo Log 还在 MVCC 中起到关键作用(后面讲)。
Redo Log(重做日志)
1
2
3
4
5
6
7
|
崩溃恢复用:
BEGIN;
UPDATE user SET name='李四' WHERE id=1;
-- InnoDB 把"id=1, name=张三 → 李四"这个修改记到 Redo Log
-- 然后修改 Buffer Pool 中的页
COMMIT;
-- 标记 Redo Log 已提交
|
1
2
3
4
5
|
崩溃发生时的恢复:
1. MySQL 重启
2. 扫描 Redo Log
3. 把已提交但还没刷到磁盘的数据,重新刷回磁盘
4. 保证数据不丢
|
这就是 WAL(Write-Ahead Logging)思想:先写日志,再写磁盘。
1
2
3
4
5
|
# Redo Log 配置
[mysqld]
innodb_log_file_size = 1G # 每个日志文件大小
innodb_log_files_in_group = 2 # 日志文件数量(共 2G Redo Log)
innodb_flush_log_at_trx_commit = 1 # 事务提交立刻刷 Redo Log
|
innodb_flush_log_at_trx_commit 三种策略:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
= 1(最安全):
每次事务提交,都把 Redo Log 立即刷到磁盘
优点:即使断电,0 数据丢失
缺点:性能最差(每次提交多一次 IO)
= 2(性能好,安全性略低):
事务提交时,Redo Log 写入 OS 缓存
每秒刷一次磁盘
优点:性能好
缺点:断电可能丢失最后 1 秒的数据
= 0(性能最好,最不安全):
每秒刷一次 Redo Log 到磁盘
优点:性能最佳
缺点:断电可能丢失最后 1 秒的数据
|
Binlog(归档日志)
1
2
3
4
|
主从复制用:
主库:所有变更都写 Binlog
从库:拉取主库的 Binlog,重放到自己机器上
→ 实现数据同步
|
Binlog 三种格式:
1
2
3
4
5
6
7
8
9
10
11
12
|
-- 查看 Binlog 格式
mysql> SHOW VARIABLES LIKE 'binlog_format';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW |
+---------------+-------+
-- 三种格式对比
ROW :记录每行的具体变更(最安全,数据一致性好,默认推荐)
STATEMENT:记录 SQL 语句(最小,但有不一致风险)
MIXED :混合模式(MySQL 自动选择)
|
2.5 三个日志的关系:两阶段提交
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
事务提交时,三个日志是怎么配合的?
BEGIN;
UPDATE user SET name='李四' WHERE id=1;
COMMIT 触发以下流程:
1. 写 Redo Log(prepare 状态)
2. 写 Binlog
3. 写 Redo Log(commit 状态)
→ 这就是"两阶段提交"
崩溃发生在哪一步?
1 之前:数据不变
1 后、2 之前:Redo Log prepare 状态,丢弃这个事务
2 后、3 之前:Binlog 有记录,Redo Log 没有 commit → 提交事务
3 之后:完整提交
|
1
2
3
4
5
6
7
8
9
10
11
|
-- 查看 Binlog 内容
mysql> SHOW BINLOG EVENTS IN 'mysql-bin.000001';
-- 查看 Binlog 文件列表
mysql> SHOW BINARY LOGS;
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000001 | 154 |
| mysql-bin.000002 | 10240 |
+------------------+-----------+
|
三、事务与隔离级别
3.1 ACID 四大特性
1
2
3
4
5
6
7
8
|
A 原子性(Atomicity) :事务要么全成功,要么全失败
→ 靠 Undo Log
C 一致性(Consistency) :事务前后数据合法
→ 靠应用层 + 数据库约束
I 隔离性(Isolation) :事务之间互不干扰
→ 靠锁 + MVCC
D 持久性(Durability) :事务一旦提交,数据不丢
→ 靠 Redo Log + Binlog
|
3.2 隔离级别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
ANSI SQL 标准定义的 4 个隔离级别(由弱到强):
READ UNCOMMITTED(读未提交)
→ 能读到其他事务未提交的数据(脏读)
→ 几乎不用
READ COMMITTED(读已提交,RC)
→ 只能读到已提交的数据
→ Oracle / PostgreSQL 默认
REPEATABLE READ(可重复读,RR)
→ 同一事务中,多次读同样数据结果一致
→ MySQL InnoDB 默认 ⭐
SERIALIZABLE(串行化)
→ 强制串行执行
→ 性能最差,最安全
|
3.3 隔离级别解决的并发问题
1
2
3
4
5
|
脏读 不可重复读 幻读
READ UNCOMMITTED 有 有 有
READ COMMITTED 无 有 有
REPEATABLE READ 无 无 有 (InnoDB 通过 MVCC + 间隙锁避免)
SERIALIZABLE 无 无 无
|
关键:InnoDB 的 REPEATABLE READ 通过 MVCC + 间隙锁,几乎完美避免了幻读。
3.4 MVCC 多版本并发控制
1
2
3
4
5
6
7
8
9
10
11
12
|
MVCC = 每行数据有多个版本
读不加锁,读写不冲突
具体实现:
每行有两个隐藏字段:
DB_TRX_ID : 创建或修改该行的事务 ID
DB_ROLL_PTR: 指向 Undo Log 的指针
一致性读:
每次读都基于"读视图(Read View)"
只读在 Read View 之前已提交的数据
不读未提交的数据
|
举例:
1
2
3
4
5
6
7
|
-- 事务 A(RR 隔离)
BEGIN;
SELECT * FROM user WHERE id=1; -- 读到 name='张三'
-- 此时事务 B 已经修改并提交了 name='李四'
SELECT * FROM user WHERE id=1; -- 仍然读到 name='张三'(可重复读)
COMMIT;
-- 事务 A 结束后再读,读到 name='李四'
|
四、锁机制:InnoDB 的并发控制
4.1 锁的种类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
行锁(Record Lock):
锁定单行数据
InnoDB 默认行级锁
间隙锁(Gap Lock):
锁定一个范围,但不包括记录本身
例如:WHERE id BETWEEN 10 AND 20,锁定 (10, 20) 区间
Next-Key Lock(行锁 + 间隙锁):
默认加锁方式
既锁当前行,也锁前面的间隙
防止"幻读"
表锁:
LOCK TABLES user WRITE;
整张表加锁,性能差
MyISAM 默认是表锁
|
4.2 死锁:常见且必须理解
1
2
3
4
5
6
7
8
|
事务 1: 事务 2:
BEGIN; BEGIN;
UPDATE user SET ... WHERE id=1;
UPDATE user SET ... WHERE id=2;
UPDATE user SET ... WHERE id=2;
UPDATE user SET ... WHERE id=1;
-- 死锁!等待对方释放
-- 死锁!等待对方释放
|
InnoDB 会自动检测死锁,并回滚其中一个事务:
1
2
|
ERROR 1213 (40001): Deadlock found when trying to get lock;
try restarting transaction
|
解决死锁的实战技巧:
1
2
3
4
5
6
7
8
9
10
11
|
1. 按固定顺序加锁
(所有事务都先锁 id=1 再锁 id=2)
2. 减小事务范围
(大事务拆成小事务)
3. 减少并发
(降低同时运行的写入事务数)
4. 设置锁等待超时
SET innodb_lock_wait_timeout = 5
|
4.3 查看锁等待
1
2
3
4
5
6
7
8
9
10
11
12
13
|
-- MySQL 8.0+
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
-- 查谁在锁谁
SELECT
blocking_trx_id,
blocking_pid,
waiting_trx_id,
waiting_pid,
waiting_query
FROM performance_schema.data_lock_waits
JOIN performance_schema.threads ...
|
1
2
3
4
|
-- MySQL 5.7
SELECT * FROM information_schema.INNODB_TRX\G
SELECT * FROM information_schema.INNODB_LOCKS\G
SELECT * FROM information_schema.INNODB_LOCK_WAITS\G
|
五、主从复制原理
5.1 为什么需要主从复制
1
2
3
4
|
1. 读写分离:主库写,从库读 → 提升读性能
2. 数据备份:从库是主库的实时备份
3. 高可用:主库挂了,从库切换顶替
4. 数据分析:报表、统计在从库跑,不影响主库
|
5.2 主从复制的原理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
主库(Master) 从库(Slave)
│ │
│ 1. 主库写数据 → 写 Binlog │
│ │
│ 2. Binlog Dump Thread │
│ 把 Binlog 推给从库 │
│ ────────────────────────────→│
│ │
│ 3. Slave IO Thread
│ 接收 Binlog,写到 Relay Log
│ │
│ 4. Slave SQL Thread
│ 读 Relay Log,重放到从库
│ │
|
1
2
3
4
5
6
7
8
9
10
11
12
|
主从复制三线程:
Master Binlog Dump Thread:
- 主库启动一个线程
- 负责把 Binlog 推给从库
Slave IO Thread:
- 从库启动一个线程
- 接收主库 Binlog,写入本地 Relay Log
Slave SQL Thread:
- 从库启动一个线程
- 读取 Relay Log,执行 SQL 重放
|
5.3 主从复制延迟
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
原因:
- 主库高并发写入
- 从库单线程重放(MySQL 5.7 之前)
- 网络延迟
- 从库硬件差
解决方案:
1. MySQL 5.7+ 开启多线程复制
SET GLOBAL slave_parallel_workers = 8;
2. MySQL 8.0 开启 writeset 多线程
SET GLOBAL binlog_transaction_dependency_tracking = WRITESET;
3. 升级到 MySQL 8.0+(默认就是并行复制)
|
5.4 GTID 复制(推荐)
1
2
3
4
5
|
传统复制:基于 Binlog 文件名 + 位点(容易出错)
GTID 复制:基于全局事务 ID(更可靠)
- 每个事务有唯一 ID
- 主从自动协商从哪同步
- 自动跳过已执行的事务
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# 主库配置
[mysqld]
gtid_mode = ON
enforce_gtid_consistency = ON
log_bin = mysql-bin
# 从库配置
[mysqld]
gtid_mode = ON
enforce_gtid_consistency = ON
log_bin = mysql-bin
# 从库 CHANGE MASTER TO
CHANGE MASTER TO
MASTER_HOST='192.168.1.100',
MASTER_USER='repl',
MASTER_PASSWORD='password',
MASTER_AUTO_POSITION=1; # GTID 模式
|
第二部分:MySQL 运维急救指南
六、特殊故障 10 类速查
每类故障包含:现象 / 原因 / 排查 / 解决
故障 1:连接数爆满,应用报 “Too many connections”
现象:
1
|
ERROR 1040 (HY000): Too many connections
|
原因:
1
2
|
应用连接没释放(没显式调用 close() 或自动断连)
或某条慢查询把所有连接都占着
|
排查:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
-- 1. 查看连接总数
mysql> SHOW STATUS LIKE 'Threads_connected';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_connected | 998 | ← 已经接近上限 1000
+-------------------+-------+
-- 2. 查看每个连接在干什么
mysql> SHOW PROCESSLIST;
-- 3. 看连接配置
mysql> SHOW VARIABLES LIKE 'max_connections';
+-----------------+-------+
| Variable_name | Value |
+-----------------+-------+
| max_connections | 1000 |
+-----------------+-------+
-- 4. 查看连接池使用情况
mysql> SHOW STATUS LIKE 'Max_used_connections%';
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| Max_used_connections | 1001 | ← 曾经爆满过
| Max_used_connections_time | 14:23:01|
+------------------------------+-------+
|
解决方案:
1
2
3
4
5
6
7
8
9
10
11
|
-- 紧急:临时提高上限
SET GLOBAL max_connections = 2000;
-- 根本:排查没释放的连接
-- 1. 应用代码检查连接池配置(确保 close 释放)
-- 2. 设置超时
SET GLOBAL wait_timeout = 300; -- 空闲连接 5 分钟超时
SET GLOBAL interactive_timeout = 300;
-- 3. 配置连接池大小
-- HikariCP: maximumPoolSize = 20~50
|
故障 2:一条 SQL 突然跑得很慢
现象:
排查:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
-- 1. 看是不是索引失效
EXPLAIN SELECT * FROM user WHERE name='小明';
-- 输出 type=ALL → 全表扫描!
+----+-------------+-------+------+---------------+------+---------+------+------+----------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+----------+
| 1 | SIMPLE | user | ALL | NULL | NULL | NULL | NULL | 100万 | Using where|
+----+-------------+-------+------+---------------+------+---------+------+------+----------+
-- 2. 看表是不是被锁
SELECT * FROM information_schema.INNODB_LOCK_WAITS\G
-- 3. 看是不是 Buffer Pool 命中率下降
mysql> SHOW STATUS LIKE 'Innodb_buffer_pool%';
+---------------------------------------+-------+
| Variable_name | Value |
+---------------------------------------+-------+
| Innodb_buffer_pool_read_requests | 1000000|
| Innodb_buffer_pool_reads | 50000 | ← 磁盘读
+---------------------------------------+-------+
-- 命中率 = (读请求 - 磁盘读) / 读请求 = 95% 正常,低于 90% 异常
|
解决方案:
1
2
3
4
5
6
7
8
|
-- 1. 添加合适的索引
CREATE INDEX idx_name ON user(name);
-- 2. 分析表,更新统计信息
ANALYZE TABLE user;
-- 3. 强制走索引(极端情况)
SELECT * FROM user FORCE INDEX (idx_name) WHERE name='小明';
|
故障 3:磁盘突然满了,数据库挂掉
现象:
1
2
|
ERROR 1114 (HY000): The table 'user' is full
ERROR 1030 (HY000): Got error 28 from storage engine
|
排查:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# 1. 查磁盘
df -h
# /dev/sda1 100G 100G 0G 100% /data
# 2. 查大文件
du -sh /var/lib/mysql/* | sort -h | tail -20
# 3. 查 Binlog
ls -lh /var/lib/mysql/mysql-bin.*
# -rw-r----- 1 mysql mysql 1.1G mysql-bin.000001
# -rw-r----- 1 mysql mysql 1.1G mysql-bin.000002
# ...
# 4. 查数据目录
du -sh /var/lib/mysql/ibdata1 # 系统表空间
du -sh /var/lib/mysql/your_db/* # 各库文件
|
解决方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# 1. 紧急清理 Binlog(保留最近 7 天)
mysql> PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY);
# 2. 或立即清理到指定 Binlog
mysql> PURGE BINARY LOGS TO 'mysql-bin.000050';
# 3. 防止 Binlog 无限增长,配置自动过期
cat /etc/my.cnf
[mysqld]
expire_logs_days = 7 # MySQL 5.7
binlog_expire_logs_seconds = 604800 # MySQL 8.0(7 天)
# 4. 收缩 ibdata1(系统表空间)
# 这条不能在线做!
# MySQL 8.0+ 使用独立表空间 innodb_file_per_table=1(默认)
# 5. 删表前先 TRUNCATE(释放空间)
# 直接 DROP TABLE 不一定释放磁盘空间给 OS
TRUNCATE TABLE temp_data;
|
故障 4:主从复制断了
现象:
1
2
|
Last_SQL_Error: Could not execute Write_rows event
Duplicate entry '12345' for key 'PRIMARY'
|
排查:
1
2
|
-- 在从库上
mysql> SHOW SLAVE STATUS\G
|
关键字段:
1
2
3
4
|
Slave_IO_Running: Yes ← IO 线程正常
Slave_SQL_Running: No ← SQL 线程挂了!
Last_SQL_Error: ... ← 看错误信息
Seconds_Behind_Master: NULL ← 落后秒数
|
解决方案(不同错误类型):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
-- 情况 1:主键冲突
-- 跳过这条错误(数据一致性自己负责!)
SET GLOBAL sql_slave_skip_counter = 1;
START SLAVE SQL_THREAD;
-- 情况 2:中继日志损坏
STOP SLAVE;
CHANGE MASTER TO RELAY_LOG_FILE='...', RELAY_LOG_POS=...;
START SLAVE;
-- 情况 3:彻底重新同步
STOP SLAVE;
RESET SLAVE ALL;
CHANGE MASTER TO MASTER_HOST='...', MASTER_AUTO_POSITION=1;
START SLAVE;
-- 情况 4:GTID 跳过错误
SET GTID_NEXT = 'uuid:transaction_id';
BEGIN; COMMIT;
SET GTID_NEXT = 'AUTOMATIC';
START SLAVE;
|
最稳的方案:用 pt-table-checksum 同步
1
2
3
4
5
6
|
# Percona Toolkit 工具
# 检查数据一致性
pt-table-checksum --host=master --user=root --password=xxx --databases=your_db
# 修复不一致
pt-table-sync --execute --sync-to-master --host=slave --databases=your_db
|
故障 5:主从同步延迟
现象:
1
|
从库落后主库 1 小时(Seconds_Behind_Master: 3600)
|
排查:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
-- 查看延迟时间
mysql> SHOW SLAVE STATUS\G
Seconds_Behind_Master: 3600
-- 查看复制性能
mysql> SHOW STATUS LIKE 'Slave%';
+----------------------------+----------+
| Variable_name | Value |
+----------------------------+----------+
| Slave_running | ON |
| Slave_retried_transactions | 0 |
| Slave_lag_time | 00:00:00 | ← MySQL 8.0 才有
+----------------------------+----------+
|
解决方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
-- 1. 开启并行复制(MySQL 8.0 默认开启)
SET GLOBAL slave_parallel_workers = 8;
SET GLOBAL slave_parallel_type = LOGICAL_CLOCK;
-- 2. MySQL 8.0 writeset 并行
SET GLOBAL binlog_transaction_dependency_tracking = WRITESET;
-- 3. 拆分大事务
-- 一个事务包含 100 万行 UPDATE → 延迟巨大
-- 拆成 100 个事务,每个 1 万行 → 延迟降低
-- 4. 升级从库硬件
-- 从库 IO 慢 → 换 SSD
-- 从库 CPU 忙 → 加核
|
故障 6:表损坏(crash 后)
现象:
1
|
ERROR 144 (HY000): Table './shop/order' is marked as crashed
|
排查:
1
2
3
4
5
6
7
8
|
# 1. 检查哪些表损坏
mysqlcheck --all-databases --check
# 2. 详细检查
myisamchk --check /var/lib/mysql/shop/order.MYI
# InnoDB 表一般会自动恢复
# 但极端情况下可能有问题
|
解决方案:
1
2
3
4
5
6
7
8
9
|
# 1. MyISAM 表(简单粗暴)
myisamchk -r /var/lib/mysql/shop/order.MYI
# 2. InnoDB 表(重启数据库自动恢复)
# 如果恢复失败,进入强制恢复模式
cat /etc/my.cnf
[mysqld]
innodb_force_recovery = 1 # 从 1 开始试,最高 6
# 6 表示完全忽略错误,但可能丢数据
|
1
2
3
4
5
6
7
|
# innodb_force_recovery 取值:
1 (SRV_FORCE_IGNORE_CORRUPT):忽略检查到的 corrupt 页
2 (SRV_FORCE_NO_BACKGROUND):阻止主线程的运行
3 (SRV_FORCE_NO_TRX_UNDO):不执行事务回滚
4 (SRV_FORCE_NO_IBUF_MERGE):不执行插入缓冲合并
5 (SRV_FORCE_NO_UNDO_LOG_SCAN):不查看 undo log
6 (SRV_FORCE_NO_LOG_REDO):不执行 redo log 前滚
|
故障 7:ibd 文件丢失或误删(独立表空间)
现象:
1
|
数据库里有 user 表,但 user.ibd 文件被删了
|
解决方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# 1. 立即停止数据库
systemctl stop mysqld
# 2. 不允许再用 user 表(备份 .frm 文件,丢弃 .ibd)
# 3. 重启数据库
systemctl start mysqld
# 4. 重建表
mysqldump --skip-lock-tables --skip-extended-insert \
--no-create-info -uroot -p your_db user > user_data.sql
mysql -uroot -p your_db < create_table.sql # 重新建表
mysql -uroot -p your_db < user_data.sql # 导入数据
|
如果 .ibd 和 .frm 都丢:
1
2
3
4
5
|
# 1. 从 Binlog 恢复数据(最差情况)
mysqlbinlog mysql-bin.000001 | mysql -uroot -p
# 2. 从备份恢复(推荐)
# 用 xtrabackup / mysqldump 定期备份
|
故障 8:无法启动 MySQL
排查:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
# 1. 看错误日志(最重要)
tail -100 /var/log/mysqld.log
tail -100 /var/log/mysql/error.log
# 2. 常见错误信息:
# "Can't start server: Bind on TCP/IP port: Address already in use"
# → 端口被占用,netstat -lnp | grep 3306 找出占用进程
# "Permission denied"
# → datadir 权限不对,chown -R mysql:mysql /var/lib/mysql
# "Table 'mysql.plugin' doesn't exist"
# → mysql 初始化没完成,mysql_install_db
# 3. 检查端口
netstat -lnp | grep 3306
lsof -i :3306
# 4. 检查磁盘
df -h /var/lib/mysql
# 5. 检查内存
free -h
# 可能 OOM 被杀
dmesg | grep -i "killed process"
|
解决方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# 重置 root 密码(如果忘记)
# 1. 停掉 mysqld
systemctl stop mysqld
# 2. 跳过授权启动
mysqld --skip-grant-tables --skip-networking &
# 3. 修改密码
mysql> FLUSH PRIVILEGES;
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'NewPassword';
# 4. 重启
systemctl restart mysqld
|
故障 9:事务长时间没提交,锁堆积
现象:
1
2
|
应用反馈:"订单处理卡住了"
数据库中:"事务 12345 一直未提交"
|
排查:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-- 1. 看所有活跃事务
mysql> SELECT * FROM information_schema.INNODB_TRX\G
*************************** 1. row ***************************
trx_id: 12345
trx_state: RUNNING
trx_started: 2026-07-25 14:23:01
trx_wait_started: NULL
trx_mysql_thread_id: 8
trx_query: UPDATE order SET status='paid' WHERE id=10001
-- 2. 看这个事务持有什么锁
mysql> SELECT * FROM information_schema.INNODB_LOCKS WHERE LOCK_TRX_ID = 12345;
-- 3. 看是不是阻塞了别人
mysql> SELECT * FROM information_schema.INNODB_LOCK_WAITS;
|
解决方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
-- 1. 如果是 KILL 应用的连接
-- 找到 trx_mysql_thread_id (如 8)
KILL 8;
-- 2. 如果是开发错误(事务中包含了非 DB 操作)
-- 重构应用代码:
-- BAD:
-- BEGIN;
-- UPDATE order SET ...;
-- sleep(10); -- 业务逻辑里 sleep,事务一直开着!
-- UPDATE order SET ...;
-- COMMIT;
-- GOOD:
-- sleep(10); -- 把 sleep 放事务外
-- BEGIN;
-- UPDATE order SET ...;
-- UPDATE order SET ...;
-- COMMIT;
-- 3. 设置超时机制
SET GLOBAL innodb_lock_wait_timeout = 10;
|
故障 10:备份失败,误删数据
现象:
处理流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# 1. 不要慌,停止写入(避免覆盖 Binlog)
# 2. 找最近的备份
# 用 mysqldump / xtrabackup / 快照 等
# 3. 用 Binlog 恢复(精确到秒)
mysqlbinlog --start-datetime='2026-07-25 14:00:00' \
--stop-datetime='2026-07-25 14:30:00' \
mysql-bin.000001 | mysql -uroot -p
# 4. 用 flashback 工具回滚(针对 DELETE 误操作)
# GitHub: binlog2sql / MyFlash
python binlog2sql.py -u root -p xxx \
--start-file='mysql-bin.000001' \
--start-position=12345 \
--stop-position=67890 \
--sql-type=DELETE \
--flashback
|
预防措施:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# 1. 启用 Binlog(必做)
[mysqld]
log_bin = mysql-bin
binlog_format = ROW
expire_logs_days = 30 # Binlog 至少保留 30 天
# 2. 启用 GTID(推荐)
gtid_mode = ON
enforce_gtid_consistency = ON
# 3. 定期全量备份
# xtrabackup 每天全备
0 3 * * * /usr/local/bin/xtrabackup_full.sh
# 4. Binlog 备份到独立存储
0 4 * * * rsync -av /var/lib/mysql/mysql-bin.* backup@backup-server:/mysql-bin/
# 5. 限速 DROP / TRUNCATE(运维侧)
-- 应用代码不开放 DROP 权限,使用专用账户
-- 主库禁用 DROP 权限:REVOKE DROP ON *.* FROM 'app'@'%'
|
七、性能优化速查
7.1 慢查询排查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
-- 1. 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 记录超过 1 秒的
-- 2. 看慢查询
mysql> SHOW VARIABLES LIKE 'slow_query_log_file';
+---------------------+----------------------------------+
| Variable_name | Value |
+---------------------+----------------------------------+
| slow_query_log_file | /var/lib/mysql/slow.log |
+---------------------+----------------------------------+
-- 3. 用 mysqldumpslow 分析
mysqldumpslow -t 10 /var/lib/mysql/slow.log
-- 4. 用 pt-query-digest 分析(更专业)
pt-query-digest /var/lib/mysql/slow.log > report.txt
|
7.2 索引优化原则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
-- 1. WHERE 字段建索引
-- BAD: SELECT * FROM user WHERE name='小明'
-- GOOD: ALTER TABLE user ADD INDEX idx_name (name);
-- 2. 联合索引顺序:左前缀原则
-- 索引 (a, b, c) 可以加速:
-- WHERE a = ? ✓
-- WHERE a = ? AND b = ? ✓
-- WHERE a = ? AND b = ? AND c = ? ✓
-- WHERE b = ? ✗ (无法使用索引)
-- 3. 索引选择性
-- 区分度高的列放前面
-- 性别(0/1)放最后,姓名(百万种)放前面
-- 4. 避免 SELECT *
-- 只查需要的列,减少回表
-- 5. 函数/运算在 WHERE 上索引失效
-- BAD: WHERE YEAR(create_time) = 2026
-- GOOD: WHERE create_time >= '2026-01-01' AND create_time < '2027-01-01'
|
7.3 关键配置推荐(my.cnf)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
[mysqld]
# === 基础配置 ===
port = 3306
datadir = /var/lib/mysql
socket = /var/lib/mysql/mysql.sock
server-id = 1
# === 连接 ===
max_connections = 1000
max_user_connections = 800
wait_timeout = 300
interactive_timeout = 300
# === 缓冲池(最重要) ===
innodb_buffer_pool_size = 服务器内存的 60-70%
innodb_buffer_pool_instances = 4
innodb_buffer_pool_load_at_startup = 1
innodb_buffer_pool_dump_at_shutdown = 1
# === 日志 ===
innodb_log_file_size = 1G
innodb_log_files_in_group = 2
innodb_flush_log_at_trx_commit = 1
log_bin = mysql-bin
binlog_format = ROW
expire_logs_days = 14
# === 慢查询 ===
slow_query_log = ON
long_query_time = 1
log_queries_not_using_indexes = ON
# === 文件 ===
innodb_file_per_table = 1
innodb_open_files = 65535
# === 复制(主库) ===
server-id = 1
log_bin = mysql-bin
gtid_mode = ON
enforce_gtid_consistency = ON
# === 复制(从库) ===
server-id = 2
relay_log = relay-bin
log_slave_updates = ON
read_only = ON
|
7.4 实战性能排查命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
-- 1. 当前线程
SHOW FULL PROCESSLIST;
-- 2. 实时等待事件(MySQL 8.0)
SELECT * FROM performance_schema.events_statements_history
ORDER BY EVENT_ID DESC LIMIT 10;
-- 3. 各表 IO 情况
SELECT * FROM sys.io_global_by_file_by_latency;
-- 4. 各表占用空间
SELECT
TABLE_SCHEMA,
TABLE_NAME,
TABLE_ROWS,
DATA_LENGTH / 1024 / 1024 AS data_mb,
INDEX_LENGTH / 1024 / 1024 AS index_mb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA NOT IN ('mysql', 'sys', 'information_schema', 'performance_schema')
ORDER BY DATA_LENGTH DESC
LIMIT 20;
-- 5. 未使用索引
SELECT * FROM sys.schema_unused_indexes;
-- 6. 慢查询 Top 10
SELECT * FROM sys.statement_analysis
ORDER BY total_latency DESC LIMIT 10;
|
八、运维检查清单
每日检查
1
2
3
4
5
6
7
8
9
10
11
|
□ 备份是否成功
$ ls -lh /backup/mysql/
□ 主从延迟是否正常
$ mysql -e "SHOW SLAVE STATUS\G" | grep Seconds_Behind
□ Binlog 是否堆积
$ ls /var/lib/mysql/mysql-bin.* | wc -l
□ Buffer Pool 命中率
$ mysql -e "SHOW STATUS LIKE 'Innodb_buffer_pool_read%';"
|
每周检查
1
2
3
4
5
6
7
8
|
□ 慢查询分析
$ pt-query-digest /var/lib/mysql/slow.log
□ 磁盘空间预测
$ du -sh /var/lib/mysql/
□ 表膨胀检查
$ mysql -e "SHOW TABLE STATUS\G" | grep -E "Data_free"
|
每月检查
1
2
3
4
5
6
7
|
□ 全量备份演练(恢复测试)
□ Binlog 远程备份验证
□ 主从切换演练
□ 慢查询 review 和 SQL 优化
|
九、几个核心思想总结
9.1 写操作的"三段式"
1
2
3
4
5
6
7
|
任何 UPDATE/INSERT/DELETE:
1. 先写 Undo Log(用于回滚)
2. 再写 Redo Log(prepare)
3. 再写 Binlog
4. Redo Log commit
崩溃时根据这三个日志的状态决定是否恢复事务
|
9.2 读操作的"Buffer Pool 优先"
1
2
3
4
5
6
|
读数据:
1. 先查 Buffer Pool(内存)
2. 命中直接返回
3. 不命中从磁盘加载
→ Buffer Pool 命中率应该 > 95%
|
9.3 复制的"两日志 + 两线程"
1
2
3
4
|
主库写 Binlog
从库 IO Thread 拉 Binlog 写到 Relay Log
从库 SQL Thread 读 Relay Log 重放
→ 三步完成主从同步
|
9.4 故障处理的"三步法"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
遇到任何 MySQL 故障,按这个顺序:
1. 看现象
- 错误信息
- 监控指标
- 性能数据
2. 找原因
- 看慢查询
- 看锁等待
- 看磁盘/内存/CPU
- 看主从状态
3. 解决问题
- 临时止血(KILL 慢查询 / 扩容 / 重启)
- 根本修复(加索引 / 调配置 / 修代码)
- 预防复发(监控告警 / 限流 / 重构)
|
结语
回顾整篇文章,我们讲了两条线:
1
2
3
4
5
6
7
8
9
10
|
原理线:
一条 SELECT 怎么从你敲下回车那一刻,
一路穿过连接器、分析器、优化器、执行器、InnoDB,
最终把数据交到你手里。
→ 懂了原理,故障排查就有"思路"。
实战线:
运维中 10 类最常见的"诡异故障",
每一种告诉你:现象、原因、排查思路、解决方案。
→ 有了预案,告警来了不慌张。
|
MySQL 的本质:
1
2
3
4
5
|
1. 数据存放:InnoDB 以 16KB 页为单位,B+ 树索引组织
2. 内存优化:Buffer Pool 把磁盘读写转化为内存读写
3. 事务保证:Redo Log + Undo Log + Binlog + 两阶段提交
4. 并发控制:MVCC + 锁(行锁、间隙锁、Next-Key Lock)
5. 数据复制:Binlog 推送,Relay Log 重放
|
当你深夜收到告警时,希望你能想起:
1
2
3
4
5
6
|
"Buffer Pool 命中率多少?"
"是不是有慢查询?"
"是不是有死锁?"
"主从延迟是多少?"
"Binlog 是否在疯狂增长?"
"磁盘是不是快满了?"
|
这些不是死记硬背的步骤,而是从原理推导出来的排查方向。
这才是真正的"懂 MySQL"。
相关推荐阅读: