1. 事情是这样的
今天遇到一个怪事:测试环境中,kms项目有个定时任务,每5秒执行一次,本来跑得好好的,突然就不动了。
最后一条日志停在:
18:06:13.594 - 登录失败,更新同步游标,now: 1773914773
之后就再也没动静了。
2. 开始破案(一步一步发现问题)
第1步:先看数据库里有什么妖孽
SELECT * FROM information_schema.INNODB_TRX;
看到一个妖孽事务:
事务ID: 139630724
开始时间: 18:06:13
当前状态: RUNNING
在干嘛: UPDATE kss_log_sync_cursor SET last_sync_time = ... where id =1
安? 一个 UPDATE 语句执行了十几分钟还没完?这不太对劲!
第2步:加个监控盯着它
我写了个监控程序,每分钟检查一次长事务:
@Component
public class TransactionMonitor {
@Scheduled(fixedDelay = 60000) // 每分钟查一次
public void checkLongRunningTransactions() {
// 查超过5分钟的事务
// 查到就告警
}
}
监控开始报警了:
18:11:22 - 发现长事务,已运行5分钟
18:12:22 - 已运行6分钟
18:13:22 - 已运行7分钟
...
18:33:22 - 已运行27分钟
同一个事务,一直没变,一直在执行那条 UPDATE!
第3步:看这个线程在干嘛
SHOW PROCESSLIST;
看到线程29的状态:waiting for handler commit
翻译成人话:“我已经执行完UPDATE了,正在等提交完成”
第4步:看 InnoDB 内部状态(关键!)
SHOW ENGINE INNODB STATUS
看到一句话:
---TRANSACTION 139630724, ACTIVE (PREPARED) 353 sec
PREPARED 是啥意思?
第5步:弄明白事务提交到底干了啥
原来 MySQL 提交一个事务,不是一秒就完事的,它要分三步走:
你执行 UPDATE
↓
你敲 COMMIT
↓
【第一步】InnoDB说:我准备好了 (PREPARE)
↓
【第二步】写 Binlog(二进制日志) ← 这里需要磁盘空间!
↓
【第三步】InnoDB说:提交完成 (COMMIT),锁释放了
PREPARED 状态 就是:
- 第一步已经完成
- 正在做第二步(写 Binlog)
- 锁还捏在手里不放
第6步:那为啥第二步卡住了?
联系运维查看mysql 磁盘使用情况,已经打算看binlog写入的文件系统是不是有问题了。。。心想mysql内部应该不能出bug吧。
df -h
结果看到:磁盘可用为0!为0!!!
破案了!磁盘满了!
Binlog 写不进去,就一直卡在 PREPARED 状态,锁不释放,其他事务干瞪眼。
3. 真相大白
完整的故事是这样的:
- 定时任务执行到
UPDATE kss_log_sync_cursor - UPDATE 执行成功
- 自动提交事务(或者代码里发了 COMMIT)
- InnoDB 进入 PREPARED 状态
- 要写 Binlog 了 → 磁盘满了!写不进去!
- 事务卡死,锁不释放
- 后续的定时任务想更新同一行 → 被阻塞
- 监控开始报警,眼睁睁看着这个事务从5分钟跑到27分钟
4. 怎么解决
第一步:紧急处理
# 1. 清理磁盘空间(删掉旧 binlog)
mysql> PURGE BINARY LOGS BEFORE DATE(NOW() - INTERVAL 3 DAY);
# 2. 杀掉卡死的事务
mysql> KILL 29;
# 3. 再看磁盘空间
df -h # 应该有空余了
第二步:长期预防
# 在 my.cnf 里加上
[mysqld]
binlog_expire_logs_seconds = 259200 # binlog 保留3天自动删
第三步:监控报警
# 写个脚本监控磁盘
df -h /var/lib/mysql | awk 'NR==2 {if ($5+0 > 80) print "磁盘快满了!"}'
5. 经验
以后排查这类问题,按这个顺序来:
-
先看磁盘(
df -h)—— 最简单,最容易被忽略 -
再看长事务(
INNODB_TRX) -
看进程状态(
PROCESSLIST) -
看 InnoDB 状态(
ENGINE INNODB STATUS)
关键线索总结
| 看到啥 | 说明啥 |
|---|---|
waiting for handler commit |
提交卡住了 |
ACTIVE (PREPARED) |
卡在写 Binlog |
df -h 100% |
找到凶手了! |
记一笔:
PREPARED + waiting for handler commit = 先看磁盘满了没
6. 最后说一句
谁能想到,一个最简单的 UPDATE 语句,会因为磁盘满了卡 27 分钟?
以后再遇到数据库卡死,先 df -h,别一上来就怀疑代码。