触发器在主从复制中必然导致数据不一致:STATEMENT模式下必然双写或漏写,ROW模式虽不重放触发器但无法保证非确定函数、临时表操作及跨表更新的一致性,根本解法是将逻辑移至应用层或改用GENERATED COLUMN。

触发器在主从复制中不是“可能”导致不一致,而是只要用了,就大概率出问题——根本原因不在你写的逻辑多严谨,而在于 MySQL 复制机制和触发器执行时机的天然冲突。
STATEMENT 模式下触发器必然双写或漏写
MySQL 在 binlog_format = STATEMENT 时只记录原始 SQL,比如 INSERT INTO orders,完全不记录触发器干了什么。从库回放时,会原样再执行一遍这条语句,连带触发器也再跑一次。
- 主库插入一行 + 触发器生成
NOW()时间戳 + 往audit_log写日志 - 从库重放时:再次插入 + 再次调用
NOW()(时间不同)+ 再次往audit_log写(但该表变更不进 binlog,从库永远没这条)
结果就是:时间字段对不上、audit_log 少记录、甚至唯一键冲突报错 ERROR 1062。
ROW 模式也不能保证触发器安全
binlog_format = ROW 虽然只记录最终行变更,看似绕过了触发器重放,但它只管“主表”,不管触发器副作用:
- 调用
UUID()、RAND()、SYSDATE():ROW 记的是值,但触发器在从库执行时又生成新值,主从字段直接不等 - 操作临时表或用户变量(如
@tmp_id):从库没有上下文,执行失败或静默跳过 - 更新其他表(如订单插入后自动加积分):ROW 日志不含积分表变更,从库永远缺这一环
也就是说,ROW 模式只是让问题更隐蔽,不是解决它。
从库是否执行触发器取决于运行时状态
别只看配置文件,SHOW CREATE TRIGGER t1_ins 查出来的 SQL SECURITY DEFINER 和当前会话的 @@sql_log_bin 才决定行为:
-
SELECT @@sql_log_bin返回 1:当前会话写 binlog,触发器默认执行;返回 0 则不写,但触发器仍可能运行(除非被权限拦住) -
DEFINER用户若在从库不存在,或没TRIGGER权限,触发器会静默失败,SHOW TRIGGERS都查不到
这种“看起来在跑、其实没生效”的状态,比直接报错更难排查。
真正能落地的解法是移除触发器副作用
靠改参数、切模式、加同步工具,都是在给定时炸弹换电池。稳定方案必须从源头切断非确定性:
- 时间戳、trace_id 等由应用层生成后传入,避免
NOW()、UUID()在从库重复计算 - 跨表更新(如订单+积分+通知)拆成应用内事务,用本地事件或消息队列保最终一致性
- 简单字段补全(如
status根据created_at自动设为'pending')改用GENERATED COLUMN,不依赖运行时逻辑
触发器最危险的地方,是你以为它只在主库跑了一次,实际上它的影响在复制链路上是不可控发散的——哪怕你确认所有从库都设了 read_only=ON,只要触发器还访问外部状态、调用非确定函数、写其他表,数据偏差就是时间问题。


















