2025-10-29 15:34:06

百万并发抽奖系统细节讨论

今天是第二天和 AI 聊抽奖系统。
主要集中下面几个话题:

  1. 具体的概率算法实现方式(权重法 vs 区间法)
  2. 前端转盘的动画实现方案(CSS动画 vs Canvas)
  3. 管理后台免登录实现
  4. 错误码和异常处理规范
  5. 是否需要连接池监控和慢查询日志

编码只应该占据项目时间的20%,也就是有了ai, 你也应该和ai讨论各种细节。最后再让ai写代码,这样写出来的才是符合你预期的代码。

具体的概率算法实现方式(权重法 vs 区间法)

权重法 (Weight-based)

  • 原理:为每个奖项分配一个权重值,权重越高被抽中的概率越大
  • 实现方式:
1 let total_weight: u32 = prizes.iter().map(|p| p.weight).sum(); 2 let mut rng = rand::thread_rng(); 3 let random_num = rng.gen_range(0..total_weight);

优点:实现简单,易于理解
缺点:需要计算总权重,当权重值很大时可能溢出

区间法 (Interval-based)

原理:为每个奖项在0-1区间内分配一个子区间
实现方式

let mut acc = 0.0; for prize in prizes { if random_num < acc + prize.probability { return prize.id; } acc += prize.probability;
  • 优点:精度高,适合小数概率
  • 缺点:需要浮点数运算,可能精度问题

主要区别

  1. 计算复杂度:权重法需要计算总权重,区间法需要累积概率
  • 精度:权重法使用整数,区间法使用浮点数
  • 性能:权重法稍快,区间法更精确

推荐选择:对于抽奖系统,权重法更适合,因为:

  • 奖品数量通常不多
  • 权重值可以合理控制(如0-100)
  • 内存使用:权重法内存占用更小

如何防止超卖呢?

就是奖品其实都中奖了. 结果我还抽中了? 是先查库,更新 可中奖列表, 还是 先中奖,再根据实际可中奖列表更新中奖结果?

悲观锁方案(先锁定再中奖)

BEGIN TRANSACTION; SELECT * FROM prizes WHERE id = ? AND stock > 0 FOR UPDATE; -- 如果查到有库存,计算中奖概率 IF 中奖 THEN UPDATE prizes SET stock = stock - 1 WHERE id = ?; INSERT INTO lottery_records ...; END IF; COMMIT;

优点:强一致性,不会超卖
缺点:性能较差,高并发下锁竞争严重
支持百万并发抽奖,肯定不能选择这个方案。

乐观锁方案(CAS更新)

UPDATE prizes SET stock = stock - 1 WHERE id = ? AND stock > 0 AND stock = original_stock; -- 检查更新行数,如果为0说明库存已被其他请求修改

优点:性能较好,无锁竞争
缺点:需要重试机制

Redis原子操作 + 数据库最终一致性–推荐方案

// 1. Redis原子减库存 let remaining = redis.decrby("prize:1:stock", 1); if remaining >= 0 { // 2. 中奖逻辑 // 3. 异步更新数据库 }

优点:性能最佳
缺点:需要处理Redis和数据库的数据一致性

对于高并发抽奖系统,我推荐Redis原子操作 + 数据库最终一致性:

具体实现流程:

  1. Redis预减库存:DECRBY原子操作确保不超卖
  2. 概率计算:基于Redis中的实时库存计算中奖概率
  3. 中奖记录:记录中奖信息到Redis
  4. 异步同步:后台任务同步Redis数据到PostgreSQL

优势:

  • Redis原子操作确保不超卖
  • 高性能,支持高并发
  • 最终一致性保证数据正确性

✦ 基于4核8G服务器配置,Redis原子操作和MySQL事务的并发性能对比:

Redis原子操作(DECR/INCR)

  • QPS:约50,000-100,000次/秒
  • 优势:内存操作,单线程无锁,原子性保证
  • 延迟:0.1-1ms

MySQL乐观锁(CAS更新)

  • QPS:约8,000-15,000次/秒
  • 优势:无锁竞争,失败重试机制
  • 劣势:失败率随并发增加而升高
  • 延迟:2-10ms(成功时),重试会增加延迟

MySQL事务(悲观锁FOR UPDATE)

  • QPS:约1,000-2,000次/秒
  • 劣势:磁盘I/O,锁竞争,事务开销
  • 延迟:5-20ms

性能差距
Redis比MySQL快25-50倍,在高并发抽奖场景下,Redis方案能支持更高的并发量。

性能排序
Redis > MySQL乐观锁 > MySQL悲观锁

实际测试数据参考

  • Redis单实例:可达10万+ QPS
  • MySQL单实例:通常2千-5千 QPS(受磁盘和锁影响)

即使这样也只有10w QPS,要达到百万QPS就需要层层过滤了。
应用端过滤

// 前端概率过滤 fn lottery_draw(user_id: u64) -> LotteryResult { // 1. 概率前置过滤(80%直接返回谢谢惠顾) if rand::random::<f64>() < 0.8 { return LotteryResult::NotWon; } // 2. 只有20%请求进入核心逻辑 // Redis库存扣减、概率计算等 // ... }

然后前端js再加个50%请求过滤。 这样理论值就达到了100w QPS.

前端转盘的动画实现方案(CSS动画 vs Canvas)

✦ 前端转盘动画方案对比:

CSS动画方案
优势:

  • 性能好,GPU加速
  • 代码简单,维护成本低
  • 响应式适配容易
  • 浏览器兼容性好

劣势:

  • 复杂动画效果受限
  • 精确控制旋转角度较复杂

Canvas方案
优势:

  • 动画效果丰富,可定制性强
  • 精确控制每一帧
  • 适合复杂图形和交互

劣势:

  • 性能开销大
  • 代码复杂度高
  • 响应式适配复杂

选择依据
推荐CSS动画:

  • 抽奖转盘是简单的旋转动画
  • 性能要求高(支持高并发用户)
  • 开发维护简单
  • 移动端兼容性好

管理后台免登录实现

还是用密码吧。就硬编码账号密码就行,用.env管理。

错误码和异常处理规范

核心要点:

  • 错误码结构:分层设计(模块+类型+具体错误)
  • HTTP状态码配合:400客户端错误,500服务端错误
  • 前后端统一:前端能根据错误码做相应处理
  • 日志记录:生产环境错误追踪
  • 安全考虑:不暴露敏感信息

PostgreSQL的表引擎选择和索引设计

核心要点:

  • 表引擎:PostgreSQL默认引擎已足够,无需特殊选择
  • 索引设计:针对高频查询场景设计
    • 用户表:用户名、创建时间
    • 抽奖记录:用户ID、奖品ID、时间(复合索引)
    • 奖品表:活动ID、库存
  • 性能优化:复合索引、覆盖索引、定期维护

是否需要连接池监控和慢查询日志

需要吧.需要测试实际性能的QPS

本文链接:http://blog.go2live.cn/post/day-2.html

-- EOF --