触发器必须剥离出事务,因其同步执行、逐行调用、共享锁上下文的特性天然不适合业务逻辑;任何SELECT、跨表写入、非确定性函数等操作都会导致阻塞和延迟失控。
触发器导致写入延迟高,不是“能优化多少”的问题,而是“必须剥离出事务”的问题。同步执行、逐行调用、共享锁上下文这三点决定了它天然不适合承担业务逻辑——尤其当延迟已明显升高时,说明你已经在用错误的工具解决异步问题。
MySQL / PostgreSQL / SQL Server 的触发器都卡在同一个地方
所有主流数据库的 AFTER INSERT 或 FOR INSERT 触发器,都是在主事务内同步执行的。哪怕只加了一行 SELECT COUNT(*) FROM log WHERE status = NEW.status,也会让单条 INSERT 从 0.3ms 拉到 12ms;批量插入 1000 行时,开销不是线性叠加,而是乘性放大。更危险的是:触发器里调 CALL notify_service()、发 HTTP、查另一张表、甚至写日志表,都会把整个事务拖住——不是慢,是阻塞。
- 常见错误现象:
Lock wait timeout exceeded、innodb_row_lock_time飙升、应用端报超时但数据库没报错 - 监控线索:看
SHOW ENGINE INNODB STATUS里的TRANSACTIONS部分,常能看到事务卡在触发器语句上 - 别信“加索引就能救”——索引只能缓解查询,无法绕过同步执行和锁继承这两个硬约束
触发器里哪些操作必须立刻删掉
以下行为只要出现一次,就足以让写入延迟失控,且无法通过参数调优修复:
-
SELECT查询其他表(尤其是没复合索引的WHERE status = NEW.status AND created_at > ...) -
UPDATE或INSERT INTO ... SELECT写入关联表(比如订单插入后更新客户积分) - 调用自定义函数(如
GET_USER_RANK()),每次调用都可能触发隐式子事务 - 在
AFTER INSERT中反向UPDATE同一表的汇总行(极易与并发事务死锁) - 使用
NOW()、UUID()、RAND()等非确定性函数(破坏主从一致性,且求值开销不可控)
真正可留在数据库侧的,只有四不原则:无查询、无跨表、无函数、无分支。例如:SET NEW.created_at = NOW()(仅限 binlog_format = ROW)、SET NEW.phone = REPLACE(NEW.phone, ' ', '')、空值校验 SIGNAL。
替代方案不是“选一个”,而是按场景分层落地
没有银弹,但有清晰路径:
- 强实时 + 强一致 → 改用
ON DUPLICATE KEY UPDATE或MERGE合并操作,把多步逻辑压成单语句 - 秒级延迟可接受 → 建轻量日志表(如
trigger_queue),触发器只做INSERT INTO trigger_queue,由外部消费者(Kafka consumer / Maxwell / 自研轮询)异步处理 - 统计类需求 → 优先用
PERSISTED计算列或索引视图,避免运行时计算 - 分布式环境 → 必须用
tx_outbox表 + 对账服务,不能依赖触发器发消息;pg_notify()只负责通知,拉取数据和重试要由消费者保障
最容易被忽略的不是技术选型,而是边界意识:触发器只该做原子性校验和字段填充,不该成为业务逻辑的默认落点。一旦发现它开始查表、调函数、发请求,就说明架构已经越界了。