发券前须校验活动上线、用户资格、券池余量、个人限领四层状态;数据库操作需事务包裹,先扣库存再记录归属;redis仅作幂等缓存;异步通知须在事务提交后触发;券设expire_at字段实现服务端自动失效。

优惠券发放前必须校验的 4 个状态
发不出去、重复发、超领、发错人——90% 的问题出在发放前没做全状态检查。is_eligible() 不能只查用户登录态,得串起活动生命周期、用户资格、券池余量、个人限领次数四层判断。
-
activity_status === 'online'且start_time ,否则直接拒绝 - 查
user_coupon_log表,用SELECT COUNT(*)算该用户对当前coupon_template_id的已领取数,对比per_user_limit - 查
coupon_pool表的remaining_count,注意这个字段必须用SELECT ... FOR UPDATE加行锁读取 - 若活动支持“新用户专享”,需额外校验
user_register_time和first_order_time是否为空
库存扣减必须用数据库行锁,别信缓存预减
用 Redis DECR 预扣库存再异步落库?线上崩过三次。缓存和 DB 不一致时,券发出去了但 DB 库存没扣,下次查就变成负数,或者被其他请求重复扣。
- 核心操作必须走 MySQL:先
SELECT remaining_count FROM coupon_pool WHERE id = ? FOR UPDATE - 判断
remaining_count > 0,再执行UPDATE coupon_pool SET remaining_count = remaining_count - 1 WHERE id = ? AND remaining_count > 0 - 检查
affected_rows === 1,为 0 说明已被抢完或并发冲突,立刻返回失败 - Redis 只作“是否已领”幂等缓存(
SETNX user:123:coupon:456 1 EX 86400),不参与库存计数
insert into user_coupon 和 update coupon_pool 必须同事务
两个表更新不同步是发券超发的隐形推手。常见错误是先更新券池,再插入用户记录,中间出异常会导致库存虚减;或者反过来,插入成功但库存没扣,下次还能再领。
- 必须包裹在
BEGIN / COMMIT / ROLLBACK中,且隔离级别设为REPEATABLE READ(MySQL 默认) - 顺序固定:先
UPDATE coupon_pool扣库存,再INSERT INTO user_coupon记录归属,最后写user_coupon_log - 不要用 Laravel/Eloquent 的
transaction()套多层模型 save —— 它可能隐式提交部分语句。手写原生 SQL 或至少用 Query Builder 显式控制 - 如果用消息队列异步发券(比如发短信通知),那队列投递必须放在
COMMIT之后,否则事务回滚了消息还在消费
用户领券后失效场景要主动清理
“已领取但未使用”的券长期占着库存,尤其在限时活动中会卡死后续发放。系统不能只靠前端按钮置灰,得有服务端兜底。
- 设置
expire_at字段,值为now() + validity_days * 86400,不是靠定时任务“每天扫一遍过期” - 用户打开领券页时,查
SELECT id FROM user_coupon WHERE user_id = ? AND status = 'unused' AND expire_at ,批量更新为 <code>expired - 对应释放库存:用
UPDATE coupon_pool SET remaining_count = remaining_count + ? WHERE id = ?,注意这里加的是实际过期数量,不是简单 +1 - 避免用
DELETE清理过期记录,保留原始数据用于审计;状态字段足够区分生命周期
真正难的不是发一张券,而是当 2000 人同时点“立即领取”,DB 连接池打满、行锁排队、主从延迟导致从库查不到最新库存——这些细节不压测、不看慢日志、不盯 SHOW ENGINE INNODB STATUS,上线当天必出事。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











