核心是利用BEFORE UPDATE触发器在更新前校验NEW.balance是否合法,如禁止负数、限制变动范围等;AFTER UPDATE无法阻止非法值写入,因更新已完成。

触发器里怎么判断 NEW.balance 是否合法
核心是利用 BEFORE UPDATE 触发器,在更新前检查 NEW.balance 是否符合业务规则。不能等更新完成再查,否则非法值已写入。常见校验逻辑包括:不允许负数、变动幅度不能超阈值、必须为整数(如游戏金币)、或需满足“变动值 = 某个明细表 sum”等前置条件。
示例场景:用户余额只允许通过充值(+)和扣费(−)操作变更,且单次变动不得低于 −1000 或高于 +5000:
DELIMITER $$
CREATE TRIGGER check_balance_update
BEFORE UPDATE ON users
FOR EACH ROW
BEGIN
IF NEW.balance < 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '余额不能为负数';
END IF;
IF NEW.balance - OLD.balance > 5000 OR NEW.balance - OLD.balance < -1000 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '单次余额变动超出允许范围';
END IF;
END$$
DELIMITER ;为什么不能在 AFTER UPDATE 里做校验
AFTER UPDATE 触发器无法阻止非法值写入——它在更新完成后才执行,此时 NEW.balance 已落库。即使你在里面抛错,事务已提交,回滚不了(除非显式加 START TRANSACTION 并手动 ROLLBACK,但触发器本身不支持)。所以所有防御性校验必须放在 BEFORE 阶段。
-
BEFORE UPDATE中修改NEW.balance是允许的(比如自动截断小数),但不推荐用于“修复”非法值,应直接报错更清晰 - 若业务允许“自动归正”,也必须确保逻辑幂等,例如
SET NEW.balance = GREATEST(0, NEW.balance) - 注意:触发器中不能调用含事务控制或外部连接的存储函数,否则可能报错
Can't update table 'xxx' in stored function/trigger
触发器对并发更新的安全性影响
触发器本身不提供额外锁机制,NEW 和 OLD 值基于当前行锁(record lock)下的快照。如果两个事务同时更新同一行,MySQL 会按加锁顺序串行执行触发器,所以校验逻辑不会因并发而失效。但要注意:
- 触发器内不要查其他表做复杂校验(如 SELECT FROM orders WHERE user_id = NEW.id),否则可能引发死锁或性能抖动
- 避免在触发器里调用
SLEEP()、循环等待或远程 API,这会阻塞整个 UPDATE 语句 - 若校验依赖另一张表的聚合结果(如“余额不能超过授信额度”),建议把额度缓存在
users.credit_limit字段,而非实时 JOIN 查询
测试时容易忽略的边界情况
光测 UPDATE users SET balance = -100 WHERE id = 1 不够。真实环境中这些情况常被漏掉:
- NULL 值更新:
UPDATE users SET balance = NULL—— 若字段允许 NULL,需显式判断IF NEW.balance IS NULL THEN ... - 浮点精度问题:如果 balance 是
DECIMAL(12,2),但应用传了100.123,MySQL 会自动四舍五入,触发器看到的是截断后的值,校验要基于这个“入库值”而非原始输入 - 触发器未覆盖 INSERT:新增用户时也可能插入非法余额,需另建
BEFORE INSERT触发器,逻辑复用校验片段 - ALTER TABLE 修改字段类型后,原有触发器仍生效,但若类型变化导致隐式转换(如从 INT 改为 BIGINT),校验表达式行为可能偏移
真正难的不是写触发器,而是厘清“谁有权限改余额”“改之前必须查哪些状态”“失败时前端看到什么错误码”。触发器只是最后一道闸门,上游逻辑没约束好,它天天报警。


















