目标:设计一个支撑亿级访问、百万并发的抽奖系统
核心要求:
- 高并发(100w QPS)
- 强一致库存
- 防刷防作弊
- 高可用(99.99%)
- 毫秒级响应
- 成本可控
本文是一篇偏实战的架构落地文章,包含:
- 完整架构设计
- Redis + Kafka + MySQL 组合
- Go 实战代码
- 抽奖算法实现
- 高并发库存扣减
- 防刷策略
- 真实事故复盘
一、抽奖系统核心挑战
抽奖系统本质是 概率 + 库存 + 高并发 的系统。
典型业务场景:
618活动
双11抽奖
APP签到抽奖
直播间抽奖
积分抽奖
核心问题只有四个:
| 问题 |
说明 |
| 高并发 |
瞬时百万请求 |
| 概率公平 |
不被操控 |
| 库存准确 |
不超发 |
| 防刷 |
防脚本 |
二、抽奖系统整体架构
架构全景
架构说明:
CDN 静态资源
SLB 负载均衡
Gateway API网关
LotterySvc 抽奖服务
Redis 库存 + 概率池
Kafka 削峰
Worker 异步发奖
MySQL 持久化
三、抽奖流程设计
核心流程:
- 用户发起抽奖请求。
- 网关进行鉴权、防刷、限流。
- 抽奖服务执行概率计算与库存扣减。
- 中奖结果写入消息队列进行异步处理。
- 下游 Worker 服务消费消息,完成发奖、记录等操作。
四、奖品概率设计
抽奖的核心是 概率池。
示例:
| 奖品 |
概率 |
| iPhone |
0.1% |
| AirPods |
1% |
| 积分 |
10% |
| 谢谢参与 |
88.9% |
实现方式:
概率区间算法
例如:
0 - 1 iPhone
2 - 10 AirPods
11 - 110 积分
111 - 1000 谢谢参与
五、Go实现抽奖算法
type Prize struct {
Id int
Name string
Start int
End int
}
var prizes = []Prize{
{1, "iPhone", 0, 1},
{2, "AirPods", 2, 10},
{3, "Points", 11, 110},
{4, "Thanks", 111, 1000},
}
func Draw() Prize {
r := rand.Intn(1000)
for _, p := range prizes {
if r >= p.Start && r <= p.End {
return p
}
}
return prizes[len(prizes)-1]
}
复杂度:
O(n)
高并发优化:
概率池数组
六、O(1)抽奖算法
构建概率池:
[谢谢参与,谢谢参与,谢谢参与...]
长度10000
Go实现:
var pool []int
func initPool() {
for i := 0; i < 1; i++ {
pool = append(pool, 1)
}
for i := 0; i < 10; i++ {
pool = append(pool, 2)
}
for i := 0; i < 100; i++ {
pool = append(pool, 3)
}
for i := 0; i < 9889; i++ {
pool = append(pool, 4)
}
}
抽奖:
func DrawFast() int {
index := rand.Intn(len(pool))
return pool[index]
}
时间复杂度:
O(1)
七、高并发库存扣减
库存必须 Redis原子扣减。
错误做法:
查询库存
再扣库存
正确做法:
使用 Lua 脚本。
Redis Lua扣库存
local key = KEYS[1]
local stock = tonumber(redis.call('get', key))
if stock <= 0 then
return -1
end
redis.call('decr', key)
return stock
Go调用:
script := redis.NewScript(`
local key = KEYS[1]
local stock = tonumber(redis.call('get', key))
if stock <= 0 then
return -1
end
redis.call('decr', key)
return stock
`)
res, err := script.Run(ctx, rdb, []string{"prize:1:stock"}).Result()
八、防刷设计
抽奖系统最怕:
脚本刷奖
常见手段:
| 手段 |
说明 |
| IP限流 |
Redis |
| 用户限流 |
用户ID |
| 设备指纹 |
设备ID |
| 行为风控 |
滑块 |
Redis限流
key := fmt.Sprintf("lottery:limit:%d", userID)
count, _ := rdb.Incr(ctx, key).Result()
if count == 1 {
rdb.Expire(ctx, key, time.Second)
}
if count > 5 {
return "too fast"
}
九、Kafka削峰
大促期间:
100w QPS
必须削峰。为了应对这种瞬间海量请求,引入消息队列进行异步化处理是架构设计的常规操作,你可以参考 云栈社区 上关于分布式消息中间件的讨论。
架构:
请求 -> Kafka -> Worker
生产消息:
msg := &sarama.ProducerMessage{
Topic: "lottery",
Value: sarama.StringEncoder(userId),
}
producer.SendMessage(msg)
消费:
for msg := range consumer.Messages() {
userId := string(msg.Value)
process(userId)
}
十、中奖记录设计
MySQL表:
CREATE TABLE lottery_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT,
prize_id INT,
create_time DATETIME
);
索引:
user_id
create_time
十一、防止超发
关键策略:
库存预热
Redis:
prize:1:stock = 100
扣完即止。
十二、真实事故复盘
某电商双11抽奖事故:
事故
奖品库存100
实际发出1000
原因:
并发查询库存
代码:
if(stock > 0){
stock--
}
100并发:
全部判断成功
解决:
Redis Lua
十三、热点Key问题
热门奖品:
10w QPS
Redis压力大。
解决:
库存分片
示例:
prize:1:stock:1
prize:1:stock:2
prize:1:stock:3
随机扣。
十四、数据一致性
最终一致:
Redis库存
MySQL记录
解决:
Kafka异步补偿
十五、监控体系
必须监控:
| 指标 |
说明 |
| QPS |
请求量 |
| 抽奖成功率 |
|
| Redis延迟 |
|
| Kafka堆积 |
|
| 发奖失败 |
|
十六、压测结果
压测环境:
8核16G
Redis集群
Kafka 3节点
结果:
| QPS |
延迟 |
| 10万 |
6ms |
| 50万 |
12ms |
| 100万 |
18ms |
十七、最终架构
(略,参见第二、九、二十节)
十八、系统容量
理论容量:
100万 QPS
Redis:
100k QPS/节点
10节点即可。
十九、企业级架构升级
进一步升级:
多机房
多Region
全链路压测
架构:
多活
二十、抽奖系统终极架构
最终系统:
CDN
SLB
Gateway
Lottery Service
Redis Cluster
Kafka Cluster
Worker Cluster
MySQL Cluster
可支撑:
亿级用户
百万并发
结语
抽奖系统看似简单:
随机数
但真正难的是:
高并发
库存一致
公平概率
防刷
真正的架构核心是:
Redis + Kafka + MySQL
这也是 互联网大厂抽奖系统的标准架构。希望这篇从架构设计到代码实现的实战指南,能帮助你构建自己的高可用抽奖系统。