优惠券库存扣减必须用原子操作,推荐redis的decr命令实现查询与扣减一体化;初始化用setnx防重,expire设过期时间;mysql仅作最终一致性备份,通过消息队列异步落库并保证幂等;核销需双重校验缓存与db状态;全链路嵌入限流、频控与前端防护防刷。

优惠券库存扣减必须用原子操作
高并发下最核心的问题是多个请求同时读取同一张优惠券的剩余数量,然后各自判断“还有库存”,再各自扣减,最终导致超发。解决的关键在于让“查询库存+扣减”变成一个不可分割的操作。
推荐使用 Redis 的 DECR 命令:先将优惠券库存以整数形式存入 Redis(如 coupon:1001:stock),用户领取时执行 DECR coupon:1001:stock。该命令返回扣减后的值,若结果 ≥ 0,说明领取成功;若为 -1,说明已抢光。整个过程由 Redis 单线程保证原子性,无需加锁。
补充建议:
- 初始化库存时用
SETNX防止重复设置 - 配合
EXPIRE设置合理过期时间,避免脏数据长期残留 - MySQL 中对应记录仅作最终一致性备份,不参与实时扣减逻辑
领取成功后需异步落库与幂等写入
Redis 扣减成功只代表资格获取,用户订单、领券记录、日志等仍需写入 MySQL。但直接同步写库会拖慢响应、增加 DB 压力,且可能因网络或事务失败导致状态不一致。
应采用「Redis 扣减 + 消息队列异步落库」方案:领取接口在 Redis 返回非负值后,立即向 Kafka/RabbitMQ 发送一条领券事件(含用户 ID、优惠券 ID、时间戳、唯一 trace_id)。消费端按序处理,插入数据库前先查表确认是否已存在该用户-优惠券组合(基于联合唯一索引),避免重复写入。
关键点:
- MySQL 表设计需包含
(user_id, coupon_id)唯一索引 - 消息体携带全局唯一 ID(如雪花 ID),便于去重和排查
- 消费端失败需支持重试 + 死信隔离,防止阻塞
核销环节要校验双重有效性
用户下单时提交优惠券,系统不能只查“有没有这张券”,还要确保它没被用过、没过期、属于当前用户、且适用本订单(如满减门槛、品类限制)。
核销流程建议分两步走:
-
前置校验:从 Redis 或缓存中快速读取券状态(如
coupon:1001:user:2001:status),检查是否为used或expired -
DB 最终确认:查 MySQL 订单关联表,确认该券未被其他订单锁定或核销,并更新其状态为
used(用UPDATE ... WHERE status = 'unused'加条件更新,影响行为 1 才算真正核销成功)
这样既保证性能,又守住数据准确底线。失败时及时提示具体原因(如“该券已被使用”或“不满足满 200 减 20 条件”),提升用户体验。
防刷与限流要嵌入全链路
技术再稳,也架不住恶意脚本高频请求。防超卖不只是数据库/缓存的事,更是风控问题。
在入口层就应做收敛:
- Nginx 层按 IP 或用户 token 限速(如 5 次/秒),拦截明显异常流量
- PHP 接口层结合 Redis 实现滑动窗口计数,对同一用户领取同一券做频次控制(如 1 小时最多领 3 张)
- 前端按钮点击后置灰 + 倒计时,配合验证码(如极验)或设备指纹识别,提高自动化成本
所有风控动作都应记录日志并打标,便于后续分析攻击模式和优化策略。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











