⚠️ 线上问题
📚 索引原理
🔧 EXPLAIN
🔍 怎么发现的?
PM
产品经理-小美
@后端 用户反馈借书单列表打开巨慢,能看看吗?好几个客户投诉了
$ trace com.milktea.service.OrderService getMyOrders
+---[2987ms] com.mysql.cj.jdbc.ClientPreparedStatement:executeQuery()
+---[2ms] java.util.ArrayList:<init>()
🔎 定位SQL
mysql> SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 3\G
Query_time: 3.214507 seconds
Rows_examined: 895432
SQL: SELECT * FROM order_info WHERE status = 'paid' ORDER BY created_at DESC LIMIT 20
这条SQL扫描了 89 万行!status 上没有索引,ORDER BY created_at 也没走索引。
📊 EXPLAIN 分析
💡 背后原理:为什么这么慢?
📚 原理:全表扫描 + Filesort
1) 为什么走了全表扫描?
WHERE status = 'paid',但 status 列上没有索引。InnoDB 的 B+树索引是按列的值排好序的。没有索引,MySQL 不知道 'paid' 的数据在哪,只能从第1行扫到最后一行,逐行判断 status 是不是 'paid'。89万行全扫一遍。
2) 为什么还有 Using filesort?
ORDER BY created_at DESC。数据在磁盘上是按主键 id 排列的(聚簇索引),不是按 created_at 排的。MySQL 要先把扫出来的数据全放内存里(sort_buffer),再排序。89万行 sort_buffer 放不下 → 溢出到磁盘临时文件 → 磁盘IO爆炸。
3) 执行流程:全表扫描89万行 → 过滤出 status='paid' 的行 → 把这些行写入 sort_buffer → sort_buffer 放不下分批排 → 写磁盘临时文件 → 归并排序 → 取前20条 → 回表取完整数据(如果 SELECT * 的话)
✅ 修复方案
✅ 修复结果
$ trace com.milktea.service.OrderService getMyOrders
+---[23ms] com.mysql.cj.jdbc.ClientPreparedStatement:executeQuery()
3214ms → 23ms,提升 140 倍。索引让 MySQL 直接定位到 status='paid' 的数据(不用全扫),而且联合索引里 created_at 已经排好了(不用额外排序)。
⚠️ 线上告警
📚 行锁 & 死锁原理
🔧 调整更新顺序
🔍 错误日志
2025-06-11 12:34:56 ERROR [OrderService] - Deadlock found when trying to get lock;
try restarting transaction
com.mysql.cj.jdbc.exceptions MySQLTransactionRollbackException:
Deadlock found when trying to get lock; try restarting transaction
🔎 定位:两个接口的更新顺序不一致
💡 背后原理:死锁是怎么产生的?
📚 原理:行锁的交叉等待
InnoDB 的行锁:UPDATE/DELETE 执行时,InnoDB 会给匹配到的每一行加排他锁(X锁)。其他事务要修改同一行,必须等锁释放。
死锁条件:两个事务互相持有对方需要的锁,形成
环路等待:
事务A
持有: order_info.id=1
等待: product.id=5
⇄
事务B
持有: product.id=5
等待: order_info.id=1
MySQL 怎么处理?:InnoDB 有死锁检测机制,发现环路后立刻回滚代价较小的事务(通常是修改行数少的那个),报错给应用。
类比:两辆车在窄路上面对面相遇。A车占着左边要往右走,B车占着右边要往左走。谁也过不去。只能让一辆车先退回去。
📊 InnoDB 锁层级体系
InnoDB 锁体系
┌───────────┘
└───────────┐
表级锁
行级锁
── 表级锁细分 ──
意向共享锁 (IS)
SELECT ... LOCK IN SHARE MODE
意向排他锁 (IX)
SELECT ... FOR UPDATE
元数据锁 (MDL)
自动加,防DDL冲突
自增锁 (AUTO-INC)
INSERT时保护自增列
── 行级锁细分 ──
Record Lock 记录锁
锁定单行记录
Gap Lock 间隙锁
锁定索引间隙(开区间)
Next-Key Lock
Record + Gap (左开右闭)
Insert Intention 插入意向锁
INSERT特殊间隙锁
💡 死锁本质:两个事务交叉持有不同行级锁(如 X锁 + Gap锁),形成环路等待。Next-Key Lock 是 RR 级别默认加锁方式,最容易引发死锁。
✅ 修复:统一更新顺序
✅ 修复结果
统一更新顺序后,两个事务都是先锁 product 再锁 order_info,不会出现交叉等待。上线后死锁消失。
预防死锁的三条规则:
① 按固定顺序访问表和行(如按 id 升序)
② 保持事务短小,减少持锁时间
③ 合理使用索引,避免没走索引导致行锁升级为表锁
📚 事务隔离 & MVCC
🔧 隔离级别
🔍 Part A:不可重复读 — 同一事务两次读结果不同
不可重复读关注的是数据内容的变化:同一事务内两次读取同一行,结果不一样。RR级别通过 MVCC 快照读解决。
📚 原理:MVCC 版本链 + ReadView 解决不可重复读
RR级别(可重复读)下,事务开始时拍一张"快照"(ReadView)。之后所有快照读(普通SELECT)都看这张快照里的数据版本。
每一行数据都有隐藏列:
• trx_id:最后修改这行的事务ID
• roll_pointer:指向上一个版本(undo log链)
读的时候,如果发现当前版本的 trx_id > 自己的 ReadView 能看到的范围,就顺着 roll_pointer 往前找,找到一个"自己能看到"的版本。
⚠️ 重要:MVCC 只解决了"不可重复读"问题(快照读层面),并没有解决"幻读"问题!
幻读针对的是当前读(SELECT ... FOR UPDATE / LOCK IN SHARE MODE),需要 Next-Key Lock 来防止。
🔍 Part B:幻读 — 同一事务两次范围查询,结果集行数不同
幻读关注的是结果集行数的变化:同一事务内两次范围查询,中间有其他事务插入了新行,导致结果集多了"幻影行"。MVCC 的快照读看不到新行 ≠ 防止了幻读,幻读是针对当前读的问题。
💡 背后原理:不可重复读 vs 幻读 的本质区别
📚 原理:MVCC 解决不可重复读,Next-Key Lock 解决幻读
不可重复读:同一事务内两次读取同一行数据,内容不同。解决方式:MVCC 快照读。
幻读:同一事务内两次范围查询,结果集行数不同(多了"幻影行")。解决方式:Next-Key Lock 当前读。
关键区别:
• 快照读(普通 SELECT)→ MVCC 通过 ReadView 保证可重复读,看不到其他事务新插入的行 → 这只是解决了"不可重复读"
• 当前读(SELECT ... FOR UPDATE / LOCK IN SHARE MODE / UPDATE / DELETE)→ 读取最新已提交数据,MVCC 的快照机制不生效 → 必须靠 Next-Key Lock 锁住间隙来防止幻读
Next-Key Lock = Record Lock(行锁)+ Gap Lock(间隙锁)
• Record Lock:锁住索引上的已有行
• Gap Lock:锁住索引之间的间隙,阻止其他事务在间隙中插入新行
• 在 RR 级别下,当前读会自动使用 Next-Key Lock,从而防止幻读
常见误区:很多人以为"RR级别下快照读看不到新插入的行就是防止了幻读" — 这是错的!快照读看不到新行是解决"不可重复读",幻读是当前读层面的问题。即使RR级别下,如果你用快照读查了一次,然后执行 UPDATE 语句(当前读)修改了那行,再快照读就能看到了 — 这才是真正可能发生幻读的场景。
📊 MVCC 版本链 + ReadView 可见性判断(解决不可重复读)
── 版本链(通过 roll_pointer 串联) ──
当前版本
trx_id = 108
amount = 35.00
← roll_pointer ←
版本1
trx_id = 105
amount = 28.00
← roll_pointer ←
版本2
trx_id = 100
amount = 52.00
── 事务A 的 ReadView(RR级别,事务开始时创建) ──
min_trx_id = 102
活跃事务最小ID
max_trx_id = 110
下一个分配ID
creator_trx_id = 102
当前事务ID
── 可见性判断规则 ──
trx_id=100 < min_trx_id=102
✅ 已提交,可见
|
trx_id=108 在 [102,110) 范围内
且在活跃列表中
❌ 未提交,不可见
➡️ 结果:顺着版本链,跳过 trx_id=108(不可见)→ 跳过 trx_id=105(不可见)→ 找到 trx_id=100(✅可见)→ 返回该版本数据
📊 Next-Key Lock 如何防止幻读(RR级别当前读)
── 索引上的记录与间隙 ──
行记录
amount=20
间隙
(20,28)
行记录
amount=28
间隙
(28,35)
行记录
amount=35
间隙
(35,52)
行记录
amount=52
间隙
(52,+∞)
── 事务A执行 SELECT * FROM order_info WHERE amount > 20 FOR UPDATE 加锁范围 ──
amount=20
不满足条件
🔒 间隙锁
(20,28)
🔒 行锁
amount=28
🔒 间隙锁
(28,35)
🔒 行锁
amount=35
🔒 间隙锁
(35,52)
🔒 行锁
amount=52
🔒 间隙锁
(52,+∞)
🚫 事务B尝试 INSERT amount=25 → 落在间隙 (20,28) → 被间隙锁阻塞 → 无法插入 → 防止了幻读!
✅ RC级别下 FOR UPDATE 只加行锁(Record Lock),不加间隙锁 → 事务B可以在 (20,28) 间隙插入 → 出现幻读!
📊 不可重复读 vs 幻读 关键对比
| 对比维度 |
不可重复读 |
幻读 |
| 关注点 |
同一行数据内容变化 |
结果集行数变化(多了幻影行) |
| 产生原因 |
其他事务 UPDATE/DELETE 了已有行 |
其他事务 INSERT 了新行 |
| 读类型 |
快照读(普通 SELECT) |
当前读(SELECT FOR UPDATE 等) |
| RR级别解决方式 |
MVCC + ReadView(快照读看旧版本) |
Next-Key Lock(锁间隙阻止INSERT) |
| RC级别 |
❌ 可能发生 |
❌ 可能发生 |
| RR级别 |
✅ 防止 |
✅ 防止* |
* RR级别下快照读通过 MVCC 防止不可重复读;当前读通过 Next-Key Lock 防止幻读。但快照读后执行 UPDATE(当前读)仍可能触发特殊场景的幻读
📊 四种事务隔离级别对比
| 隔离级别 |
脏读 |
不可重复读 |
幻读 |
InnoDB 防止方式 |
适用场景 |
READ UNCOMMITTED 读未提交 |
❌ 可能 |
❌ 可能 |
❌ 可能 |
无任何防护 |
几乎不用 |
READ COMMITTED 读已提交 (RC) |
✅ 防止 |
❌ 可能 |
❌ 可能 |
每次 SELECT 生成新 ReadView |
Oracle默认 / 需要实时数据 |
REPEATABLE READ 可重复读 (RR) |
✅ 防止 |
✅ 防止 |
✅ 防止* |
快照读:MVCC + ReadView 防不可重复读 当前读:Next-Key Lock 防幻读 |
MySQL默认 / 大多数业务 |
SERIALIZABLE 串行化 |
✅ 防止 |
✅ 防止 |
✅ 防止 |
所有读加共享锁,完全串行 |
对一致性要求极高 / 性能差 |
* InnoDB 在 RR 级别:快照读通过 MVCC 防不可重复读;当前读通过 Next-Key Lock 防幻读。快照读后执行 UPDATE 再查可能触发特殊场景的幻读
⚠️ 生产事故
📚 undo log & 事务
💡 背后原理:为什么 ROLLBACK 没用?
📚 原理:autocommit 和 undo log
InnoDB 的 undo log:每次修改数据前,先把旧值记到 undo log 里。如果执行 ROLLBACK,就按 undo log 恢复。
但关键是:MySQL 默认 autocommit=1,每条 SQL 自成一个事务。执行完就自动 COMMIT 了。COMMIT 之后 undo log 就没用了(purge 线程会清理)。
所以:在 DataGrip 里执行 DELETE,默认自动提交了。你 ROLLBACK 回滚的是一个空的、新的事务,不是之前的 DELETE。
类比:你把文件扔进回收站(undo log还在),可以撤销。但你点了"清空回收站"(COMMIT),就真的没了。
✅ 能恢复吗?怎么预防?
如果还没提交(在BEGIN事务里):直接 ROLLBACK,undo log 会恢复所有数据。✅
如果已经提交了:
① 从 binlog 恢复(如果开了 binlog)—— 最可靠
② 从备份恢复 + binlog 重做(推荐方案)
③ 使用第三方工具如 mysqlbinlog 解析 binlog
预防措施:
① DataGrip/Navicat 设置"安全模式":UPDATE/DELETE 没有 WHERE 不让执行
② 生产环境用 sql_safe_updates=1
③ 重要操作先 BEGIN → 确认结果 → COMMIT
⚠️ 功能问题
📚 回表 & 临时表
🔍 问题代码
List<Order> all = orderMapper.selectAll();
ExcelUtil.export(all);
💡 背后原理:为什么一次查100万行会崩?
📚 原理:回表 + 内存 + 网络传输
1) SELECT * 的回表问题:如果走了二级索引,每一条都要回聚簇索引取完整数据。100万行 = 100万次回表。
2) 内存问题:100万个 Java 对象,每个对象假设500字节 = 500MB 堆内存。很可能 OOM。
3) 网络问题:MySQL 把100万行从磁盘读到 Buffer Pool → 通过网络传给 Java 应用。网络带宽被打满。
类比:你要把技术10年的所有借书单都搬到新店。不能一次全搬(搬不动),应该分批搬。
✅ 修复:分批导出 + 流式写入
💡 游标分页(基于 id)比 OFFSET 分页快很多:不用跳过前N行,直接从上次的位置继续。SELECT 指定字段而不是 *,减少回表数据量。
⚠️ 线上故障
📚 连接管理 & 长事务
com.alibaba.druid.pool.GetConnectionTimeoutException:
wait millis 60000, active 50, maxActive 50
🔍 排查:谁占着连接不放?
💡 背后原理
📚 原理:MySQL连接 & 连接池
MySQL 的连接模型:每个连接对应一个线程。连接不是免费的——要分配内存、创建线程、鉴权。
连接池:应用启动时建好 N 个连接(如50个),请求来了从池里借,用完还回去。避免反复创建/销毁连接的开销。
连接池耗尽的根因:
① 长事务:一个方法里 @Transactional 包了太多逻辑(调远程服务、发邮件...),连接被占着几十秒
② 慢查询:SQL 太慢,连接被占着等结果
③ 忘记关闭:代码bug,拿到了连接但没有 finally 里 close
类比:技术有50个收银台。如果一个客人占着收银台打电话(长事务),其他人就只能排队。50个都占满了,新客人就进不来了。
修复方案:
① 紧急处理:KILL CONNECTION 12; 杀掉长时间 Sleep 的连接
② 根本解决:缩小 @Transactional 范围,只在数据库操作部分加事务
③ 加监控:Druid 连接池配置 removeAbandoned=true,超时自动回收
④ 排查慢查询并优化
⚠️ 线上故障
📚 DDL原理 & Online DDL
💡 背后原理:ALTER TABLE 在做什么?
📚 原理:DDL的COPY vs INPLACE
老版本(COPY算法):
① 创建一个新表(带新字段)
② 把旧表数据逐行拷贝到新表
③ 删除旧表,新表改名为旧表名
④ 全程锁表!500万行拷贝可能要几分钟
新版本(MySQL 5.6+ Online DDL, INPLACE算法):
① 在原表上直接修改表结构(不拷贝)
② 同时记录期间的 DML 变更到"在线日志"
③ 修改完后,把日志里的变更回放
④ 只有最后很短的时间需要锁(毫秒级)
但有些操作不能用 INPLACE:
• 修改列类型(如 INT → BIGINT)→ 必须COPY
• 修改字符集 → 必须COPY
• 删除主键 → 必须COPY
类比:COPY = 把整栋楼拆了重建(居民全搬走)。INPLACE = 在现有楼层上加一层(居民继续住,只有最后封顶时暂停一下)。
📊 DDL 算法对比:COPY vs INPLACE
❌ COPY 算法(旧版)
① 创建临时表(含新列结构)
↓
② 逐行拷贝旧表数据到临时表
↓
③ 删除旧表,将临时表改名为原表
④ 重建所有索引
🔒 全程锁表!500万行需数分钟
DML 全部阻塞
✅ INPLACE 算法(MySQL 5.6+)
① 在原表上直接修改表结构定义
↓
② 同时记录期间 DML 变更到在线日志
↓
③ 修改完后回放日志中的变更
④ 极短时间加锁完成元数据更新
🔓 几乎不锁表!仅毫秒级锁定
DML 正常执行
解决方案:
① MySQL 5.6+ 自动用 INPLACE,确认:ALTER TABLE ... , ALGORITHM=INPLACE, LOCK=NONE;
② 超大表(千万级)用工具:pt-online-schema-change 或 gh-ost
③ 在低峰期执行
④ 执行前检查:SHOW PROCESSLIST; 确认没有长事务
📚 深翻页 & 回表
🔍 问题SQL
💡 背后原理:为什么 LIMIT 50000,10 这么慢?
📚 原理:OFFSET 的代价
LIMIT 50000, 10 的意思是:从第50001行开始取10条。
但 MySQL 不是直接跳到第50001行!它的执行流程是:
① 从索引的第1条开始扫描
② 扫描前50010条
③ 扔掉前50000条
④ 返回最后10条
如果 SELECT *,每一条都要回表。扫描50010条 = 回表50010次!
类比:你要翻到一本书第500页。不能直接翻过去,必须从第1页开始,一页一页翻,翻到第500页。越往后翻越慢。
✅ 修复:延迟关联
📚 InnoDB MVCC & COUNT
💡 背后原理
📚 原理:InnoDB 的 COUNT 要遍历索引
为什么不能直接存一个总数?
因为 MVCC!每个事务看到的数据不一样。事务A可能看不到事务B刚删的那行,也可能看不到事务C刚插的那行。所以没有一个"绝对的总数",每个事务都得自己数一遍。
InnoDB 怎么 COUNT?:选一个最小的二级索引,遍历所有叶子节点数一遍。因为二级索引比聚簇索引小(每页能存更多条目),所以遍历更快。
MyISAM 为什么快?:MyISAM 不支持事务,没有 MVCC 问题,直接存了一个精确的行数。O(1) 返回。
类比:InnoDB 像一个有多本"快照"的技术——不同时间点看,书架上的书数量不一样,所以每次都要现数。MyISAM 只有一本,门口计数器记了总数。
优化方案:
① 维护一张单独的统计表,每次 INSERT/DELETE 时 +1/-1(业务层保证)
② 用 Redis 缓存总数(定时同步,允许微小误差)
③ 用 EXPLAIN SELECT COUNT(*) 确认走了最小索引
④ 如果只要"大概多少",用 SHOW TABLE STATUS 的 Rows 列(估算值,秒返)
⚠️ 线上问题
📚 主从复制 & binlog
💡 背后原理
📚 原理:主从复制是异步的
主从架构:写请求到主库(Master),读请求到从库(Slave)。
复制流程:
① 主库执行SQL
写入binlog
→
② 从库IO线程
拉取binlog
→
③ 写入relay log
(中继日志)
→
④ SQL线程
重放relay log
延迟产生的原因:主库写入 binlog 后,从库要拉取、写入、重放。这个过程是
异步的。如果主库写入量大(大促/批量操作),从库可能落后好几秒。
用户下单后立刻去从库读:主库已经有了这条借书单,但从库还没来得及重放 → 读不到!
类比:你在总部下了借书单(主库),分店(从库)收到通知需要时间。你立刻打电话到分店问,分店还没收到通知,说"没有这个借书单"。
解决方案:
① 强制路由到主库:写完之后的第一次读,走主库(ShardingSphere等中间件支持)
② 半同步复制:主库等至少一个从库确认收到 binlog 才返回成功
③ 并行复制:MySQL 5.7+ 多线程重放 binlog
④ 监控延迟:SHOW SLAVE STATUS\G 看 Seconds_Behind_Master
📚 隐式类型转换
🔍 MyBatis 映射的坑
<select id="getByPhone" resultType="User">
SELECT * FROM user WHERE phone = #{phone}
</select>
userMapper.getByPhone(13812341234);
💡 背后原理
📚 原理:隐式类型转换 = 函数调用 = 索引失效
MySQL 的类型转换规则:
• 字符串列 = 数字 → MySQL 把每一行的字符串转成数字再比较 → 索引失效
• 数字列 = 字符串 → MySQL 把字符串转成数字(只转一次)→ 索引不失效
所以 WHERE user_id = '101'(INT=STRING)不会失效,但 WHERE phone = 13812341234(VARCHAR=INT)会失效。
类比:电话簿按名字拼音排好了(索引),但你要用编号查人。MySQL 要先把所有人的名字翻成编号,再和你的编号比。相当于对每行做了函数处理。
📚 字符集 & 编码
💡 背后原理
📚 原理:字符集不匹配
字符集链条(三个环节要一致):
Java 应用
(UTF-8)
→
JDBC 连接
(characterEncoding)
→
MySQL Server
(character_set_server)
→
数据库/表/列
(CHARSET)
任何一个环节字符集不一致,都会导致乱码。
常见问题:
• 数据库/表用了
latin1(MySQL老版本默认),不支持中文
• JDBC 连接串没加
characterEncoding=utf8
• 表用了
utf8 而不是
utf8mb4 → 存不了emoji 😂
utf8 vs utf8mb4:MySQL的 utf8 只支持3字节UTF8字符,utf8mb4 支持4字节(emoji、生僻字)。
永远用 utf8mb4。
修复:
ALTER DATABASE milk_tea_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE product CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
JDBC URL 加 ?characterEncoding=utf-8
📚 事务 & 锁持有时间
@Transactional
public void placeOrder(OrderDTO dto) {
orderMapper.insert(order); // 1. 插入借书单
productMapper.updateStock(..); // 2. 扣库存 ← 加了行锁
payService.pay(dto); // 3. 调支付接口 ← 😱 外部HTTP调用,可能3秒!
notifyService.sendMsg(..); // 4. 发消息 ← 又是外部调用
}
💡 背后原理
📚 原理:事务越长,锁持有越久
Spring 的 @Transactional 在方法开始时 BEGIN,方法结束时 COMMIT。
问题:
① 步骤2扣库存时,对 product 行加了行锁
② 步骤3调支付接口(HTTP远程调用),可能要2-5秒
③ 这2-5秒里,行锁一直被持有!其他事务要扣同一商品的库存,只能等!
④ 如果支付服务更慢(比如10秒),大量事务排队等锁 → 连接池耗尽 → 雪崩
类比:你占着收银台(事务),付钱的时候发现支付宝要等5秒。5秒里别人都用不了这个收银台。
修复原则:事务里只放数据库操作,不放外部调用
① 步骤1、2放事务里(纯DB操作)
② 事务提交后,再调支付接口和发消息
③ 支付结果通过回调接口更新借书单状态(另一个小事务)
④ 或者用消息队列(RabbitMQ)异步处理支付和通知
⚠️ 运维问题
📚 存储空间 & binlog
$ du -sh /var/lib/mysql/*
4.2G /var/lib/mysql/milk_tea_shop/
12.8G /var/lib/mysql/binlog.*
1.1G /var/lib/mysql/ibdata1
256M /var/lib/mysql/ib_logfile*
💡 背后原理:磁盘空间被什么吃了?
📚 原理:binlog + undo log + 数据文件
MySQL 磁盘空间的组成:
1) binlog(归档日志):记录所有数据变更操作,用于主从复制和数据恢复。默认永不过期!你的数据库运行1年,binlog 可能几百GB。
2) 数据文件(.ibd):InnoDB 每个表的数据存在 .ibd 文件里。DELETE 删了数据,文件不会自动缩小(只是标记为可复用)。这就是"碎片空间"。
3) undo log:事务的回滚日志。如果有长事务一直不提交,undo log 就会越来越大(因为要保留旧版本给这个长事务读)。
4) 临时文件:大排序(Filesort)、大 GROUP BY 会产生临时表,写磁盘。
类比:技术的话费账单(binlog)从开业到现在全部保留(12GB)、翻页笔记本删了几页但没撕掉(碎片)、有个老读者三个月前开始看索引卡还没决定(长事务 undo log)。
清理方案(从大到小排列):
① 清理 binlog:PURGE BINARY LOGS BEFORE DATE_SUB(NOW(), INTERVAL 7 DAY); 只保留7天
② 设置自动过期:SET GLOBAL expire_logs_days = 7;
③ 清理碎片:OPTIMIZE TABLE order_info;(会锁表,用 pt-online-schema-change 替代)
④ 杀长事务:SELECT * FROM information_schema.innodb_trx; 找到跑了几小时的事务
⑤ 清理慢查询日志:RESET MASTER;(慎用!会清空所有binlog)