等一下,先澄清一个常见误解:在 Row 格式的 binlog 下,从库本来就不会执行触发器。主库上的触发器效果已经体现在每行变更的具体数据中了,从库只需要应用这些行变更,不需要再重放一遍触发器的逻辑——这不是 bug,而是设计如此。

触发器在从库执行时会放大锁竞争
在主库上,触发器通常只在当前事务的上下文中执行,影响范围相对有限。真正的问题出在从库一侧:SQL thread在 MySQL 5.6 及更早版本中是单线程的,即便到了 5.7 之后支持并行复制,其粒度依然受到限制。所有触发器逻辑都必须串行排队执行。一旦某个触发器内包含一条UPDATE或INSERT ... SELECT这样的扫描操作,就等于在一个狭窄的管道里塞进了一个大型操作——行锁或表锁被长时间持有,后续 relay log 中的大量事件只能停滞等待。
这种场景下的典型表现是:你在SHOW PROCESSLIST中看到Sla ve_SQL_Running_State卡死在Updating或Waiting for table metadata lock,同时Seconds_Behind_Master持续飙升,根本无法回落。
几个容易踩坑的地方:
- 触发器里调用了存储函数,或者访问了没有索引的字段——全表扫描伺候
- 多个事务同时触发同一张日志表的写入——热点行争用,从库直接卡成单点
- 触发器里又嵌套触发另一个表的触发器——链式延迟,放大效应雪上加霜
ROW 格式下触发器行为不可控
当前生产环境大多使用binlog_format=ROW,这本身是推荐的做法。但需要注意一个关键细节:Row 格式下,主库将变更后的行镜像写入 binlog,而触发器本身不会被记录到 binlog 中。换句话说,触发器只在主库上执行一次,从库不会重复触发。除非你显式设置log_bin_trust_function_creators=1并手动同步触发器定义——这种做法极不推荐,不要尝试。
问题出在哪里?假设主库上的触发器自动填充了updated_at字段,而从库表结构中根本没有这个触发器,那么数据就会出现不一致。反过来,如果你硬要在从库也部署相同的触发器,执行时机(before/after)和上下文(比如NEW值的来源)都会不一致,结果可能更糟——重复更新,甚至违反约束。
总结一下几种情况的风险:
- Row 复制:从库跳过触发器 → 数据差异风险
- Statement 复制:触发器被重放 → 但 Statement 格式本身就不安全,非确定性函数很容易翻车
- 即使启用 gtid_mode=ON → 触发器语义在主从两端不对等的问题,GTID 也无法解决
大事务 + 触发器 = 延迟雪崩
想象一个典型的批量导入场景:一条INSERT INTO orders (...) SELECT ... FROM staging;插入 10 万行,每行都触发一个AFTER INSERT去更新统计表。主库上还能依靠 bulk insert 缓冲扛一下,但从库的SQL thread只能逐行回放——每回放一行,就执行一次触发器逻辑。原本一次批量操作,硬生生被拆解成 10 万次单行累积执行。
此时你去观察从库状态,会看到Relay_Log_Space增长缓慢,但Seconds_Behind_Master直线上升,Exec_Master_Log_Pos几乎停滞不动。这就是典型的“延迟雪崩”。
如何规避?几个关键原则:
- 在高写入量的表上尽量不定义触发器,尤其是汇总、计数、审计这类逻辑
- 改用应用层的异步任务(比如 Kafka+Flink)或者定时 job 来做统计,不要指望从库实时触发
- 实在要用,确保触发器内操作足够轻量——只写内存表、不带 JOIN、没有子查询
从库禁用触发器不是万能解法
有人尝试在从库执行SET sql_log_bin = 0;后直接删掉触发器,以为能跳过逻辑。这种做法非常危险。一旦主库后续做了 DDL 修改触发器定义,或者这个触发器本身就是业务一致性的关键路径(比如生成唯一流水号),从库缺失它,数据会直接错乱。
更稳妥的做法是什么?根本不要在依赖主从复制的架构里,把触发器当作跨表联动的工具。真正需要强一致性的关联写入,应该由应用层统一控制事务边界;弱一致场景(比如日志归档),用EVENT或外部调度来替代。
还有一个容易被忽略的点:即使你本人没有主动创建触发器,某些 ORM 框架(比如 Django 的pre_sa ve)或者中间件(比如 ShardingSphere 的自动分表路由)可能在连接层注入了类触发行为。这些行为到了从库的线程模型下,同样会暴露出放大效应。排查延迟问题时,记得把这层也考虑进去。
