statement模式下触发器必然导致主从双写或漏写,因只记录原始sql且重放时再次执行;row模式虽不重放触发器,但若触发器调用非确定函数、操作临时表或更新其他表,仍会造成主从不一致;根本解法是将触发器逻辑移至应用层或改用generated column。

触发器在STATEMENT模式下必然双写或漏写
MySQL默认STATEMENT格式只记录原始SQL,比如INSERT INTO orders,不记录触发器实际做了什么。从库重放时,会重新执行这条语句+触发器逻辑——如果触发器里用了NOW()、USER()或依赖主库会话变量,结果就和主库不一致。常见现象包括:从库某张日志表多出几行、计数字段对不上、唯一键冲突报错ERROR 1062。
ROW模式也不能保证触发器安全
即使binlog_format=ROW,只要触发器干了这三件事,主从仍会偏离:
- 调用非确定性函数:
UUID()、RAND()、SYSDATE()——ROW只记最终行变更,不保证从库重放时生成相同值 - 操作临时表或用户变量(如
@tmp_id)——从库没有这些上下文,执行直接失败或静默跳过 - 更新其他表(比如主表插入后自动往
audit_log写一条记录)——ROW日志只包含主表变更,audit_log的变更不会进binlog,从库永远缺失
从库是否执行触发器取决于@@sql_log_bin和SQL SECURITY
别只看配置文件,运行时值才决定行为:
- 在从库执行
SELECT @@sql_log_bin;,返回1表示当前会话会写binlog,触发器默认执行;返回0则不写binlog,但触发器仍可能运行(除非被权限或DROP拦住) -
SHOW CREATE TRIGGER t1_ins;查SQL SECURITY:如果是DEFINER,从库会以定义者身份执行——若该用户在从库不存在或没TRIGGER权限,就会静默失败或报错 -
DEFINER用户必须在从库存在且有对应权限,否则SHOW TRIGGERS可能根本查不到这个触发器
真正可靠的解法不是调参数,而是砍掉触发器副作用
把触发器逻辑移到应用层是最稳妥的:
- 时间戳、唯一ID等由应用生成后传入,避免
NOW()、UUID()在从库重复计算 - 跨表更新(如订单+积分+通知)拆成应用内事务,用消息队列或本地事件保证最终一致性
- 简单字段补全(如
status根据created_at自动设为'pending')改用GENERATED COLUMN,不依赖运行时逻辑
ROW模式只是让问题更隐蔽,而不是解决它。只要触发器还访问外部状态、调用非确定函数、写其他表,主从数据偏差就是时间问题。











