实战-企业级抽奖系统
概述
业务难点
- 抽奖的业务需求,既复杂又多变
- 奖品类型和概率设置
- 如何公平的抽奖,安全的发奖?
技术挑战
- 网络并发编程,数据读写的并发安全性问题
- 高效的抽奖和发奖,提高并发和性能
- 系统优化,怎么把Redis更好的利用起来
Go的优势
- 高并发,Go协程优于Php多进程、Java多线程模式
- 高性能,编译后的二进制优于Php解释型、Java虚拟机
- 高效网络模型,epoll优于Php的BIO、Java的NIO
抽奖一:年会抽奖
代码
1 | /* |
并发测试
1 | package main |
抽奖二:彩票
两种类型:即开即得型(刮刮乐)+ 双色球自选型
1 | /* |
抽奖三:微信摇一摇
代码
1 | /** |
并发压力测试
手机奖品增加数量为20000个,命中率100%,其余奖品设置为false
压力测试:wrk -t10 -c10 -d5 http://localhost:8080/lucky
看一下日志:wc -l /Users/liuwq/Documents/log/lottery_demo.log
出现了超发的问题!!!线程不安全!
问题解决:加锁。
1 | // 并发锁 |
抽奖四:支付宝集福卡
代码
1 | package main |
并发压力测试
因为集福卡不存在共享变量,也就没有并发安全性问题,giftList 每次都是 new 一个。
压力测试:wrk -t10 -c10 -d5 http://localhost:8080/lucky
抽奖五:微博抢红包
代码
1 | /** |
并发压力测试
压力测试:wrk -t10 -c10 -d5 http://localhost:8080/set?uid=1&money=100&num=100
可以看到控制台报错了~
优化1
是因为针对map这种数据结构,本身就是不安全的。
问题解决:
- 采用互斥锁
- 采用同步方法
sync.Map()
1 | // var packetList map[uint32][]uint = make(map[uint32][]uint) |
1 | //for id, list := range packetList { |
1 | // packetList[id] = list |
1 | //list, ok := packetList[uint32(id)] |
1 | //更新红包列表中的信息 |
改造完成!!
优化2
接下来,针对list切片的线程安全性做出优化:
1 | /** |
优化3
将单任务消息队列改造为多任务消息队列
1 | // var chTasks chan task = make(chan task) |
1 | func newApp() *iris.Application { |
1 | //发送任务 |
1 | func fetchPackagelistMoney(chTasks chan task) { |
优化4
可以考虑将packetList也改造为一个16个的。
抽奖六:抽奖大转盘
代码
1 | package main |
并发压力测试
设置一下奖品的概率
1 | {100, 1000, 0, 9999, 1000}, |
压力测试:wrk -t10 -c10 -d5 http://localhost:8080/prize
对中奖条数进行count,查看控制台count:
发现多了6条数据,说明并发不安全!
加锁
1 | var mu sync.Mutex = sync.Mutex{} |
使用原子整型
1 | Left *int32 // 剩余数 |
系统设计和架构设计
需求整理和提炼
前端页面的需求
- 交互效果,大转盘的展示
- 用户登录,每天抽奖次数限制
- 获奖提示和中奖列表
后端接口的需求
- 奖品列表、抽奖、中奖列表
- 抽奖接口需要满足高性能和高并发要求
- 安全抽奖,奖品不能超发,合理均匀发放
后台管理的需求
- 基本数据管理:奖品、优惠券、用户、IP黑名单、中奖记录
- 实时更新奖品信息,更新奖品库存,奖品中奖周期等
- 后台定时任务,生成发奖计划,填充奖品池
用户操作和业务流程
用户操作步骤
奖品状态变化
抽奖业务流程
数据库设计
数据表
奖品表
优惠券表
抽奖记录表
用户(黑名单)表
IP黑名单表
用户每日次数表
缓存设计
设计和利用缓存
- 目标:提高系统性能,减少数据库依赖
- 原则:平衡好“系统性能、开发时间、复杂度”
- 方向:数据读多写少,数据量有限,数据分散
使用Redis缓存的地方
- 奖品:数量少,更新频率低,最佳的全量缓存对象
- 优惠券:一次性导入,优惠券编码缓存为set类型
- 中奖记录:读写差不多,可以缓存部分统计数据,如:最新中奖记录,最近大奖发放记录等
- 用户黑名单:读多写少,可以按照uid散列
- IP黑名单,类似用户黑名单,可以按照IP散列
- 用户每日参与次数:读和写次数差异没有黑名单那么明显,缓存后的收益不明显
系统架构设计
网络架构图
系统架构图
项目框架和核心代码
数据模型的生成(xorm-cmd)
数据库连接:
1
root:12345678@(127.0.0.1:3306)/lucky_wheel?charset=utf8
```shell
cd /private/var/www/go/src/github.com/go-xorm/cmd/xorm1
2
3
4
3. ```shell
./xorm reverse mysql 数据库账号:密码@(127.0.0.1:3306)/数据库名?charset=utf8 templates/goxorm
# 在models目录就是生成的数据模型文件
核心的dao和service类
dao面向数据库,service面向数据服务
- dao的基础方法:Get、GetAll、CountAll、Search、Delete、Update、Create
- service的基础方法:Get、GetAll、CountAll、Search、Delete、Update、Create
- 特殊方法:例如根据ip查找之类的
用户登陆和退出
基于Cookie的用户状态
- ObjLoginuser 登录用户对象
- 登录用户对象与Cookie的读写
- Cookie的安全校验值,不能被篡改
Redis缓存优化
- 性能,大量数据都可以缓存,大量的读取直接通过Redis
- 原子性操作,redis中的数据递增、递减操作,优于MySQL中的操作
- Redis缓存属于新增加的冗余数据,要注意数据更新保持一致性
奖品的发奖计划数据维护
精确到每分钟的发奖计划
- utils.ResetGiftPrizeData 重新计算奖品的发奖计划数据
- 发奖周期内每天,每分钟的发奖数量的概率是相同的
- 一天中不同时段需要根据预设的概率,发放奖品的数量不同
自动填充奖品池的服务
根据发奖计划填充奖品池
- utils.DistributionGiftPool 既要更新奖品池,又要更新发奖计划数据
- incrGiftPool 并发的增加奖品池库存,二次补充库存
- 自动填充奖品池的服务
奖品池与发奖计划的实现
总结
- 发奖计划,让奖品的数量合理的分配到发奖周期内
- 每天不同时段的时段根据访问量来设置概率更加合理
- 根据发奖计划填充奖品池,保证奖品可以有节奏的发放
如何设置奖品更加合理
更详细的奖品属性
- 数量,关于不限量奖品,因为每次中奖都能发奖,性能会差些
- 概率,预估每天的参与人数,根据总体奖品数量设置概率
- 位置排序,中奖概率大的放在前面,如果中奖编码区间会重叠,则范围小的放在前面
完全的随机还是人为控制发放的节奏
- 同一个IP得到多次大奖,同一个用户得到多次大奖,合理吗?(用户黑名单、IP黑名单)
- 奖品集中在一个较短的时段内全部发放出去,合理吗?(发奖计划、奖品池)
- 所有用户获得大奖的概率一样,合理吗?(用户等级)
更多的运营策略
抽奖的运营策略
- 精准的发放大奖,设置有效时间,限定用户等级,特定用户?
- 虚拟券与电商的优惠券结合,与游戏推广码结合?
- 抽奖需要消耗虚拟币或者其他虚拟积分?
可以引入thrift框架
通过thrift实现rpc的服务端和客户端
- 封装传输和序列化协议,让不同语言的系统像本地代码一样调用
- 定义thrift,生成各语言的代码,实现Go服务端和其他客户端代码
- Go和其他语言都基于thrift框架编程,减少联调和定制的开发时间
问题和思考
架构方面
- 简单描述下抽奖系统的网络架构?
- 客户端以及网络连接,服务端负载均衡,服务端应用服务器,数据库,缓存,存储服务
- 从用户的访问开始,到得到抽奖的返回结构,会经历怎样的一个网络请求?
- 在系统架构分层设计方面,会分为哪几层,分别有哪些内容呢?
- 业务层、框架层(web框架、rpc框架等)、服务层(第三方服务)、资源层(数据库、缓存、存储等)
并发安全性方面
- 防重入问题,一个用户每天只有一次抽奖机会,怎么保证不不超过唯一的一次抽奖呢?
- 奖品库存更新问题,大量的用户并发请求抽奖,中奖后奖品的库存数量扣减,怎么保证库存扣减准确无误,不出现多扣或者少扣呢?
- 优惠券发放问题,不同的优惠券,怎么保证抽奖时,一个优惠券只能发送给一个用户,而不会出现同一个优惠券发给了多个用户?
抽奖系统的难点方面
- 基于数据库的抽奖系统,性能方面会有很大的瓶颈,那么使用Redis缓存来做优化的时候,会怎么来设计Redis缓存的使用呢?什么情况合适,什么情况不合适使用缓存呢?
- 作为一个整体系统,后台管理工作量大,数据库、缓存,多份数据的维护,增加了复杂度、开发难度和工作量
- 读多写少,适合用缓存
- 抽奖的时候,中奖概率是随机匹配,怎么提高抽奖效率?
- 随机以及奖品匹配
- 发奖的时候,为了保证公平性,在各个时段都能均匀的发奖,要怎么来设计和实现呢?
- 引入奖品池,按照预计的节奏发奖
运营策略方面
- 针对用户和请求,会考虑哪些情况和限制规则呢?
- 用户限制,一天内抽奖次数,大奖不要重复给到同一个人
- IP限制,一天内抽奖次数的限制,大奖的限制
- 用户等级限制,新用户机会更多还是老用户机会更多?(老用户)
- 黑名单和白名单的规则,作用和目的是什么呢?
- 为了区分用户价值,要怎么来设计运营策略和规则呢?