目录
- 业务难点
- 技术选项
- 抽奖活动
- 需求分析
- 微信摇一摇得抽奖活动
- 微博抢红包
- 抽奖大转盘
- 抽奖活动总结
业务难点
设计一个抽奖系统,这个系统并不是具体化,是抽象化,具有以下的几个难点:
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:/
* curl --data "users=,567" http://localhost:8080/import
* curl http://localhost:/lucky
*/
package main
import (
"fmt"
"github.com/kataras/iris/v"
"github.com/kataras/iris/v/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(":")
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:/import
// params : users
func (c *lotteryController) PostImport() string {
strUsers := c.Ctx.FormValue("users")
users := strings.Split(strUsers, ",")
// 批量线程导入时候 发现有多线程的问题 数据统计不正确
mu.Lock()
defer mu.Unlock()
count := len(userList)
for _, u := range users {
u = strings.TrimSpace(u)
if len(u) > {
userList = append(userList, u)
}
}
count := len(userList)
return fmt.Sprintf("当前总共参与抽奖的用户数:%d, 成功导入的用户数:%d\n", count, count2-count1)
}
// GetLucky GET http://localhost:/lucky
func (c *lotteryController) GetLucky() string {
// 抽奖地方进行锁的判断
mu.Lock()
defer mu.Unlock()
count := len(userList)
if count > {
index := rand.New(rand.NewSource(time.Now().UnixNano())).Intn(int32(count))
user := userList[index]
// 需要 删除被挑选过的人 直接可以删除 当前index 下的数据 更好
userList = append(userList[:index], userList[index+1:]...)
return fmt.Sprintf("当前中奖用户:%s, 剩余用户数:%d\n", user, count-)
} else if count == {
user := userList[]
return fmt.Sprintf("当前中奖用户:%s, 剩余用户数:%d\n", user, count-)
} else {
return fmt.Sprintf("当前中奖完毕,没有用户参与中奖\n")
}
}
单元测试问题,对于 userList 的 多线程下发生数据竞争问题 :
package main
import (
"fmt"
"github.com/kataras/iris/v/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("当前总共参与抽奖的用户数:\n")
for i :=; i < 100; i++ {
wg.Add()
// 不会出现协程并发性问题
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("当前总共参与抽奖的用户数:\n")
e.GET("/lucky").Expect().Status(httptest.StatusOK)
e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal("当前总共参与抽奖的用户数:\n")
}
微信摇一摇得抽奖活动
/**
* 微信摇一摇得功能
* wrk -t -c10 -d5 http://localhost:8080/lucky 进行压力测试 查看代码是否有竞争异常问题和 接口请求量速度
*/
package main
import (
"fmt"
"github.com/kataras/iris/v"
"github.com/kataras/iris/v/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 =
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,)
g := gift{
id:,
name: "手机大奖",
pic: "",
link: "",
gType: giftTypeRealLarge,
data: "",
dataList: nil,
total:,
left:,
inuse: true,
rate:,
rateMin:,
rateMax:,
}
g := gift{
id:,
name: "充电器",
pic: "",
link: "",
gType: giftTypeRealSmall,
data: "",
dataList: nil,
total:,
left:,
inuse: false,
rate:,
rateMin:,
rateMax:,
}
g := gift{
id:,
name: "优惠卷满减50",
pic: "",
link: "",
gType: giftTypeCouponFix,
data: "mall-coupon-",
dataList: nil,
total:,
left:,
inuse: false,
rate:,
rateMin:,
rateMax:,
}
g := gift{
id:,
name: "直降优惠卷",
pic: "",
link: "",
gType: giftTypeCoupon,
data: "",
dataList: []string{"c", "c02", "c03", "c04", "c05"},
total:,
left:,
inuse: false,
rate:,
rateMin:,
rateMax:,
}
g := gift{
id:,
name: "金币",
pic: "",
link: "",
gType: giftTypeCoin,
data: "金币",
dataList: nil,
total:,
left:,
inuse: false,
rate:,
rateMin:,
rateMax:,
}
giftList[] = &g1
giftList[] = &g2
giftList[] = &g3
giftList[] = &g4
giftList[] = &g5
// s数据整理 中奖区间数据
rateStart :=
for _, data := range giftList {
if !data.inuse {
continue
}
data.rateMin = rateStart
data.rateMax = rateStart + data.rate
if data.rateMax >= rateMax {
data.rateMax = rateMax
rateStart =
} else {
rateStart += data.rate
}
}
}
func main() {
initLog()
initGift()
mu = sync.Mutex{}
app := newApp()
err := app.Listen(":")
if err != nil {
panic(fmt.Sprintf("web server start error : %s\n", err))
}
}
// Get http://localhost:
func (c *lotteryController) Get() string {
count :=
total :=
for _, data := range giftList {
if data.inuse && (data.total == || (data.total > 0 && data.left > 0)) {
count++
total += data.left
}
}
return fmt.Sprintf("当前有效奖品种类数量:%d, 限量奖品总数量:%d\n", count, total)
}
// GetLucky http://localhost:/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 > && 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() int {
seed := time.Now().UnixNano()
code := rand.New(rand.NewSource(seed)).Intn(int32(rateMax))
return code
}
func sendCoin(data *gift) (bool, string) {
if data.total == {
// 数量无数
return true, data.data
} else if data.left > {
data.left -=
return true, data.data
} else {
return false, "奖品已经发完"
}
}
// 不同优惠卷
func sendCoupon(data *gift) (bool, string) {
if data.left > {
left := data.left -
data.left = left
return true, data.dataList[left%]
} else {
return false, "奖品已经发完"
}
}
func sendCouponFix(data *gift) (bool, string) {
if data.total == {
// 数量无数
return true, data.data
} else if data.left > {
data.left -=
return true, data.data
} else {
return false, "奖品已经发完"
}
}
func sendRealSmall(data *gift) (bool, string) {
if data.total == {
// 数量无数
return true, data.data
} else if data.left > {
data.left -=
return true, data.data
} else {
return false, "奖品已经发完"
}
}
func sendRealLarge(data *gift) (bool, string) {
if data.total == {
// 数量无数
return true, data.data
} else if data.left > {
data.left -=
return true, data.data
} else {
return false, "奖品已经发完"
}
}
func saveLuckyData(code int, 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/v"
"github.com/kataras/iris/v/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(":")
if err != nil {
panic(fmt.Sprintf("web server start error : %s\n", err))
}
}
// Get http://localhost:
func (c *lotteryController) Get() map[uint][2]int {
// 返回当前全部得红包
rs := make(map[uint][2]int)
packageList.Range(func(key, value interface{}) bool {
id := key.(uint)
list := value.([]uint)
var money int
for _, v := range list {
money += int(v)
}
rs[id] = []int{len(list), money}
return true
})
return rs
}
// GetSet http://localhost:/set?uid=1&money=100&num=100
func (c *lotteryController) GetSet() string {
uid, errUid := c.Ctx.URLParamInt("uid")
moeny, errMoney := c.Ctx.URLParamFloat("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 *)
if uid < || 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 :=.55 // 随机分配最大值
if num > {
rMax =.01
} else if num < {
rMax =.80
}
list := make([]uint, num)
leftMoney := moenyTotal
leftNum := num
for leftNum > {
if leftNum == {
list[num-] = uint(leftMoney)
break
}
// 剩余钱数等于剩余红包数每个红包进行均分
if leftMoney == leftNum {
for i := num - leftNum; i < num; i++ {
list[i] =
break
}
}
// 随机分配最大值
rMoney := int(float(leftMoney-leftNum) * rMax)
m := r.Intn(rMoney)
if m < {
m =
}
list[num-leftNum] = uint(m)
leftMoney -= m
leftNum--
}
// 红包得UUID
id := r.Uint()
packageList.Store(id, list)
return fmt.Sprintf("/get?id=%d&uid=%d&num=%d", id, uid, num)
}
// GetGet http://localhost:/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 < || id < 1 {
return fmt.Sprintf("")
}
listq, ok := packageList.Load(uint(id))
list := listq.([]int)
if !ok || len(list) < {
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) > {
if i == len(list)- {
packageList.Store(uint(id), list[:i])
} else if i == {
packageList.Store(uint(id), list[1:])
} else {
packageList.Store(uint(id), append(list[:i], list[i+1:]...))
}
} else {
packageList.Delete(uint(id))
}
return fmt.Sprintf("恭喜你抢到一个红包, 红包金额:%d \n", money)
}
第二种方式: chan 队列方式 解决线程安全
/**
* 微信抢红包 普通得 map 发生 竞争情况 所以需要使用 互斥 sync.Map
* 在大量得写 和 读得情况下会发生 竞争
*
* 单核任务 修改成 核心 进行抢红包
*/
package main
import (
"fmt"
"github.com/kataras/iris/v"
"github.com/kataras/iris/v/mvc"
"math/rand"
"sync"
"time"
)
// 红包列表
var packageList *sync.Map = new(sync.Map)
type task struct {
id uint
callback chan uint
}
const taskNum = // 初始化队列数量
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(":")
// 启动多个子线程进行 红包抢
for i :=; 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:
func (c *lotteryController) Get() map[uint][2]int {
// 返回当前全部得红包
rs := make(map[uint][2]int)
packageList.Range(func(key, value interface{}) bool {
id := key.(uint)
list := value.([]uint)
var money int
for _, v := range list {
money += int(v)
}
rs[id] = []int{len(list), money}
return true
})
return rs
}
// GetSet http://localhost:/set?uid=1&money=100&num=100
func (c *lotteryController) GetSet() string {
uid, errUid := c.Ctx.URLParamInt("uid")
moeny, errMoney := c.Ctx.URLParamFloat("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 *)
if uid < || 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 :=.55 // 随机分配最大值
if num > {
rMax =.01
} else if num < {
rMax =.80
}
list := make([]uint, num)
leftMoney := moenyTotal
leftNum := num
for leftNum > {
if leftNum == {
list[num-] = uint(leftMoney)
break
}
// 剩余钱数等于剩余红包数每个红包进行均分
if leftMoney == leftNum {
for i := num - leftNum; i < num; i++ {
list[i] =
break
}
}
// 随机分配最大值
rMoney := int(float(leftMoney-leftNum) * rMax)
m := r.Intn(rMoney)
if m < {
m =
}
list[num-leftNum] = uint(m)
leftMoney -= m
leftNum--
}
// 红包得UUID
id := r.Uint()
packageList.Store(id, list)
return fmt.Sprintf("/get?id=%d&uid=%d&num=%d", id, uid, num)
}
// GetGet http://localhost:/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 < || id < 1 {
return fmt.Sprintf("")
}
listq, ok := packageList.Load(uint(id))
list := listq.([]int)
if !ok || len(list) < {
return fmt.Sprintf("红包不存在, id =%d \n", id)
}
// 构造一个任务
callback := make(chan uint)
t := task{id: uint(id), callback: callback}
// 发送任务
chTasks := chTaskList[id%taskNum]
chTasks <- t
// 接受返回结果值
money := <-callback
if money <= {
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) > {
if i == len(list)- {
packageList.Store(uint(id), list[:i])
} else if i == {
packageList.Store(uint(id), list[1:])
} else {
packageList.Store(uint(id), append(list[:i], list[i+1:]...))
}
} else {
packageList.Delete(uint(id))
}
t.callback <- uint(money)
} else {
t.callback <-
}
}
}
抽奖大转盘
后端设置各个奖品得中奖概率和数量限制,更新库存时候发现并发安全性质问题 和微信摇一摇 类似
使用CAS进行安全代码进行修改,不在使用同步锁,CAS乐观锁比sync.mutSync 会快一些
/**
* 大转盘程序
* curl http://localhost:/
* curl http://localhost:/debug
* curl http://localhost:/prize
* 固定几个奖品,不同的中奖概率或者总数量限制
* 每一次转动抽奖,后端计算出这次抽奖的中奖情况,并返回对应的奖品信息
*
* 增加互斥锁,保证并发库存更新的正常
* 压力测试:
* wrk -t -c100 -d5 "http://localhost:8080/prize"
*/
package main
import (
"fmt"
"github.com/kataras/iris/v"
"github.com/kataras/iris/v/mvc"
"log"
"math/rand"
"strings"
"sync/atomic"
"time"
)
// Prate 奖品中奖概率
type Prate struct {
Rate int // 万分之N的中奖概率
Total int // 总数量限制, 表示无限数量
CodeA int // 中奖概率起始编码(包含)
CodeB int // 中奖概率终止编码(包含)
Left *int // 剩余数使用CAS乐观锁 进行修改
}
var left = int(1000)
// 奖品列表
var prizeList []string = []string{
"一等奖,火星单程船票",
"二等奖,凉飕飕南极之旅",
"三等奖,iPhone一部",
"", // 没有中奖
}
// 奖品的中奖概率设置,与上面的 prizeList 对应的设置
var rateList []Prate = []Prate{
//Prate{, 1, 0, 0, 1},
//Prate{, 2, 1, 2, 2},
Prate{, 1000, 0, 9999, &left},
//Prate{,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(":")
if err != nil {
panic(fmt.Sprintf("web server start error : %s\n", err))
}
}
// Get GET http://localhost:/
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:/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()
//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 == {
// 无限奖品
fmt.Println("中奖: ", myPrize)
return myPrize
} else if *prizeRate.Left > {
// 还有剩余奖品
left := atomic.AddInt(prizeRate.Left, -1)
if left >= {
log.Printf("奖品:%s", myPrize)
return myPrize
}
}
// 有限且没有剩余奖品,无法发奖
myPrize = "很遗憾,再来一次"
return myPrize
}
// GetDebug GET http://localhost:/debug
func (c *lotteryController) GetDebug() string {
c.Ctx.Header("Content-Type", "text/html")
return fmt.Sprintf("获奖概率: %v", rateList)
}
抽奖活动总结
- 并发安全性质问题,互斥锁,队列, CAS递减方式
- 优化,通过散列减小单个集合得大小