触发器导致队列积压的根本原因在于将异步逻辑强行塞入同步事务,使trigger_queue写入受主事务锁制约、索引冗余及消费者拉取无序加剧性能瓶颈;其建表需极简字段、仅两个必要索引、禁用触发器内更新;消费者须有序拉取、分批处理、超时控制与指数重试;非必要逻辑(如纯计算、原生函数)应直接在触发器中执行,避免入队。

触发器本身不产生队列积压,但它会把本该异步的任务硬塞进同步事务,导致消费者来不及拉取、消息表写入变慢、延迟持续升高——问题不在“队列”,而在“不该进队列的逻辑还卡在事务里”。
为什么 trigger_queue 表写入也会堆积?
很多人建了 trigger_queue 表就以为万事大吉,结果发现它自己也堵了。根本原因不是队列服务不行,而是触发器往里写的时候,仍处在主事务中:
- 每次
INSERT INTO trigger_queue都要加行锁,若主表高并发写入(比如每秒 200 次订单插入),等于每秒 200 次对trigger_queue的串行写入请求 -
trigger_queue上如果建了多余索引(比如给event_type和created_at各建一个单列索引),写入时要更新多个 B+ 树,性能断崖式下降 - 消费者用
SELECT ... FOR UPDATE LIMIT 100拉取,但没加ORDER BY id,InnoDB 可能扫描大量未处理记录才找到可锁的批次,反而加剧锁等待
trigger_queue 表怎么建才扛得住高并发?
这张表不是随便建的,它本质是事务边界外的第一道缓冲,设计必须克制:
- 字段极简:
id BIGINT PRIMARY KEY AUTO_INCREMENT、table_name VARCHAR(64)、row_id BIGINT、event_type CHAR(10)、created_at DATETIME(3)——别加status、processed_at、retry_count这类字段,它们该由消费者维护 - 索引只留两个:
INDEX(event_type, created_at)(供消费者按类型+时间拉取)、INDEX(row_id)(供下游根据主键反查,避免 JOIN) - 引擎必须是
InnoDB,且关闭innodb_doublewrite(仅限日志类表,需权衡 crash 安全性) - 绝对禁止在触发器里对这张表做
UPDATE或DELETE——哪怕只是标记“已入队”,那也是消费者的事
消费者任务为什么越跑越慢?
消费者不是“越勤快越好”。常见错误是让它每 10ms 就跑一次,结果频繁抢锁、空转、还干扰主业务 SQL 执行:
- 拉取语句必须带
ORDER BY id LIMIT 100,否则 InnoDB 在 RR 隔离下可能锁住整片间隙 - 处理完一批后,要用
DELETE FROM trigger_queue WHERE id IN (…)或UPDATE … SET status = 'done'(如果加了 status 字段),但删/改操作本身不能太重——建议分批,每批 ≤ 500 行 - 消费者进程得有失败重试机制,但重试间隔要指数退避(如 1s → 3s → 9s),别一失败就立刻重拉,否则会和新写入的记录抢锁
- 如果下游逻辑涉及 HTTP 调用或 Redis 写入,必须设超时(
connectTimeout=1s,readTimeout=2s),超时就丢弃或进死信队列,别让一次失败拖垮整个消费线程
哪些逻辑根本不用进 trigger_queue?
不是所有“后置动作”都值得走队列。真正该进队列的,只有那些:耗时 > 50ms、可能失败、需要重试、或依赖外部系统。其余一律砍掉:
-
NEW.updated_at = NOW()、NEW.version = OLD.version + 1—— 这些直接写在BEFORE UPDATE里,毫秒级,无 IO - 生成 UUID 或订单号(如
NEW.order_no = CONCAT('ORD-', UNIX_TIMESTAMP(), '-', FLOOR(RAND() * 1000)))—— 纯计算,别调函数 - 简单审计字段填充(如
NEW.created_by = USER())—— 数据库原生函数,安全可靠 - 但凡出现
SELECT、INSERT INTO ... SELECT、CALL、HTTP、Redis.set,就必须进队列,且不能在触发器里执行
最容易被忽略的点:trigger_queue 的写入延迟,往往不是因为消费者慢,而是因为主事务太长——检查你是否还在触发器里查了没索引的配置表、或调了没声明 READS SQL DATA 的函数。先砍掉这些,队列才能真正“轻”起来。

















