一个抽奖系统,真正难的不是随机,而是把这4件事拆对
抽奖系统最容易写的是随机数。难的,是不重复、不断货、不中改、奖品可追溯。
先把这4件事处理好,系统才能真正稳定。

不少人以为抽奖难就只在算法:
- 随机数怎么写
- 权重怎么算
但真实线上问题很少是“随机”本身,更多是这4类事故:
- 用户反复点,重复中奖
- 两个人并发抢最后一份库存,系统超卖
- 一套玩法下要维护多套抽奖逻辑
- 中奖后没发到奖,或核销后没法追溯
这篇给你一份可以直接用于一版产品的抽奖架构:先稳住主链路,再逐步扩展。
先说结论:第一版先稳住主链路
抽奖助手首版不先上 Redis / MQ / 调度器,直接用 MySQL 把一致性边界打牢。
- 先把幂等、库存和结果落地做对
- 先把发奖、核销打通并可追溯
- 有了稳定主链路后再考虑异步化
这样能最快确认最核心问题是否正确,不会把复杂度扩散到很多系统里。
这4件事,怎么拆
1. 规则与表现分离:动画只能展示,结果只能由服务端决定
服务端先返回唯一 draw_option_code,前端只负责展示对应结果。
同一个抽奖活动可以有多种展现(大转盘、随机卡片、赛跑),但背后只走一套抽奖判定。
展现配置解析顺序也要固定:
- 有来源绑定配置:用来源配置
- 否则用默认配置
- 都不可用则拒绝访问
这条规则可以避免“页面换了就改结果逻辑”的风险。
2. 奖项与奖品分离:玩法和履约不能混一起
抽奖选项和真实奖品必须分开。
lottery_draw_option:记录抽中了什么(一等奖、二等奖、未中奖)reward_definition:记录发什么(券、实物、虚拟权益)
这样一张券能被不同活动复用,但每个抽奖选项仍有独立库存,业务就不会互相污染。
3. 参与、结果、发奖、核销分离:每张表只回答一类问题
marketing_participation:一次参与行为(含requestId幂等)lottery_draw_record:本次命中的选项reward_issue_order:发奖流程到哪一步verification_voucher+verification_record:核销状态与动作
特别说明:
participant_user_code是原始参与人claim_user_code是领奖人
参与人绑定主账号后,历史参与不能重算,只能改变奖品承接关系。
4. 库存与流水分离:一份库存表,一张流水表
reward_inventory:当前余额,决定能不能发奖reward_inventory_flow:每次变动记录,便于追溯
关键原则:
- 扣减前先做条件判断并原子更新库存
(draw_option_code, business_type, business_code)做唯一键,防重复影响- 结果、库存、流水、发奖同一事务提交,避免半成功状态
抽奖主链路(适合直接放到文章中的流程图说明)

- 用户提交
requestId - 校验幂等,创建参与记录
- 服务端计算
draw_option_code - 命中未中奖:仅落结果
- 命中奖品:库存条件扣减成功后,落结果、发奖单、流水、凭证
- 参与标记为成功
- 事务提交后再播放开奖动画
并发保护点:
- 同一
requestId只允许一条参与记录 - 同一库存变动有幂等唯一键
- 扣减失败时重试其他候选选项,避免“无限重试”
发布时别忘了的边界
- 抽奖只支持已发布且可用状态的活动与展现配置
- 活动至少要有一个启用选项,权重必须有效
- 奖品/库存、发奖和核销状态独立,避免互相覆盖
- 核销不成功要可逆回退,数据仍保留完整链路
可直接放在文末的验收清单
- 同一
requestId不重复抽奖 - 并发只允许最后库存产生一次发奖
- 未中奖不创建发奖单和核销凭证
- 一次中奖只对应一张发奖单
- 一次核销有唯一可追溯记录
- 前端样式调整不改动抽奖判定
技术补充:抽奖助手 ER 图(字段级)
erDiagram
direction LR
产品 ||--o{ 选项 : 配置
选项 ||--|| 库存 : 库存
选项 ||--o{ 结果 : 结果
奖品 ||--o{ 选项 : 奖品
用户 ||--o{ 参与 : 参与
参与 ||--|| 结果 : 命中
结果 ||--o| 发奖单 : 发奖
发奖单 ||--o| 凭证 : 凭证
凭证 ||--o| 核销 : 核销
一句话收束:抽奖不在于会不会写随机数,而在于边界写得不写对。