必须用BEFORE,因积分变动需在业务数据落库前完成校验与计算,避免扣超后回滚成本高;BEFORE可读写NEW字段做预处理,AFTER仅能查或发通知,不适用于积分核算。

触发器执行时机选 BEFORE 还是 AFTER?
积分变动必须在业务数据落库前完成校验和计算,否则会出现“订单已提交但积分不足却扣成功”这类逻辑漏洞。所以所有涉及积分变更的触发器都该用 BEFORE INSERT 或 BEFORE UPDATE —— 这样你能在写入主表前检查 member_points 是否足够,甚至直接修改即将插入的行(比如把负积分修正为 0)。
用 AFTER 的典型后果是:事务已提交,积分扣超了,回滚成本高,还得额外补救逻辑。
-
BEFORE触发器中可读写NEW行的字段(如NEW.points_change),适合做预处理 -
AFTER触发器不能改NEW或OLD,只能查或发通知,不适合积分核算 - 若需记录积分流水,可在
BEFORE中完成主表更新,在AFTER中插入日志(但注意跨表事务一致性)
如何避免触发器里查不到最新会员余额?
常见错误是在触发器里写 SELECT points INTO @p FROM members WHERE id = NEW.member_id,结果查到的是事务开始时的快照值,不是本事务中前面已更新过的积分。MySQL 默认隔离级别下,触发器内无法看到同事务中其他语句对同一行的修改。
正确做法是:把积分字段冗余进业务表(如订单表加 before_points、after_points),或在应用层统一管理积分状态,触发器只做“原子更新”:
UPDATE members SET points = points + NEW.change_value WHERE id = NEW.member_id;
这条语句本身是原子的,不依赖先查后算,也规避了幻读和并发覆盖问题。
- 禁止在触发器中做“查→算→更”三步操作
- 用
UPDATE ... SET points = points + ?替代SELECT + UPDATE - 如果必须判断余额(如扣积分前校验),用
SELECT ... FOR UPDATE显式加行锁,但会降低并发度,慎用
INSERT 和 UPDATE 触发器怎么区分积分方向?
积分累加(如签到、购物返利)和扣除(如兑换、退款)往往发生在不同事件上,但可能共用一张操作表(如 points_log)。关键不是靠表名判断,而是看 NEW.change_value 的正负号:
假设 points_log(change_value INT, reason VARCHAR(20)),则:
- 当
NEW.change_value > 0:执行累加,同时可校验上限(如单日最多+10000) - 当
NEW.change_value < 0:执行扣除,必须检查members.points + NEW.change_value >= 0 - 若
NEW.reason = 'refund'且NEW.change_value > 0,说明设计反了——要统一语义,避免靠字符串判断逻辑
别写两个触发器分别处理“加”和“减”,维护成本高;一个触发器内用条件分支更可控。
为什么触发器更新失败时整个事务会回滚?
这是 MySQL 的默认行为:触发器内任何 SQL 报错(比如违反外键、除零、子查询返回多行),都会导致当前 DML 语句失败,并让整个事务回滚。这其实是优点——保证积分和业务状态强一致。
但要注意几个易踩的坑:
- 在触发器里调用存储函数,而函数里用了未声明
READS SQL DATA的查询,可能报错 -
INSERT INTO points_log失败(如字段超长、NOT NULL 缺失),会连带让原订单插入失败 - 触发器里写
SELECT ... INTO但没匹配到行,MySQL 会抛NO DATA异常,必须用DECLARE CONTINUE HANDLER捕获
真正难处理的是分布式场景:积分表和订单表不在同一库,触发器无能为力。这时候就得放弃触发器,改用应用层两阶段提交或消息队列最终一致。

















