触发器适合轻量校验而非事务控制,可拦截非法操作或记录对账任务,但不能保证跨语句余额一致;可靠一致性需应用层事务+定时对账。

触发器能实时校验余额一致性,但不能替代应用层事务控制
MySQL 触发器可以监听 INSERT、UPDATE、DELETE 流水操作,并在执行前后检查或修正余额,但它运行在单条语句上下文中,无法感知跨语句的事务边界。如果一笔业务涉及多条流水(如转账拆为“扣减A”+“增加B”),仅靠触发器无法保证二者余额同步更新——任一语句失败时,另一个表可能已误改。
常见错误现象:ERROR 1442 (HY000): Can't update table 'balance' in stored function/trigger because it is already used by statement which invoked this stored function/trigger,这是在触发器里直接修改同一事务中正被操作的表导致的。
- 触发器适合做轻量级校验(如抛出异常中断非法操作),而非主动修正余额
- 若需自动修正,应在应用层用显式事务包裹流水写入 + 余额更新,再辅以定时对账任务兜底
- 触发器中的
NEW和OLD只反映当前行变更,不包含关联账户信息,跨账户逻辑需额外 JOIN 或子查询(性能敏感)
用 BEFORE INSERT 触发器拦截非法流水金额
在插入新流水前验证金额是否合理(例如非零、符号与业务类型匹配),避免脏数据污染后续对账。这是最安全、开销最小的触发器使用方式,不涉及表间写操作,无递归风险。
示例:限制支出类流水(type = 'OUT')金额必须为负数
CREATE TRIGGER check_transaction_amount_before_insert
BEFORE INSERT ON transaction_log
FOR EACH ROW
BEGIN
IF NEW.type = 'OUT' AND NEW.amount >= 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'OUT transaction amount must be negative';
END IF;
END;
- 使用
SIGNAL主动报错可阻止语句执行,比事后修正更可靠 - 避免在触发器中调用函数或查表(如查当前余额),否则会显著拖慢批量插入
- 注意字符集和排序规则:
NEW.type比较时若字段是VARCHAR且含空格,建议加TRIM()
用 AFTER UPDATE 触发器驱动异步对账日志(非实时修正)
不要在触发器里直接更新 balance 表,但可以往对账任务表插入待处理记录,由后台任务统一拉取、校验、修复。这种方式解耦了写入路径与一致性保障,规避了触发器递归和锁竞争问题。
示例:当流水状态变为 'CONFIRMED',记一条对账待办
CREATE TRIGGER log_reconciliation_task_after_update
AFTER UPDATE ON transaction_log
FOR EACH ROW
BEGIN
IF OLD.status != 'CONFIRMED' AND NEW.status = 'CONFIRMED' THEN
INSERT INTO reconciliation_queue (tx_id, account_id, created_at)
VALUES (NEW.id, NEW.account_id, NOW());
END IF;
END;
-
reconciliation_queue表应有唯一索引(如(tx_id, account_id))防止重复入队 - 后台任务需按
tx_id重新计算该笔流水对账户余额的影响,并与balance表比对,差异大于阈值才告警或自动补偿 - 切勿在触发器中执行
SELECT ... FOR UPDATE,极易引发死锁
真正可靠的对账必须依赖定时任务 + 幂等修复逻辑
触发器只是辅助手段,生产环境的余额一致性最终靠的是周期性全量/增量对账。比如每5分钟扫描最近1小时的流水,按 account_id 分组重算余额,与 balance 表当前值比对。
关键点不在“怎么算”,而在“怎么修”:
- 修复语句必须带版本号或时间戳条件,例如
UPDATE balance SET amount = ? WHERE account_id = ? AND updated_at < ?,避免覆盖其他并发更新 - 每次修复生成审计日志(含旧值、新值、触发原因),便于追溯
- 对账任务本身要支持断点续跑:记录最后处理的
transaction_log.id,崩溃后从该位置继续
最容易被忽略的是:流水表和余额表的金额精度是否一致?DECIMAL(18,2) 和 DECIMAL(18,6) 直接相加会导致隐式截断,对账差异可能就来自这里。


















