必须用 before update 触发器实时拦截核销,因过期是动态时间状态,after insert 无法响应时效变化;触发器内用 now() 对比 expire_at,若过期则设 new.status = 'expired' 并使 update 影响行为 0 行,实现无感降级。

优惠券过期状态不能靠定时任务轮询或应用层判断来“兜底”——必须在数据库层面用 BEFORE UPDATE 或 AFTER UPDATE 触发器实时拦截和修正,否则并发下单时会出现已过期券仍被核销的严重资损。
为什么不能用 AFTER INSERT 触发器自动设 expired = 1?
因为过期是时间维度的动态状态,不是插入时就能确定的静态属性。一张券可能刚插入时未过期,但几小时后就该失效;而 AFTER INSERT 只执行一次,无法响应时间推移。真正需要的是:每次查询/核销前检查有效期,并在必要时原子化更新状态。
- 正确做法是把过期判断逻辑下沉到核销前的
BEFORE UPDATE触发器中,针对status字段变更(如从'unused'→'used')做拦截 - 触发器内用
NOW()对比expire_at,若已过期则直接将NEW.status改为'expired',并抛出警告(不中断事务)或设置NEW.status = 'invalid' - 避免在触发器里调用存储过程或复杂子查询,否则会显著拖慢
UPDATE性能
BEFORE UPDATE 触发器中如何安全判断过期并阻止核销?
关键不是“阻止”,而是“无感降级”:让核销语句本身返回影响行为 0 行,而不是报错中断事务。这样上层应用可统一按“影响行数 == 0”处理失败,无需额外捕获 SQL 异常。
- 触发器内不要用
SIGNAL SQLSTATE抛异常(会中断事务),改用条件赋值:IF NEW.status = 'used' AND OLD.status = 'unused' AND OLD.expire_at - 必须限定作用范围:
WHERE status IN ('unused', 'used')的 UPDATE 才进触发器,避免全表 UPDATE 意外触发 -
expire_at字段务必建索引,否则每次核销都要全表扫描判断过期
MySQL 8.0+ 中触发器读取当前时间的精度陷阱
NOW() 在同一个事务中返回固定时间戳,但优惠券核销通常跨多个事务(查券、锁券、扣减库存、写日志),这意味着不同环节看到的“当前时间”可能不一致。更稳妥的是用 SYSDATE(),它每次调用都返回真实系统时间。
- 错误写法:
IF OLD.expire_at —— 若事务开启早于过期时刻,即使现在已过期,<code>NOW()仍返回旧值 - 正确写法:
IF OLD.expire_at ,确保每次判断都是真实此刻 - 注意:
SYSDATE()无法被查询缓存,但优惠券核销本就不该走缓存,影响可忽略
触发器不是银弹:它只管单行状态变更,无法解决分布式场景下多服务同时读写同一张券的问题。真正可靠的方案,是在触发器基础上叠加数据库行锁(SELECT ... FOR UPDATE)或乐观锁(version 字段),否则两个请求几乎同时读到“未过期”,都会进入触发器并成功更新——这是最容易被忽略的竞态点。











