今天是第二天和 AI 聊抽奖系统。
主要集中下面几个话题:
- 具体的概率算法实现方式(权重法 vs 区间法)
- 前端转盘的动画实现方案(CSS动画 vs Canvas)
- 管理后台免登录实现
- 错误码和异常处理规范
- 是否需要连接池监控和慢查询日志
编码只应该占据项目时间的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;
- 优点:精度高,适合小数概率
- 缺点:需要浮点数运算,可能精度问题
主要区别
- 计算复杂度:权重法需要计算总权重,区间法需要累积概率
- 精度:权重法使用整数,区间法使用浮点数
- 性能:权重法稍快,区间法更精确
推荐选择:对于抽奖系统,权重法更适合,因为:
- 奖品数量通常不多
- 权重值可以合理控制(如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原子操作 + 数据库最终一致性:
具体实现流程:
- Redis预减库存:DECRBY原子操作确保不超卖
- 概率计算:基于Redis中的实时库存计算中奖概率
- 中奖记录:记录中奖信息到Redis
- 异步同步:后台任务同步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
