GoLang抽奖系统简易实现流程
上后左爱 人气:0业务难点
设计一个抽奖系统,这个系统并不是具体化,是抽象化,具有以下的几个难点:
1、抽奖业务需要 复杂多变
2、奖品类型和概率设置
3、公平的抽奖和安全的发奖
4、并发安全性问题 一个人不能枪多次
5、高效的抽奖和发奖,提供高并发和性能
6、 如何使用redies进行优化
技术选项
- 高并发 Go 协程优先于 PHP多进程,Java的 多线程模型
- 高性能编译后的二进制优先于PHP解释性和Java虚拟机
- 高效的网络模型 epoll 模型优先于PHPBIO模型和Java NIO模型
抽奖活动
- 年会抽奖,彩票刮奖,微信摇一摇,抽奖大转盘,集福卡等活动,本项目以 抽奖大转盘作为一种活动进行设计。
- 项目实战内容: 框架/核心代码后台功能 ,合理设置奖品和发送奖品, mysql+优化-使用redies 发奖计划与奖品池, 压力测试和更多的运营策略(系统的性能有更好的了解,运营产品的策略有更多), 引入 thirft 框架(RPC 框架), 设计接口生成代码, 服务端接口和客户端程序
需求分析
1. go mod 配置
2. 配置国内代理: go env -w GOPROXY=https://goproxy.cn,https://goproxy.io,direct
3. go get -u -v github.com/kataras/iris 下载包在 GOPATH的PKG目录下
4. iris:功能: 安全认证,缓存 cookies 文件 MVC, 模板 丰富的示例代码
5. https://iris-go.com/v10/recipe
*年会抽奖程序
使用的是iris 这个web 框架 进行处理
/** * curl http://localhost:8080/ * curl --data "users=123,567" http://localhost:8080/import * curl http://localhost:8080/lucky */ package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "math/rand" "strings" "sync" "time" ) var userList []string // 共享变量读写前后 需要增加 锁的设定 简单方式添加互斥锁 var mu sync.Mutex type lotteryController struct { Ctx iris.Context } // 启动一个 iris 应用 func newApp() *iris.Application { app := iris.New() mvc.New(app.Party("/")).Handle(&lotteryController{}) return app } func main() { app := newApp() userList = []string{} mu = sync.Mutex{} err := app.Listen(":8080") if err != nil { panic(fmt.Sprintf("web server start error: %s\n", err)) return } } func (c *lotteryController) Get() string { count := len(userList) return fmt.Sprintf("当前总共参与抽奖的用户数:%d\n", count) } // PostImport POST http://localhost:8090/import // params : users func (c *lotteryController) PostImport() string { strUsers := c.Ctx.FormValue("users") users := strings.Split(strUsers, ",") // 批量线程导入时候 发现有多线程的问题 数据统计不正确 mu.Lock() defer mu.Unlock() count1 := len(userList) for _, u := range users { u = strings.TrimSpace(u) if len(u) > 0 { userList = append(userList, u) } } count2 := len(userList) return fmt.Sprintf("当前总共参与抽奖的用户数:%d, 成功导入的用户数:%d\n", count2, count2-count1) } // GetLucky GET http://localhost:8090/lucky func (c *lotteryController) GetLucky() string { // 抽奖地方进行锁的判断 mu.Lock() defer mu.Unlock() count := len(userList) if count > 1 { index := rand.New(rand.NewSource(time.Now().UnixNano())).Int31n(int32(count)) user := userList[index] // 需要 删除被挑选过的人 直接可以删除 当前index 下的数据 更好 userList = append(userList[0:index], userList[index+1:]...) return fmt.Sprintf("当前中奖用户:%s, 剩余用户数:%d\n", user, count-1) } else if count == 1 { user := userList[0] return fmt.Sprintf("当前中奖用户:%s, 剩余用户数:%d\n", user, count-1) } else { return fmt.Sprintf("当前中奖完毕,没有用户参与中奖\n") } }
单元测试问题,对于 userList 的 多线程下发生数据竞争问题 :
package main import ( "fmt" "github.com/kataras/iris/v12/httptest" "sync" "testing" ) func TestMVC(t *testing.T) { app := newApp() e := httptest.New(t, app) // 使用同步等待锁 var wg sync.WaitGroup e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal("当前总共参与抽奖的用户数:0\n") for i := 0; i < 100; i++ { wg.Add(1) // 不会出现协程并发性问题 go func(i int) { defer wg.Done() e.POST("/import").WithFormField("users", fmt.Sprintf("test_u%d", i)).Expect().Status(httptest.StatusOK) }(i) } wg.Wait() e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal("当前总共参与抽奖的用户数:100\n") e.GET("/lucky").Expect().Status(httptest.StatusOK) e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal("当前总共参与抽奖的用户数:99\n") }
微信摇一摇得抽奖活动
/** * 微信摇一摇得功能 * wrk -t10 -c10 -d5 http://localhost:8080/lucky 进行压力测试 查看代码是否有竞争异常问题和 接口请求量速度 */ package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "log" "math/rand" "os" "sync" "time" ) var mu sync.Mutex const ( giftTypeCoin = iota // 虚拟币 giftTypeCoupon // 不同卷 giftTypeCouponFix // 不同卷 giftTypeRealSmall // 实物小奖 giftTypeRealLarge // 十五大奖 ) type gift struct { id int name string pic string link string gType int data string // 奖品数据(特定得配置信息) dataList []string total int left int inuse bool rate int // 万分之N rateMin int rateMax int } // 最大中奖号码 const rateMax = 1000 var logger *log.Logger // 奖品类表 var giftList []*gift type lotteryController struct { Ctx iris.Context } // 启动一个 iris 应用 func newApp() *iris.Application { app := iris.New() mvc.New(app.Party("/")).Handle(&lotteryController{}) return app } func initLog() { f, _ := os.Create("G:\\goLandProject\\lottery_demo.log") logger = log.New(f, "", log.Ldate|log.Lmicroseconds) } func initGift() { giftList = make([]*gift, 5) g1 := gift{ id: 1, name: "手机大奖", pic: "", link: "", gType: giftTypeRealLarge, data: "", dataList: nil, total: 20000, left: 20000, inuse: true, rate: 10000, rateMin: 0, rateMax: 0, } g2 := gift{ id: 2, name: "充电器", pic: "", link: "", gType: giftTypeRealSmall, data: "", dataList: nil, total: 5, left: 5, inuse: false, rate: 10, rateMin: 0, rateMax: 0, } g3 := gift{ id: 3, name: "优惠卷满200减50", pic: "", link: "", gType: giftTypeCouponFix, data: "mall-coupon-2018", dataList: nil, total: 50, left: 50, inuse: false, rate: 500, rateMin: 0, rateMax: 0, } g4 := gift{ id: 4, name: "直降优惠卷", pic: "", link: "", gType: giftTypeCoupon, data: "", dataList: []string{"c01", "c02", "c03", "c04", "c05"}, total: 50, left: 50, inuse: false, rate: 100, rateMin: 0, rateMax: 0, } g5 := gift{ id: 5, name: "金币", pic: "", link: "", gType: giftTypeCoin, data: "10金币", dataList: nil, total: 100, left: 100, inuse: false, rate: 5000, rateMin: 0, rateMax: 0, } giftList[0] = &g1 giftList[1] = &g2 giftList[2] = &g3 giftList[3] = &g4 giftList[4] = &g5 // s数据整理 中奖区间数据 rateStart := 0 for _, data := range giftList { if !data.inuse { continue } data.rateMin = rateStart data.rateMax = rateStart + data.rate if data.rateMax >= rateMax { data.rateMax = rateMax rateStart = 0 } else { rateStart += data.rate } } } func main() { initLog() initGift() mu = sync.Mutex{} app := newApp() err := app.Listen(":8080") if err != nil { panic(fmt.Sprintf("web server start error : %s\n", err)) } } // Get http://localhost:8080 func (c *lotteryController) Get() string { count := 0 total := 0 for _, data := range giftList { if data.inuse && (data.total == 0 || (data.total > 0 && data.left > 0)) { count++ total += data.left } } return fmt.Sprintf("当前有效奖品种类数量:%d, 限量奖品总数量:%d\n", count, total) } // GetLucky http://localhost:8080/lucky func (c *lotteryController) GetLucky() map[string]interface{} { mu.Lock() defer mu.Unlock() code := luckyCode() ok := false result := make(map[string]interface{}) result["success"] = ok // 对 code 与 rateMin -rateMax 区间内进行对比 判断是否获奖 for _, data := range giftList { if !data.inuse || (data.total > 0 && data.left <= 0) { continue } if data.rateMin <= int(code) && data.rateMax > int(code) { sendData := "" switch data.gType { case giftTypeCoin: ok, sendData = sendCoin(data) case giftTypeCoupon: ok, sendData = sendCoupon(data) case giftTypeCouponFix: ok, sendData = sendCouponFix(data) case giftTypeRealSmall: ok, sendData = sendRealSmall(data) case giftTypeRealLarge: ok, sendData = sendRealLarge(data) } if ok { // 中奖后得到奖品 生成中奖记录 saveLuckyData(code, data, sendData) result["success"] = true result["id"] = data.id result["name"] = data.name result["data"] = sendData break } } } return result } func luckyCode() int32 { seed := time.Now().UnixNano() code := rand.New(rand.NewSource(seed)).Int31n(int32(rateMax)) return code } func sendCoin(data *gift) (bool, string) { if data.total == 0 { // 数量无数 return true, data.data } else if data.left > 0 { data.left -= 1 return true, data.data } else { return false, "奖品已经发完" } } // 不同优惠卷 func sendCoupon(data *gift) (bool, string) { if data.left > 0 { left := data.left - 1 data.left = left return true, data.dataList[left%5] } else { return false, "奖品已经发完" } } func sendCouponFix(data *gift) (bool, string) { if data.total == 0 { // 数量无数 return true, data.data } else if data.left > 0 { data.left -= 1 return true, data.data } else { return false, "奖品已经发完" } } func sendRealSmall(data *gift) (bool, string) { if data.total == 0 { // 数量无数 return true, data.data } else if data.left > 0 { data.left -= 1 return true, data.data } else { return false, "奖品已经发完" } } func sendRealLarge(data *gift) (bool, string) { if data.total == 0 { // 数量无数 return true, data.data } else if data.left > 0 { data.left -= 1 return true, data.data } else { return false, "奖品已经发完" } } func saveLuckyData(code int32, g *gift, data string) { logger.Printf("lucky, code =%d ,id =%d, name =%d, data =%s, left=%d \n", code, g.id, g.name, data, g.left) }
微博抢红包
- 红包得集合,红包内红包数量读写都存在并发安全性问题
- 第一种方式 使用 Sync.Map 互斥锁得方式
/** * 微信抢红包 普通得 map 发生 竞争情况 所以需要使用 互斥 sync.Map * 在大量得写 和 读得情况下会发生 竞争 * */ package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "math/rand" "sync" "time" ) // 红包列表 var packageList *sync.Map = new(sync.Map) type lotteryController struct { Ctx iris.Context } // 启动一个 iris 应用 func newApp() *iris.Application { app := iris.New() mvc.New(app.Party("/")).Handle(&lotteryController{}) return app } func main() { app := newApp() err := app.Listen(":8080") if err != nil { panic(fmt.Sprintf("web server start error : %s\n", err)) } } // Get http://localhost:8080 func (c *lotteryController) Get() map[uint32][2]int { // 返回当前全部得红包 rs := make(map[uint32][2]int) packageList.Range(func(key, value interface{}) bool { id := key.(uint32) list := value.([]uint) var money int for _, v := range list { money += int(v) } rs[id] = [2]int{len(list), money} return true }) return rs } // GetSet http://localhost:8080/set?uid=1&money=100&num=100 func (c *lotteryController) GetSet() string { uid, errUid := c.Ctx.URLParamInt("uid") moeny, errMoney := c.Ctx.URLParamFloat64("money") num, errNum := c.Ctx.URLParamInt("num") if errUid != nil || errNum != nil || errMoney != nil { fmt.Sprintf("errUid=%d, errMoney=%d, errNum=%d \n", errUid, errMoney, errNum) } moenyTotal := int(moeny * 100) if uid < 1 || moenyTotal < num || num < 1 { return fmt.Sprintf("参数数值异常, uid=%d, money=%d, num=%d \n", uid, moeny, num) } // 金额分配算法 r := rand.New(rand.NewSource(time.Now().UnixNano())) rMax := 0.55 // 随机分配最大值 if num > 1000 { rMax = 0.01 } else if num < 10 { rMax = 0.80 } list := make([]uint, num) leftMoney := moenyTotal leftNum := num for leftNum > 0 { if leftNum == 1 { list[num-1] = uint(leftMoney) break } // 剩余钱数等于剩余红包数每个红包进行均分 if leftMoney == leftNum { for i := num - leftNum; i < num; i++ { list[i] = 1 break } } // 随机分配最大值 rMoney := int(float64(leftMoney-leftNum) * rMax) m := r.Intn(rMoney) if m < 1 { m = 1 } list[num-leftNum] = uint(m) leftMoney -= m leftNum-- } // 红包得UUID id := r.Uint32() packageList.Store(id, list) return fmt.Sprintf("/get?id=%d&uid=%d&num=%d", id, uid, num) } // GetGet http://localhost:8080/get?id=1&uid=1 func (c *lotteryController) GetGet() string { id, errid := c.Ctx.URLParamInt("id") uid, errUid := c.Ctx.URLParamInt("uid") if errUid != nil || errid != nil { return fmt.Sprintf("") } if uid < 1 || id < 1 { return fmt.Sprintf("") } listq, ok := packageList.Load(uint32(id)) list := listq.([]int) if !ok || len(list) < 1 { return fmt.Sprintf("红包不存在, id =%d \n", id) } // 分配随机数获取红包 r := rand.New(rand.NewSource(time.Now().UnixNano())) i := r.Intn(len(list)) money := list[i] // 更新红包中列表信息 if len(list) > 1 { if i == len(list)-1 { packageList.Store(uint32(id), list[:i]) } else if i == 0 { packageList.Store(uint32(id), list[1:]) } else { packageList.Store(uint32(id), append(list[:i], list[i+1:]...)) } } else { packageList.Delete(uint32(id)) } return fmt.Sprintf("恭喜你抢到一个红包, 红包金额:%d \n", money) }
第二种方式: chan 队列方式 解决线程安全
/** * 微信抢红包 普通得 map 发生 竞争情况 所以需要使用 互斥 sync.Map * 在大量得写 和 读得情况下会发生 竞争 * * 单核任务 修改成 16 核心 进行抢红包 */ package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "math/rand" "sync" "time" ) // 红包列表 var packageList *sync.Map = new(sync.Map) type task struct { id uint32 callback chan uint } const taskNum = 16 // 初始化队列数量 var chTaskList []chan task = make([]chan task, taskNum) type lotteryController struct { Ctx iris.Context } // 启动一个 iris 应用 func newApp() *iris.Application { app := iris.New() mvc.New(app.Party("/")).Handle(&lotteryController{}) return app } func main() { app := newApp() err := app.Listen(":8080") // 启动多个子线程进行 红包抢 for i := 0; i < taskNum; i++ { chTaskList[i] = make(chan task) go fetchPackageListMoney(chTaskList[i]) } if err != nil { panic(fmt.Sprintf("web server start error : %s\n", err)) } } // Get http://localhost:8080 func (c *lotteryController) Get() map[uint32][2]int { // 返回当前全部得红包 rs := make(map[uint32][2]int) packageList.Range(func(key, value interface{}) bool { id := key.(uint32) list := value.([]uint) var money int for _, v := range list { money += int(v) } rs[id] = [2]int{len(list), money} return true }) return rs } // GetSet http://localhost:8080/set?uid=1&money=100&num=100 func (c *lotteryController) GetSet() string { uid, errUid := c.Ctx.URLParamInt("uid") moeny, errMoney := c.Ctx.URLParamFloat64("money") num, errNum := c.Ctx.URLParamInt("num") if errUid != nil || errNum != nil || errMoney != nil { fmt.Sprintf("errUid=%d, errMoney=%d, errNum=%d \n", errUid, errMoney, errNum) } moenyTotal := int(moeny * 100) if uid < 1 || moenyTotal < num || num < 1 { return fmt.Sprintf("参数数值异常, uid=%d, money=%d, num=%d \n", uid, moeny, num) } // 金额分配算法 r := rand.New(rand.NewSource(time.Now().UnixNano())) rMax := 0.55 // 随机分配最大值 if num > 1000 { rMax = 0.01 } else if num < 10 { rMax = 0.80 } list := make([]uint, num) leftMoney := moenyTotal leftNum := num for leftNum > 0 { if leftNum == 1 { list[num-1] = uint(leftMoney) break } // 剩余钱数等于剩余红包数每个红包进行均分 if leftMoney == leftNum { for i := num - leftNum; i < num; i++ { list[i] = 1 break } } // 随机分配最大值 rMoney := int(float64(leftMoney-leftNum) * rMax) m := r.Intn(rMoney) if m < 1 { m = 1 } list[num-leftNum] = uint(m) leftMoney -= m leftNum-- } // 红包得UUID id := r.Uint32() packageList.Store(id, list) return fmt.Sprintf("/get?id=%d&uid=%d&num=%d", id, uid, num) } // GetGet http://localhost:8080/get?id=1&uid=1 func (c *lotteryController) GetGet() string { id, errid := c.Ctx.URLParamInt("id") uid, errUid := c.Ctx.URLParamInt("uid") if errUid != nil || errid != nil { return fmt.Sprintf("") } if uid < 1 || id < 1 { return fmt.Sprintf("") } listq, ok := packageList.Load(uint32(id)) list := listq.([]int) if !ok || len(list) < 1 { return fmt.Sprintf("红包不存在, id =%d \n", id) } // 构造一个任务 callback := make(chan uint) t := task{id: uint32(id), callback: callback} // 发送任务 chTasks := chTaskList[id%taskNum] chTasks <- t // 接受返回结果值 money := <-callback if money <= 0 { return "很遗憾,没有抢到红包\n" } else { return fmt.Sprintf("恭喜你抢到一个红包, 红包金额:%d \n", money) } } // 使用队列方式, 需要不断从chan 通道中获取数据 func fetchPackageListMoney(chTasks chan task) { for { t := <-chTasks id := t.id l, ok := packageList.Load(id) if ok && l != nil { // 分配随机数获取红包 list := l.([]int) r := rand.New(rand.NewSource(time.Now().UnixNano())) i := r.Intn(len(list)) money := list[i] // 更新红包中列表信息 if len(list) > 1 { if i == len(list)-1 { packageList.Store(uint32(id), list[:i]) } else if i == 0 { packageList.Store(uint32(id), list[1:]) } else { packageList.Store(uint32(id), append(list[:i], list[i+1:]...)) } } else { packageList.Delete(uint32(id)) } t.callback <- uint(money) } else { t.callback <- 0 } } }
抽奖大转盘
后端设置各个奖品得中奖概率和数量限制,更新库存时候发现并发安全性质问题 和微信摇一摇 类似
使用CAS进行安全代码进行修改,不在使用同步锁,CAS乐观锁比sync.mutSync 会快一些
/** * 大转盘程序 * curl http://localhost:8080/ * curl http://localhost:8080/debug * curl http://localhost:8080/prize * 固定几个奖品,不同的中奖概率或者总数量限制 * 每一次转动抽奖,后端计算出这次抽奖的中奖情况,并返回对应的奖品信息 * * 增加互斥锁,保证并发库存更新的正常 * 压力测试: * wrk -t10 -c100 -d5 "http://localhost:8080/prize" */ package main import ( "fmt" "github.com/kataras/iris/v12" "github.com/kataras/iris/v12/mvc" "log" "math/rand" "strings" "sync/atomic" "time" ) // Prate 奖品中奖概率 type Prate struct { Rate int // 万分之N的中奖概率 Total int // 总数量限制,0 表示无限数量 CodeA int // 中奖概率起始编码(包含) CodeB int // 中奖概率终止编码(包含) Left *int32 // 剩余数使用CAS乐观锁 进行修改 } var left = int32(1000) // 奖品列表 var prizeList []string = []string{ "一等奖,火星单程船票", "二等奖,凉飕飕南极之旅", "三等奖,iPhone一部", "", // 没有中奖 } // 奖品的中奖概率设置,与上面的 prizeList 对应的设置 var rateList []Prate = []Prate{ //Prate{1, 1, 0, 0, 1}, //Prate{2, 2, 1, 2, 2}, Prate{5, 1000, 0, 9999, &left}, //Prate{100,0, 0, 9999, 0}, } type lotteryController struct { Ctx iris.Context } // 启动一个 iris 应用 func newApp() *iris.Application { app := iris.New() mvc.New(app.Party("/")).Handle(&lotteryController{}) return app } func main() { app := newApp() err := app.Listen(":8080") if err != nil { panic(fmt.Sprintf("web server start error : %s\n", err)) } } // Get GET http://localhost:8080/ func (c *lotteryController) Get() string { c.Ctx.Header("Content-Type", "text/html") return fmt.Sprintf("大转盘奖品列表:<br/> %s", strings.Join(prizeList, "<br/>\n")) } // GetPrize GET http://localhost:8080/prize func (c *lotteryController) GetPrize() string { c.Ctx.Header("Content-Type", "text/html") // 第一步,抽奖,根据随机数匹配奖品 seed := time.Now().UnixNano() r := rand.New(rand.NewSource(seed)) // 得到个人的抽奖编码 code := r.Intn(10000) //fmt.Println("GetPrize code=", code) var myPrize string var prizeRate *Prate // 从奖品列表中匹配,是否中奖 for i, prize := range prizeList { rate := &rateList[i] if code >= rate.CodeA && code <= rate.CodeB { // 满足中奖条件 myPrize = prize prizeRate = rate break } } if myPrize == "" { // 没有中奖 myPrize = "很遗憾,再来一次" return myPrize } // 第二步,发奖,是否可以发奖 if prizeRate.Total == 0 { // 无限奖品 fmt.Println("中奖: ", myPrize) return myPrize } else if *prizeRate.Left > 0 { // 还有剩余奖品 left := atomic.AddInt32(prizeRate.Left, -1) if left >= 0 { log.Printf("奖品:%s", myPrize) return myPrize } } // 有限且没有剩余奖品,无法发奖 myPrize = "很遗憾,再来一次" return myPrize } // GetDebug GET http://localhost:8080/debug func (c *lotteryController) GetDebug() string { c.Ctx.Header("Content-Type", "text/html") return fmt.Sprintf("获奖概率: %v", rateList) }
抽奖活动总结
- 并发安全性质问题,互斥锁,队列, CAS递减方式
- 优化,通过散列减小单个集合得大小
加载全部内容