触发器不能提交或回滚事务,仅适合轻量检查或本地更新;跨库/发消息等操作须交由应用层或异步服务完成,通过tx_outbox表+对账补偿实现最终一致性。

触发器里不能直接提交或回滚事务
MySQL 和 PostgreSQL 的触发器运行在父语句的事务上下文中,AUTOCOMMIT 关闭时,COMMIT 或 ROLLBACK 会报错:ERROR 1609 (HY000): Transaction is not allowed in stored function or trigger。想靠触发器“自己搞定一致性”是行不通的。
常见错误现象:在 INSERT 触发器里调用 INSERT INTO remote_log 后立刻 COMMIT,结果整个主事务失败,但日志已写入——反而破坏一致性。
- 触发器只适合做轻量、无副作用的检查或本地状态更新(如自增字段、时间戳)
- 跨库、发消息、调外部 API 这类操作,必须交给应用层或异步服务承接
- PostgreSQL 的
AFTER STATEMENT触发器虽支持pg_notify(),但通知本身不保证下游消费成功,仍属“尽力而为”
用触发器写事务日志表,但别指望它自动重试
最终一致性真正落地,得靠一个独立的“对账+补偿”服务轮询日志表。触发器只负责把变更可靠记下来,比如:
CREATE TABLE tx_outbox (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
event_type VARCHAR(32) NOT NULL,
payload JSON NOT NULL,
status ENUM('pending', 'sent', 'failed') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
关键点不是“怎么写”,而是“怎么用”:
- 所有业务逻辑写完后,在同一事务中
INSERT INTO tx_outbox——这样能保证业务和日志原子性 -
status字段必须设为pending,且不能被触发器改写;后续由外部服务更新 - 避免在日志表里存大字段(如
TEXT或长JSON),否则影响轮询性能和 binlog 体积 - MySQL 5.7+ 建议用
ROW_FORMAT=COMPRESSED+KEY_BLOCK_SIZE=4控制日志表膨胀
分布式事务失败后,触发器无法感知远程结果
假设你在订单表 orders 的 AFTER INSERT 触发器里往 Kafka 发消息,但 Kafka 不可用。触发器不会抛异常(除非你显式 SIGNAL),主事务照常提交,数据就“看起来成功了”,实际下游没收到。
这不是触发器的错,是模型误用:
- 触发器没有网络 I/O 能力,所谓“发消息”其实是往本地表写记录,或调用 UDF(极不推荐)
- 真正发消息的动作必须由应用层或独立消费者完成,且要带重试、死信、幂等 key
- 如果硬要用触发器联动,唯一安全路径是:触发器写
tx_outbox→ 应用监听 binlog 解析该表 → 消费并更新status - 注意 MySQL 的
binlog_format=ROW才能捕获tx_outbox变更;MIXED或STATEMENT会丢事件
为什么不用 XA 或 Seata?因为触发器根本不参与两阶段
XA 是协调者驱动的同步协议,要求所有参与者支持 PREPARE/COMMIT 接口。触发器只是数据库内部的钩子,既不暴露接口,也不注册到事务管理器。
所以别纠结“怎么让触发器接入 Seata”——它压根不在同一个抽象层上:
- Seata 的
AT模式靠解析 SQL 自动生成 undo_log,对触发器无感知 - 如果你在触发器里修改了别的表,那些变更不会被 Seata 管理,可能造成脏写
- 真正需要强一致的场景(如资金扣减),应放弃触发器,改用应用层的 TCC 或 Saga 模式
- 触发器唯一合理的角色,就是当“快照记录员”:只写、不读、不跳转、不决策
最常被忽略的一点:tx_outbox 表本身要加唯一索引防重复写入,比如 (event_type, business_id),否则并发插入可能产生双写,下游就得靠幂等处理——这又绕回去了。

















