QPS拐点由触发器引发的锁竞争、WAL暴增和事务串行化三重放大导致,非单条SQL耗时所致;压测需分层递增线程数,监控Innodb_row_lock_time_avg、binlog写入量及主从延迟,并结合performance_schema定位隐式锁与调用链爆炸。

看QPS拐点,不是单条SQL耗时
触发器的性能损耗根本不在EXPLAIN里,也不体现在慢查询日志的单条语句耗时上。它真实作用于系统吞吐量——QPS会随并发线程数增加突然断崖下跌。压测必须分层:从--threads=16开始,逐步加到64、128,记录每档的QPS和p95延迟。多数业务在64线程后,触发器带来的损耗会从20%跳到40%以上,这不是线性叠加,而是锁竞争+WAL暴增+事务串行化三重放大。
常见错误现象:slow_query_log里找不到慢SQL,但SHOW ENGINE INNODB STATUS中innodb_row_lock_time_avg飙升;performance_schema.events_statements_history_long能查到大量含TRIGGER字样的事件,但TIMER_WAIT字段为空或极小——说明耗时不被单独计时,却被真实消耗了。
抓WAL暴增和主从延迟突变
一个INSERT INTO t VALUES (1),(2),(3)本该是1次写入,有AFTER触发器就变成3次独立事务+3次binlog记录+3次锁申请。批量写入时,Innodb_os_log_written指标会异常凸起,主从延迟Seconds_Behind_Master同步上涨。
实操建议:
- 监控
Innodb_rows_updated和Handler_update比值:若接近1:1,说明每行UPDATE都触发了额外操作 - 主库
SHOW PROCESSLIST看着空闲,但从库Replication_SQL_Running: Yes下Seconds_Behind_Master持续涨,大概率是触发器生成的额外binlog事件拖住了SQL线程 - 确认
binlog_format = ROW(必须),MIXED或STATEMENT会让触发器行为不可预测
查锁范围是否因触发器失控
触发器内SQL没走索引,会导致锁范围扩大。比如UPDATE stats SET cnt = cnt + 1 WHERE type = NEW.type,而stats(type)无索引,就会锁全表或大范围间隙——这在高并发下直接引发锁等待风暴。
MySQL 8.0+ 必须查performance_schema.data_locks,过滤EVENT_NAME LIKE '%trigger%',确认锁类型是不是TABLE或RECORD,以及LOCK_DATA是否包含非预期的多行值。
容易踩的坑:
-
SELECT COUNT(*) FROM big_table WHERE user_id = OLD.user_id——即使只查一行,没索引就是全表扫,每改一行都扫一遍 - 触发器里
UPDATE原表(如orders的AFTER INSERT里再UPDATE orders),MySQL 8.0+会报ERROR 1442 (HY000),但低版本可能静默失败或死锁 - 用
sysbench oltp_insert这种单表脚本压测,它不模拟真实字段约束和跨表校验,结果严重乐观
识别隐式递归与调用链爆炸
最危险的不是慢,而是不可见的连锁反应。比如用户表UPDATE触发器去更新统计表,而统计表的UPDATE又触发另一个触发器去写日志表——这不是两层嵌套,是调用链爆炸。线上偶发卡顿、死锁,90%查不到源头,因为SHOW PROCESSLIST只显示主SQL,阻塞源藏在触发器里。
验证方法:
- 查
pg_stat_activity(PostgreSQL)或information_schema.INNODB_TRX(MySQL),看xact_start与TRX_STARTED时间差是否远大于应用层预期 - 用
pt-deadlock-logger捕获死锁日志,重点看Query字段是否含TRIGGER字样 - 禁用触发器后压测,若QPS恢复且
Created_tmp_disk_tables下降明显,基本可锁定是触发器内游标或JOIN导致内存耗尽
复杂点在于:触发器逻辑本身可能没问题,但和现有索引、事务隔离级别、甚至其他触发器组合后,锁等待链就不可控了。上线前必须在同构环境跑完整业务链路压测,不能只测单点SQL。



















