RDS慢查询把我带进锁等待坑里
事故发生时我盯错了地方
上周三 21:37,CES 报警把我从会议里拽出来,短信内容是订单服务 /api/order/list 5 分钟 P95 超过 2000ms,实时值 2864ms。业务同事同一时间丢了录屏,页面转圈 6 到 8 秒。那一小时网关每分钟请求量 4100,和前一天同时间段差 3.2%,流量没突增。我还去看了 Nginx upstream 的 5xx,比平时多 0.1%,也不像雪崩。
我当时以为是 JVM Full GC。
我在三台应用机上各跑了一遍检查,机器规格 ECS s6.large.2,2 核 4GB,Ubuntu 22.04.4,Java 17.0.10,Spring Boot 3.2.4。命令是:
grep -c 'Pause Full' /data/logs/order/gc.log
jstat -gcutil 28741 1000 10
三台机器 Pause Full 结果都是 0,Young GC 典型一条是 Pause Young (Normal) (G1 Evacuation Pause) 41.7ms,最长 63ms。现在回头看,信号已经很清楚了,我还是没刹住,21:49 直接扩容到 s6.xlarge.2(4 核 8GB),21:56 控制台显示生效。P95 只从 2864ms 掉到 2520ms,7 分钟后又回到 2600ms 上下,按量这波花了 96 元。
这一步我付了学费。
22:18 我又做了第二次错误尝试,只在 10.23.14.27 这台实例把 Hikari 连接池拉大,想把线程排队吃掉。改的是 Nacos 里的 order-service-prod.yaml,改完热加载,22:21 生效,参数从 80/20 调到 120/40,超时仍是 3000ms:
spring.datasource.hikari.maximumPoolSize=120
spring.datasource.hikari.minimumIdle=40
spring.datasource.hikari.connectionTimeout=3000
spring.datasource.hikari.maxLifetime=1800000
10 分钟后,RDS 总连接数冲到 600 上限,应用日志出现两类错误,一类是池子拿不到连接,一类是数据库拒绝新连接:
java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 3001ms.
at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:696)
com.mysql.cj.jdbc.exceptions.CommunicationsException: Too many connections
SQLState: 08004, ErrorCode: 1040
22:31 我把配置回滚到 80/20,连接耗尽告警在 2 分钟内消失,但接口慢问题没动静。
数据库是华为云 RDS for MySQL 8.0.28,主备,4 核 8GB,150GB 高 IO 云盘,max_connections=600,innodb_lock_wait_timeout=50。报警时 CPU 78% 到 84% 抖动,磁盘带宽没打满,Threads_running 峰值 129(平时 20 到 30),慢 SQL 从 900/h 冲到 1.3 万/h,10 分钟内出现 217 次 1205。应用侧报错是:
2026-10-16 21:41:23.882 ERROR o.h.engine.jdbc.spi.SqlExceptionHelper
SQL Error: 1205, SQLState: HY000
Lock wait timeout exceeded; try restarting transaction
2026-10-16 21:41:23.883 ERROR c.z.order.dao.OrderMapper
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException:
Lock wait timeout exceeded; try restarting transaction
我在跳板机直接连库,先看会话和锁,没先追调用链,这个顺序现在看也不够好。命令是:
mysql -h rds-3a91f6.mysql.rds.myhuaweicloud.com -P3306 -uops -p
SHOW FULL PROCESSLIST;
SHOW GLOBAL STATUS LIKE 'Threads_running';
SHOW VARIABLES LIKE 'long_query_time';
SHOW ENGINE INNODB STATUS\G;
SELECT waiting_trx_id, blocking_trx_id FROM information_schema.innodb_lock_waits;
long_query_time 是 0.5 秒,样本量够。processlist 里我抓到 14 条 State: Waiting for record lock,最长一条 Time=37 秒,SQL 是结算任务的 UPDATE,阻塞链顶端是同租户的另一条大事务。
我怎么把锁等待压下去
根因是两条 SQL 撞在一起。在线查询写成了 DATE(updated_at)=CURDATE(),把索引列包函数,执行计划里 type=ALL,rows=3187421,Extra=Using where; Using filesort。结算任务由 Quartz 跑,表达式 0 */10 * * * ?,每 10 分钟单事务更新 2 万行,事务持续 18 到 26 秒,期间一直持锁:
SELECT id,status,updated_at
FROM t_order
WHERE tenant_id = 17
AND DATE(updated_at) = CURDATE()
AND status IN (1,2)
ORDER BY updated_at DESC
LIMIT 50;
UPDATE t_order
SET settle_status = 1, settle_time = NOW()
WHERE tenant_id = 17
AND settle_status = 0
AND created_at < NOW() - INTERVAL 2 DAY;
00:35 我开始改线上查询,改法很直接,把函数条件改成时间范围,让优化器能走范围扫描;同时补联合索引,顺序按过滤和排序放成 (tenant_id, status, updated_at, id)。DDL 总耗时 11 分 32 秒,其中 metadata lock 等了 2 分 14 秒,我在 processlist 里看到 Waiting for table metadata lock,说明长事务确实在拖:
ALTER TABLE t_order
ADD INDEX idx_tenant_status_updated (tenant_id, status, updated_at, id),
ALGORITHM=INPLACE,
LOCK=NONE;
SELECT id,status,updated_at
FROM t_order
WHERE tenant_id = 17
AND updated_at >= CURDATE()
AND updated_at < CURDATE() + INTERVAL 1 DAY
AND status IN (1,2)
ORDER BY updated_at DESC
LIMIT 50;
索引生效后我马上跑了 EXPLAIN ANALYZE,同一条查询从 1.9s 降到 34ms,扫描行数从 300 多万降到 612。
结算任务我没有停在一句“切小批次”,实际改了三处。单次上限从 20000 改到 500,每批独立提交,批次间 sleep 50ms 给在线请求插队。这个方案我一开始是拒绝的,因为担心任务窗口拖长,先试了 2000/批,压测里事务还是 6 到 8 秒,information_schema.innodb_lock_waits 里的等待条数会从 0 突然跳到 9,没过线。降到 500 后,单批事务 180 到 320ms,整轮任务从 24 秒变成 2 分 47 秒,但在线接口恢复了稳定,我接受这个交换:
do {
UPDATE t_order
SET settle_status = 1, settle_time = NOW()
WHERE tenant_id = 17
AND settle_status = 0
AND created_at < NOW() - INTERVAL 2 DAY
ORDER BY id
LIMIT 500;
COMMIT;
SELECT ROW_COUNT() INTO affected;
DO SLEEP(0.05);
} while (affected > 0);
为了夜里不再抖,我把 RDS 临时升到 8 核 16GB 顶了 6 小时,额外 38.7 元,第二天 09:20 降回 4 核 8GB。
改完后我盯了 48 小时,/api/order/list 的 P95 从 2864ms 到 184ms,P99 从 6.7s 到 410ms;慢 SQL 从 1.3 万/h 到 317/h;Threads_running 峰值从 129 到 22;1205 第二天只剩 3 次,都是旧任务重试打出来的,第三天归零。现在还没收尾的是两个历史报表 SQL,凌晨 01:10 和 01:40 各跑一次,全表扫描 43s 和 58s,会把只读连接池 120 个连接打满。我下周准备先把 90 天前数据拆到归档表,报表改查归档库,再做一轮压测看要不要加只读实例专跑报表任务,slow log 阈值先维持 0.5 秒不动。
- 点赞
- 收藏
- 关注作者
评论(0)