✨ 引言:抽奖系统,看似简单,其实是"硬骨头"
很多开发者认为抽奖系统只是"从一堆奖品中随机挑一个",但如果放在真实业务环境中,比如:
- 用户量大(上百万用户同时参与秒抽)
- 要求高并发、高可用、无重复抽奖
- 奖品数量有限,抽完即止
- 要求公平(不同奖品概率不同)
- 奖项配置灵活可扩展(运营频繁调整)
抽奖系统立刻从"简单逻辑"跃升为一个典型的高并发场景下的业务架构挑战。
本文将从0到1,带你设计并实现一个真实可落地、可扩展的企业级抽奖系统,涵盖:
- 系统架构设计
- 核心技术点讲解(幂等、库存扣减、概率算法等)
- 分布式挑战与解决方案
- 实战代码&示意图
- 架构师视角的设计思维
🧩 一、业务背景与核心需求分析
某大型电商平台双11促销,计划上线"抽奖送Yu7"活动
业务需求摘要:
需求编号 | 描述 |
---|---|
R1 | 用户每天可抽奖1次,点击即抽,无等待 |
R2 | 奖品有不同类型(实物、优惠券、积分),有概率权重 |
R3 | 抽奖记录需可追踪,奖品需自动发放 |
R4 | 抽奖活动在高并发下仍需稳定运行,不能重复抽奖 |
R5 | 运营可以后台配置奖品、时间、概率、库存等 |
技术挑战:
- 如何保证高并发下不超发奖品?
- 如何实现概率算法且支持运营动态调整?
- 如何做到幂等抽奖、防作弊?
- 如何实现奖品发放流程异步化提升性能?
🏗️ 二、系统架构设计:抽奖服务怎么拆?
我们按"前后端解耦 + 服务职能清晰 + 可扩展"进行分层架构设计。
系统总览图:
css
[用户前端 H5/APP]
|
[API 网关(鉴权、限流)]
|
----------------------------
| | | |
用户服务 抽奖服务 奖品服务 记录服务
| |
[Redis、消息队列]
服务拆分详解:
职责说明 | |
---|---|
用户服务 | 登录校验、抽奖资格校验 |
抽奖服务 | 抽奖入口、概率计算、幂等控制 |
奖品服务 | 奖品库存扣减、发放逻辑(异步) |
抽奖记录服务 | 抽奖日志记录,奖品归档 |
管理后台 | 抽奖活动配置、奖品维护、数据统计 |
🔧 三、关键模块实现
3.1 抽奖概率算法
每个奖项有不同概率,例如:
奖品ID | 奖品名 | 概率(%) |
---|---|---|
1 | iPhone 15 | 0.1% |
2 | 京东卡50元 | 1% |
3 | 优惠券20元 | 10% |
4 | 谢谢参与 | 88.9% |
🔧 三、关键模块实现
3.1 抽奖概率算法
每个奖项有不同概率,例如:
奖品ID | 奖品名 | 概率(%) |
---|
1 | Yu7 | 0.001% |
---|
2 | 京东卡50元 | 1% |
---|
3 | 优惠券20元 | 10% |
---|
4 | 谢谢参与 | 88.9% |
---|
算法实现(权重随机):
arduino
public class PrizeSelector {
public static Prize draw(List<Prize> prizeList) {
int totalWeight = prizeList.stream().mapToInt(Prize::getWeight).sum();
int random = new Random().nextInt(totalWeight) + 1;
int current = 0;
for (Prize prize : prizeList) {
current += prize.getWeight();
if (random <= current) {
return prize;
}
}
return null;
}
}
3.2 幂等控制 + 防止重复抽奖
使用 Redis 的 SETNX
(set if not exists):
vbnet
String key = "draw:" + userId + ":" + date;
boolean firstDraw = redisTemplate.opsForValue().setIfAbsent(key, "1", 1, TimeUnit.DAYS);
if (!firstDraw) {
throw new BizException("您今天已经抽过奖啦!");
}
3.3 奖品库存扣减(原子性)
避免并发抽中同一个奖项导致"超发",用 Redis Lua 脚本实现扣减:
lua
local stock = tonumber(redis.call('get', KEYS[1]))
if stock <= 0 then
return 0
end
redis.call('decr', KEYS[1])
return 1
Java 中使用:
ini
String script = ... // 上面的 Lua
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(redisScript, Arrays.asList("prize:stock:" + prizeId));
if (result == 0) {
throw new BizException("奖品已抽完");
}
📬 四、奖品发放机制设计:异步更高效
抽奖结果流程图:
css
[用户点击抽奖]
↓
[抽奖服务]
→ 计算中奖
→ Redis扣库存
→ 发送MQ消息(中奖用户、奖品信息)
↓
[发奖服务异步消费]
- 发放积分 / 优惠券
- 插入发放记录
使用 RabbitMQ / Kafka 实现异步发奖:
ini
// 抽奖成功后发送消息
PrizeSendDTO dto = new PrizeSendDTO(userId, prizeId);
rabbitTemplate.convertAndSend("prize.queue", dto);
typescript
// 消费端发奖
@RabbitListener(queues = "prize.queue")
public void handlePrize(PrizeSendDTO dto) {
// 发放逻辑:发券、通知、更新数据库等
}
📊 五、运维与监控
- Prometheus + Grafana:监控抽奖接口 TPS、失败率、奖品库存
- ELK:记录抽奖日志、运营后台行为
- SkyWalking:链路追踪,快速定位抽奖卡顿
🧠 六、总结:为什么这是一个"经典架构练兵场"?
抽奖系统虽然业务简单,但几乎涵盖了真实业务中的大多数挑战:
- 高并发控制(Redis、限流、幂等)
- 分布式事务(库存扣减 + 发奖解耦)
- 动态配置(奖品、概率、库存都支持配置中心)
- 运维监控(可观察性强,关键数据可追溯)
- 代码复用性强(可转化为活动系统、签到、福袋等)
这也是很多面试、实战演练中最常用的业务场景。