业务难点
设计一个抽奖系统,这个系统并不是具体化,是抽象化,具有以下的几个难点:
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("大转盘奖品列表:
%s", strings.Join(prizeList, "
\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递减方式
- 优化,通过散列减小单个集合得大小
到此这篇关于GoLang抽奖系统简易实现流程的文章就介绍到这了,更多相关Go抽奖系统内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!