statement模式下触发器必然双写:主库执行insert时触发一次,从库重放sql时再次触发,导致audit_log多出一行、主从行数不一致;row模式虽不重放触发器,但若调用uuid()/now()、更新其他表或操作临时表,仍会造成主从不一致。

STATEMENT模式下触发器必然执行两次
主库执行一条INSERT,触发器顺带往audit_log写日志;从库重放时,binlog_format=STATEMENT会让SQL Thread再次执行这条INSERT,并再次触发同一逻辑——结果就是audit_log多出一行,主从行数对不上。这不是bug,是STATEMENT格式的设计行为:它只记原始语句,不记“是否已触发过”。
常见现象包括:ERROR 1062 Duplicate entry(唯一键冲突)、ERROR 1452 Cannot add or update a child row(外键约束失败)、计数字段比主库多1。这些错误在SHOW SLAVE STATUS\G里往往显示Slave_SQL_Running: No,但Last_Error可能被掩盖成“语句执行成功”,必须查错误日志才能定位。
- 别指望
SET sql_log_bin = 0在从库临时禁用触发器——它只关binlog写入,不阻止触发器运行,且会破坏复制链路语义 -
SHOW TRIGGERS在从库能看到定义,但不代表它被调用;实际是否执行取决于binlog_format和SQL SECURITY,不是看有没有定义 - MIXED模式不可靠:MySQL自动降级到STATEMENT时,触发器照样双写,且无明确日志提示
ROW模式下触发器不执行,但副作用可能丢失
binlog_format=ROW时,主库把触发器产生的最终行变更(比如orders表某行的status从pending变active)写进binlog,从库只应用这个变更,**不重新执行触发器**。这是安全的前提,但仅限于“触发器只改当前表”。
一旦触发器干了这三件事,主从就又脱节了:
- 调用
UUID()、NOW()、RAND()等非确定性函数——ROW只记“值变成什么”,不记“怎么变的”,从库用本地时间/随机数生成新值,和主库不一致 - 更新其他表(如
INSERT INTO points_log)——这些变更不会进binlog,从库永远缺失 - 操作临时表或用户变量(如
@tmp_id)——从库没有上下文,执行直接失败或静默跳过
排查方法很简单:SHOW CREATE TRIGGER t1_ins,然后搜UUID(、NOW(、INSERT INTO 、@这些关键词。
真正有效的修复路径只有两条
硬切配置参数(比如改binlog_format或关log_slave_updates)解决不了根本问题。数据偏差是逻辑层面的,不是日志格式能兜住的。
- 把触发器逻辑移到应用层:时间戳、ID、状态字段由应用生成后传入,避免
NOW()、UUID()在从库重复计算;跨表更新(订单+积分+通知)拆成应用内事务,用本地事件或消息队列保证最终一致性 - 用
GENERATED COLUMN替代简单计算:比如status根据created_at自动设为'pending',不用触发器运行时判断;MySQL 8.0+支持存储式生成列,既免触发器,又保证主从一致
如果必须保留触发器,只允许它做一件事:在BEFORE INSERT/UPDATE中给NEW.field := DEFAULT(field)或NEW.updated_at := NOW()——但后者仍需主从时钟严格同步(chrony误差NOW()本身就会漂移。
已经偏差的数据怎么补?别跳错,先定位缺失点
盲目执行SET GLOBAL sql_slave_skip_counter = 1或pt-slave-restart --skip-count 1只会让问题更隐蔽。主从偏差往往不是单条记录错,而是语义链断裂(比如审计日志少写,导致后续补偿逻辑失效)。
正确做法分三步:
- 用
pt-table-checksum全量比对主从数据差异,指定--nocheck-binlog-format绕过格式校验,聚焦行内容 - 从
Last_Error提取表名和主键条件,比如Can't find record in 'user_points'→ 在主库查SELECT * FROM user_points WHERE id = 12345 - 手动在从库补缺失行(注意字段顺序、默认值、字符集),再
START SLAVE;若量大,用pt-table-sync --execute自动生成修复SQL
最易被忽略的是:触发器更新同一张表的多个字段时,从库SQL线程串行重放可能导致覆盖。例如主库事务中先设status='active'再设updated_at=NOW(),但从库重放时中间插入了其他事务的UPDATE,最终status被回滚为'pending'——这种错序问题无法靠日志格式修复,只能靠应用层幂等或最终一致性设计兜底。











