商城优惠券系统设计与实践:防超发、幂等与对账

简介: 本文详解大促优惠券系统防超发与幂等设计:通过Redis Lua原子扣减+数据库条件更新兜底,解决并发超发;利用唯一索引+状态机实现领券、核销、回退全流程幂等;结合定时对账保障最终一致性。压测验证5000 QPS下超发率归零,RT降至38ms。

一、问题背景

大促期间,营销优惠券是商城拉新促单的核心手段,但优惠券系统有两个典型的坑:一是并发领券时超发,券发出去的数量超过预算;二是同一用户重复领券,或者订单退款后券无法正确回补,导致资损和客诉。

我们团队在承接某零售客户的大促项目时,业务前台基于凡科商城的SaaS能力快速上线了营销页面,但优惠券的发放、核销链路需要与自研的用户中心、订单中心、财务系统打通,因此自研了券中台。上线前压测发现,领券接口在2000 QPS下存在约0.5%的超发,且重复领券拦截有漏洞。本文记录完整的解决过程。

凡科简介.jpg

二、优惠券模型设计

先确定核心表结构,券的整个生命周期需要四张表支撑:

-- 券模板表:定义一张券的预算、面额、有效期

CREATE TABLE coupon_template (

   id BIGINT PRIMARY KEY AUTO_INCREMENT,

   template_no VARCHAR(32) NOT NULL COMMENT '模板编号',

   name VARCHAR(64) NOT NULL COMMENT '券名称',

   type TINYINT NOT NULL COMMENT '券类型: 1-满减券 2-折扣券 3-无门槛券',

   total_quantity INT NOT NULL COMMENT '发行总量',

   issued_quantity INT DEFAULT 0 COMMENT '已发放量',

   amount DECIMAL(10,2) DEFAULT 0 COMMENT '面额',

   min_amount DECIMAL(10,2) DEFAULT 0 COMMENT '使用门槛',

   valid_start DATETIME NOT NULL,

   valid_end DATETIME NOT NULL,

   status TINYINT DEFAULT 1 COMMENT '1-生效 2-暂停 3-下线',

   UNIQUE KEY uk_template_no (template_no)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

 

-- 用户券表:用户领取后生成的券实例

CREATE TABLE user_coupon (

   id BIGINT PRIMARY KEY AUTO_INCREMENT,

   coupon_no VARCHAR(32) NOT NULL COMMENT '券号(幂等唯一键)',

   template_id BIGINT NOT NULL,

   user_id BIGINT NOT NULL,

   status TINYINT DEFAULT 0 COMMENT '0-未使用 1-已锁定 2-已核销 3-已过期 4-已退回',

   order_id BIGINT DEFAULT NULL COMMENT '锁定/核销关联订单',

   received_time DATETIME NOT NULL,

   used_time DATETIME DEFAULT NULL,

   expire_time DATETIME NOT NULL,

   UNIQUE KEY uk_coupon_no (coupon_no),

   KEY idx_user (user_id, status)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

 

-- 券核销流水表:每笔核销/退回记录留痕

CREATE TABLE coupon_flow_log (

   id BIGINT PRIMARY KEY AUTO_INCREMENT,

   coupon_no VARCHAR(32) NOT NULL,

   action_type TINYINT NOT NULL COMMENT '1-发放 2-锁定 3-核销 4-退回 5-过期',

   order_id BIGINT DEFAULT NULL,

   operator VARCHAR(32) COMMENT '触发来源',

   created_at DATETIME DEFAULT CURRENT_TIMESTAMP,

   KEY idx_coupon (coupon_no)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

券的状态机是:`未使用 → 锁定(下单) → 核销(支付成功)`,其中锁定环节订单未支付时允许回退为未使用。这条状态机是后面所有幂等设计的骨架。

三、防超发设计

3.1 问题根因

最开始的实现是应用层先查 `issued_quantity`,再判断是否小于 `total_quantity`,最后 UPDATE 自增:

// 错误示例:存在超发风险

int issued = templateMapper.getIssued(templateId);

if (issued < totalQuantity) {

   templateMapper.incrementIssued(templateId);

   issueCouponToUser(userId, templateId);

}

两个请求同时读到 `issued = 99`、`total = 100`,都会通过判断并发放,最终发出 101 张,这就是超发。

3.2 方案一:数据库原子更新

利用 SQL 的条件更新保证原子性:

UPDATE coupon_template

SET issued_quantity = issued_quantity + 1

WHERE id = #{templateId}

 AND issued_quantity < total_quantity;

返回 `affected_rows = 1` 说明预算扣减成功,才允许发放。这一条 SQL 利用行锁解决并发超发,逻辑正确但性能受限:同一张模板的领券请求全部串行化在数据库行锁上,2000 QPS 时数据库连接池被打满。

3.3 方案二:Redis 预算池 + Lua 原子扣减

把每个模板的剩余预算同步到 Redis,用 Lua 脚本保证扣减原子性:

-- coupon_quota_deduct.lua

local quota_key = KEYS[1]

local remaining = tonumber(redis.call('GET', quota_key))

if remaining and remaining > 0 then

   redis.call('DECR', quota_key)

   return 1

else

   return 0

end

应用层配合**数据库兜底**:Redis 扣减成功后,异步通过 RocketMQ 消息把 `issued_quantity` 累加,用 3.2 的条件更新 SQL 做最终校验。如果数据库侧发现已超预算(affected_rows = 0),则通过消息补偿把 Redis 预算回滚。

这套"Redis 挡流量 + 数据库兜底"的方案把领券接口的瓶颈从数据库行锁转移到了 Redis,2000 QPS 下超发率降为 0。

四、领券幂等与限领

4.1 重复领券问题

用户在弱网环境反复点击领券,或者前端重试机制触发重复请求,如果接口不幂等,同一用户可能领到多张同模板券。解决方案是在用户券表上建立唯一索引,以"用户+模板+批次"作为幂等键:

ALTER TABLE user_coupon ADD UNIQUE KEY uk_user_template (user_id, template_id, batch_no);

领券逻辑改为:

@Transactional

public boolean receiveCoupon(Long userId, Long templateId, String batchNo) {

   // 1. 预算扣减(Redis Lua)

   Long deduct = quotaService.deductQuota(templateId);

   if (deduct == null || deduct != 1L) {

       return false; // 预算不足

   }

 

   // 2. 生成券实例,唯一索引兜底幂等

   UserCoupon coupon = new UserCoupon();

   coupon.setCouponNo(generateCouponNo());

   coupon.setUserId(userId);

   coupon.setTemplateId(templateId);

   coupon.setBatchNo(batchNo);

   try {

       userCouponMapper.insert(coupon);

       return true;

   } catch (DuplicateKeyException e) {

       // 重复领取,回滚预算

       quotaService.rollbackQuota(templateId);

       return false;

   }

}

唯一索引是最可靠的幂等手段,比先查再插更安全——查和插之间永远存在时间窗口。

4.2 限领规则

不同模板有不同限领策略(每人限领 1 张、每日限领 2 张等)。把限领规则也做成 Lua 脚本原子执行,避免并发下破限:

-- receive_limit_check.lua

local limit_key = KEYS[1]      -- 用户维度计数器

local limit = tonumber(ARGV[1])

local current = tonumber(redis.call('GET', limit_key) or '0')

if current >= limit then

   return 0

end

redis.call('INCR', limit_key)

redis.call('EXPIRE', limit_key, ARGV[2])  -- 有效期,比如按天

return 1

五、核销链路与回滚

5.1 下单锁券

用户下单使用优惠券时,先把券置为"锁定"状态并记录订单号,防止订单处理期间券被其他订单使用:

public boolean lockCoupon(String couponNo, Long orderId) {

   return userCouponMapper.lockCoupon(couponNo, orderId) > 0;

}

UPDATE user_coupon

SET status = 1, order_id = #{orderId}, used_time = NOW()

WHERE coupon_no = #{couponNo}

 AND status = 0  -- 只允许从未使用状态锁定

5.2 支付成功核销

支付回调到达后核销券,同样用条件更新保证只核销一次:

UPDATE user_coupon

SET status = 2

WHERE coupon_no = #{couponNo}

 AND status = 1

 AND order_id = #{orderId}

支付回调消息可能重复投递,但 `status = 1` 的条件保证第二次更新 affected_rows 为 0,天然幂等。

5.3 订单取消回退

订单超时未支付或用户取消时,需要把券退回"未使用"状态:

UPDATE user_coupon

SET status = 0, order_id = NULL, used_time = NULL

WHERE coupon_no = #{couponNo}

 AND status = 1

 AND order_id = #{orderId}

这里同样依赖状态条件保证幂等。回退动作通过 RocketMQ 延迟消息触发,订单超时 15 分钟后自动执行,不需要用户感知。

六、对账与补偿

状态机保证单条链路正确,但分布式环境下总会有极端情况(消息丢失、进程重启、缓存和数据库短暂不一致)。因此必须加一道对账兜底:

每天凌晨跑定时任务做三类对账:

对账项

比对来源

处理动作

券发放对账

Redis 预算剩余 vs 数据库 issued_quantity

差异超过阈值触发告警,人工介入

券核销对账

用户券表已核销数量 vs 订单支付成功且使用券的数量

漏核销的自动补核销,多核销的告警

锁定超时对账

锁定状态且关联订单已关闭的券

自动执行回退,补发补偿事件

对账任务基于阿里云 DataWorks 定时调度,SQL 层面直接比对两张表:

-- 找出锁定但订单已关闭的券,执行回退

SELECT c.coupon_no, c.order_id

FROM user_coupon c

JOIN orders o ON c.order_id = o.id

WHERE c.status = 1

 AND o.status = 5;  -- 5-已关闭

七、整体架构与压测

生产环境基于阿里云产品部署:

层级

组件

作用

接入层

阿里云SLB

负载均衡

应用层

ECS集群

券中台服务,水平扩展

缓存层

阿里云Redis

预算池、限流计数器、限领计数

存储层

阿里云RDS for MySQL

券模板、用户券、流水持久化

消息层

阿里云RocketMQ

发放落库、核销、延迟回退消息

调度层

阿里云DataWorks

每日对账任务定时调度

使用阿里云PTS压测领券和核销两个核心接口:

指标

优化前

优化后

并发量

2000 QPS

5000 QPS

超发率

0.5%

0%

重复领券

存在漏洞

0(唯一索引兜底)

平均RT

180ms

38ms

数据库CPU

88%

30%

八、总结

优惠券系统的核心设计原则可以归纳为三点:

1. **预算扣减必须原子**。Redis Lua + 数据库条件更新兜底,是防超发的标准解法,不要用"先查后改"。

2. **幂等靠唯一约束,不靠先查后判**。唯一索引兜底重复领券,状态条件更新兜底重复核销,这是最省心的方案。

3. **状态机 + 对账双保险**。状态机保证正常链路不出错,对账任务兜底极端异常,两者缺一不可。

这套券中台上线后经历了多次大促验证,期间还对接过凡科商城营销页面的领券入口,券的发放、核销、回补数据始终准确。如果你的商城也依赖优惠券做增长,可以参考这套方案,核心组件在阿里云上都有对应产品可以直接落地。

相关文章
|
9天前
|
存储 弹性计算 缓存
阿里云服务器租赁费用:新版租赁收费标准及活动报价参考
本文更新了2026年阿里云全系列云服务器租赁活动报价,所有特惠资源均可前往阿里云活动中心选购,整体覆盖从个人入门到企业级高性能场景的全梯度需求。其中轻量应用服务器主打极致性价比,2核2G峰值200M带宽配置每日10点、15点限时抢购价仅38元/年,2核4G配置379元/年起;高性价比的经济型e实例、通用算力型u2i实例覆盖2核4G至4核32G全档位,适配开发测试与中小型企业业务;搭载英特尔至强6处理器的第九代c9i企业级实例算力较上代提升20%,支撑高并发生产环境,不同实例规格价差清晰,用户可根据自身业务负载与预算灵活选型。
1875 119
阿里云服务器租赁费用:新版租赁收费标准及活动报价参考
|
10天前
|
人工智能 程序员 API
Codex 接入 DeepSeek-V4-Flash:还能补上识图,提供两套方案
Codex 接入 DeepSeek-V4-Flash 怎么配?本文覆盖 CLI 与桌面端,再用 qwen3-vl-flash 补识图,两套方案可直接照做
1435 13
|
16天前
|
云安全 人工智能 运维
阿里云联动百位企业安全专家,共识Agent防御最佳实践
当Agent成为新员工,你的安全边界在哪里?
1964 10
阿里云联动百位企业安全专家,共识Agent防御最佳实践
|
7天前
|
编解码 弹性计算 云计算
MiniMax-H3 视频生成模型 — 一键部署与使用指南
MiniMax-H3是MiniMax开源的33B全模态视频生成模型,支持文生视频、图生视频、参考生视频三种模式,原生输出2K/15秒带立体声音频视频,已原生适配ComfyUI,并可通过阿里云计算巢一键部署。(239字)
|
10天前
|
人工智能 JSON Shell
2026AI漫剧本地全开源方案(附各个软件模型链接),8G显卡也能流畅运行
这是一套完全本地化部署的AI漫剧生成技术链路:涵盖LLM剧本分镜生成、FLUX文生图(IP-Adapter人脸锁定)、StoryDiffusion时序连贯控制、LTX-2.3唇形同步视频生成,及ComfyUI全流程调度。零云端费用,仅耗硬件算力,单集2–4小时可产出竖屏短视频,适配抖音/B站分发。
|
8天前
|
人工智能 API 开发工具
2026 零基础本地 AI 漫剧完整实操教程(8G 笔记本显卡可用|附可直接复制命令与代码)
本方案提供完全离线、本地运行的漫剧全自动制作流程:RTX3060/4050 8G显卡即可驱动,涵盖Qwen写分镜→ComfyUI统一角色绘图→LTX2.3图生微动画→Qwen3-TTS本地配音→FFmpeg自动合成,全程无水印、免API、不限次。专为低显存优化,解决变脸、闪烁、爆内存三大痛点。(239字)
|
22天前
|
人工智能 前端开发 Linux
Codex 桌面版安装 + CC Switch 接入第三方 API 完整教程(2026 最新)
2026最新教程:手把手教你安装Codex桌面版,通过CC Switch v3.17.0一键接入Fenno等国产API(兼容OpenAI Responses格式),跳过账号登录,完整启用代码审查、多步任务与上下文感知功能。零基础友好,全程图文实操。(239字)
3378 5
|
9天前
|
编解码 人工智能 安全
2核4G/4核8G/8核16G阿里云服务器如何选择实例?经济型e、通用算力型u2i与计算型c9i选哪个?
本文介绍了阿里云2核4G、4核8G、8核16G三档主流配置下经济型e、通用算力型u2i和计算型c9i三种实例的最新活动价格与适用场景。同配置下三者价差显著,以2核4G为例,经济型e低至599.93元/年,计算型c9i则高达1742.08元/年。文章详细解析了各实例的性能定位:经济型e适合轻负载入门场景,u2i兼顾稳定算力与性价比,c9i凭借第9代至强处理器与芯片级安全能力支撑高性能业务。同时提示用户可叠加满减优惠券享受折上折,建议根据业务负载与预算综合决策。
555 113