iris 抽奖实例5

2021-08-12  本文已影响0人  码农工号9527

微博抢红包
在测试项目目录下创建main.go文件,内容如下:

/**
 * 微博抢红包
 * 两个步骤
 * 1 抢红包,设置红包总金额,红包个数,返回抢红包的地址
 * curl "http://localhost:8080/set?uid=1&money=100&num=100"
 * 2 抢红包,先到先得,随机得到红包金额
 * curl "http://localhost:8080/get?id=1&uid=1"
 * 注意:
 * 线程不安全1,红包列表 packageList map 的并发读写会产生异常
 * 测试方法: wrk -t10 -c10 -d5  "http://localhost:8080/set?uid=1&money=100&num=10"
 * fatal error: concurrent map writes
 * 线程不安全2,红包里面的金额切片 packageList map[uint32][]uint 并发读写不安全,虽然不会报错
 */
package main

import (
    "fmt"
    "github.com/kataras/iris/v12"
    "github.com/kataras/iris/v12/mvc"
    "log"
    "math/rand"
    "os"
    "time"
)
// 文件日志
var logger *log.Logger
// 当前有效红包列表,int64是红包唯一ID,[]uint是红包里面随机分到的金额(单位分)
var packageList map[uint32][]uint = make(map[uint32][]uint)

func main() {
    app := newApp()
    app.Run(iris.Addr(":8080"))
}

// 初始化Application
func newApp() *iris.Application {
    app := iris.New()
    mvc.New(app.Party("/")).Handle(&lotteryController{})
    return app
}

// 初始化日志
func initLog() {
    f, _ := os.Create("/var/logs/lottery_demo.log")
    logger = log.New(f, "", log.Ldate|log.Lmicroseconds)
}

// 抽奖的控制器
type lotteryController struct {
    Ctx iris.Context
}

// 返回全部红包地址
// GET http://localhost:8080/
func (c *lotteryController) Get() map[uint32][2]int {
    rs := make(map[uint32][2]int)
    for id, list := range packageList {
        var money int
        for _, v := range list {
            money += int(v)
        }
        rs[id] = [2]int{len(list),money}
    }
    return rs
}

// 发红包
// GET http://localhost:8080/set?uid=1&money=100&num=100
func (c *lotteryController) GetSet() string {
    uid, errUid := c.Ctx.URLParamInt("uid")
    money, errMoney := c.Ctx.URLParamFloat64("money")
    num, errNum := c.Ctx.URLParamInt("num")
    if errUid != nil || errMoney != nil || errNum != nil {
        return fmt.Sprintf("参数格式异常,errUid=%s, errMoney=%s, errNum=%s\n", errUid, errMoney, errNum)
    }
    moneyTotal := int(money * 100)
    if uid < 1 || moneyTotal < num || num < 1 {
        return fmt.Sprintf("参数数值异常,uid=%d, money=%d, num=%d\n", uid, money, num)
    }
    // 金额分配算法
    leftMoney := moneyTotal
    leftNum := num
    // 分配的随机数
    r := rand.New(rand.NewSource(time.Now().UnixNano()))
    rMax := 0.55 // 随机分配最大比例
    list := make([]uint, num)
    // 大循环开始,只要还有没分配的名额,继续分配
    for leftNum > 0 {
        if leftNum == 1 {
            // 最后一个名额,把剩余的全部给它
            list[num-1] = uint(leftMoney)
            break
        }
        // 剩下的最多只能分配到1分钱时,不用再随机
        if leftMoney == leftNum {
            for i:=num-leftNum; i < num ; i++ {
                list[i] = 1
            }
            break
        }
        // 每次对剩余金额的1%-55%随机,最小1,最大就是剩余金额55%(需要给剩余的名额留下1分钱的生存空间)
        rMoney := int(float64(leftMoney-leftNum) * rMax)
        m := r.Intn(rMoney)
        if m < 1 {
            m = 1
        }
        list[num-leftNum] = uint(m)
        leftMoney -= m
        leftNum--
    }
    // 最后再来一个红包的唯一ID
    id := r.Uint32()
    packageList[id] = list
    // 返回抢红包的URL
    return fmt.Sprintf("/get?id=%d&uid=%d&num=%d\n", id, uid, num)
}

// 抢红包
// GET http://localhost:8080/get?id=1&uid=1
func (c *lotteryController) GetGet() string {
    uid, errUid := c.Ctx.URLParamInt("uid")
    id, errId := c.Ctx.URLParamInt("id")
    if errUid != nil || errId != nil {
        return fmt.Sprintf("参数格式异常,errUid=%s, errId=%s\n", errUid, errId)
    }
    if uid < 1 || id < 1 {
        return fmt.Sprintf("参数数值异常,uid=%d, id=%d\n", uid, id)
    }
    list, ok := packageList[uint32(id)]
    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[uint32(id)] = list[:i]
        } else if i == 0 {
            packageList[uint32(id)] = list[1:]
        } else {
            packageList[uint32(id)] = append(list[:i], list[i+1:]...)
        }
    } else {
        delete(packageList, uint32(id))
    }
    return fmt.Sprintf("恭喜你抢到一个红包,金额为:%d\n", money)
}

启动:

[root@localhost lottery]# go run _deamon/5weiboRedPacket/main.go 
Now listening on: http://localhost:8080
Application started. Press CTRL+C to shut down.

curl设置红包并抽红包:

[root@localhost Work]# curl "http://localhost:8080/set?uid=1&money=100&num=100"
/get?id=1620842754&uid=1&num=100
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
恭喜你抢到一个红包,金额为:1300
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
......
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
恭喜你抢到一个红包,金额为:4
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
恭喜你抢到一个红包,金额为:16
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
恭喜你抢到一个红包,金额为:1

查看剩余红包数量和金额:

[root@localhost Work]# curl "http://localhost:8080/"
{
 "1620842754": [
  76,
  6046
 ]
}

抽到红包金额为空时:

[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/"
{}
[root@localhost Work]# curl "http://localhost:8080/get?id=1620842754&uid=1"
红包不存在,id=1620842754

可以发现抽到1的居多,抽奖不均,为了使抽奖金额均匀点,将rMax分配的更细致一些,加入一些条件判断:

......
    rMax := 0.55 // 随机分配最大比例
    if num > 1000 {
        rMax = 0.01
    } else if num > 100 {
        rMax = 0.1
    } else if num > 10 {
        rMax = 0.3
    }
......

重启再次尝试设置红包并抽奖:

[root@localhost Work]# curl "http://localhost:8080/set?uid=1&money=100&num=100"
/get?id=686930033&uid=1&num=100
[root@localhost Work]# curl "http://localhost:8080/get?id=686930033&uid=1"
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/get?id=686930033&uid=1"
恭喜你抢到一个红包,金额为:6
[root@localhost Work]# curl "http://localhost:8080/get?id=686930033&uid=1"
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/get?id=686930033&uid=1"
恭喜你抢到一个红包,金额为:385
[root@localhost Work]# curl "http://localhost:8080/get?id=686930033&uid=1"
恭喜你抢到一个红包,金额为:12
[root@localhost Work]# curl "http://localhost:8080/get?id=686930033&uid=1"
恭喜你抢到一个红包,金额为:92
[root@localhost Work]# curl "http://localhost:8080/get?id=686930033&uid=1"
恭喜你抢到一个红包,金额为:4
[root@localhost Work]# curl "http://localhost:8080/get?id=686930033&uid=1"
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/get?id=686930033&uid=1"
恭喜你抢到一个红包,金额为:1600
[root@localhost Work]# curl "http://localhost:8080/get?id=686930033&uid=1"
恭喜你抢到一个红包,金额为:1
[root@localhost Work]# curl "http://localhost:8080/get?id=686930033&uid=1"
恭喜你抢到一个红包,金额为:1
......

发现红包金额已经变得均匀了一些了。

在未作任何并发性问题优化的条件下,先尝试进行压力测试看看结果:

[root@localhost Work]# /usr/local/bin/wrk -t10 -c10 -d5  "http://localhost:8080/set?uid=1&money=100&num=10"
Running 5s test @ http://localhost:8080/set?uid=1&money=100&num=10
  10 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   683.77us    1.10ms  18.78ms   95.54%
    Req/Sec     1.11k   639.36     1.92k    61.54%
  2939 requests in 5.10s, 426.89KB read
  Socket errors: connect 0, read 10, write 714018, timeout 0
Requests/sec:    576.03
Transfer/sec:     83.67KB

终端看到一堆报错:

[root@localhost lottery]# go run _deamon/5weiboRedPacket/main.go 
Now listening on: http://localhost:8080
Application started. Press CTRL+C to shut down.
fatal error: concurrent map writes

goroutine 85 [running]:
runtime.throw(0xbc24d2, 0x15)
    /usr/local/go/src/runtime/panic.go:1117 +0x72 fp=0xc000239470 sp=0xc000239440 pc=0x438032
runtime.mapassign_fast32(0xaf2d20, 0xc0004df1a0, 0xcfbdcabc, 0xc000789950)
    /usr/local/go/src/runtime/map_fast32.go:101 +0x33e fp=0xc0002394b0 sp=0xc000239470 pc=0x4138be
main.(*lotteryController).GetSet(0xc0005416a0, 0x0, 0x0)
    /data/Work/lottery/_deamon/5weiboRedPacket/main.go:121 +0x425 fp=0xc0002395d8 sp=0xc0002394b0 pc=0xa66ea5
runtime.call32(0xc00005c240, 0xc000011808, 0xc0007d90e0, 0x800000018)
    /usr/local/go/src/runtime/asm_amd64.s:551 +0x3e fp=0xc000239608 sp=0xc0002395d8 pc=0x46cafe
reflect.Value.call(0xb08a00, 0xc0005416a0, 0xa13, 0xbb41b8, 0x4, 0x10e4aa0, 0x0, 0x0, 0x3, 0xc000239880, ...)
    /usr/local/go/src/reflect/value.go:476 +0x8e7 fp=0xc000239810 sp=0xc000239608 pc=0x49a887
reflect.Value.Call(0xb08a00, 0xc0005416a0, 0xa13, 0x10e4aa0, 0x0, 0x0, 0xa13, 0xe, 0x0)
    /usr/local/go/src/reflect/value.go:337 +0xb9 fp=0xc000239890 sp=0xc000239810 pc=0x499d59
github.com/kataras/iris/v12/mvc.(*ControllerActivator).handlerOf.func2(0xcd0310, 0xc000764120)
    /data/GOPATH/pkg/mod/github.com/kataras/iris/v12@v12.1.8/mvc/controller.go:497 +0x3cb fp=0xc000239988 sp=0xc000239890 pc=0xa65e4b
github.com/kataras/iris/v12/context.Do(0xcd0310, 0xc000764120, 0xc000011ad0, 0x1, 0x1)
    /data/GOPATH/pkg/mod/github.com/kataras/iris/v12@v12.1.8/context/context.go:1030 +0x82 fp=0xc0002399b8 sp=0xc000239988 pc=0x8b0f82
github.com/kataras/iris/v12/context.(*context).Do(0xc000764120, 0xc000011ad0, 0x1, 0x1)
    /data/GOPATH/pkg/mod/github.com/kataras/iris/v12@v12.1.8/context/context.go:1217 +0x55 fp=0xc0002399f0 sp=0xc0002399b8 pc=0x8b1455
github.com/kataras/iris/v12/core/router.(*routerHandler).HandleRequest(0xc0004ec240, 0xcd0310, 0xc000764120)
    /data/GOPATH/pkg/mod/github.com/kataras/iris/v12@v12.1.8/core/router/handler.go:250 +0x55f fp=0xc000239ae8 sp=0xc0002399f0 pc=0x91a19f
github.com/kataras/iris/v12/core/router.(*Router).BuildRouter.func1(0xcbfa88, 0xc000287340, 0xc000542c00)
    /data/GOPATH/pkg/mod/github.com/kataras/iris/v12@v12.1.8/core/router/router.go:135 +0x8f fp=0xc000239b48 sp=0xc000239ae8 pc=0x92466f
github.com/kataras/iris/v12/core/router.(*Router).ServeHTTP(0xc000081d10, 0xcbfa88, 0xc000287340, 0xc000542c00)
    /data/GOPATH/pkg/mod/github.com/kataras/iris/v12@v12.1.8/core/router/router.go:227 +0x48 fp=0xc000239b70 sp=0xc000239b48 pc=0x91fc88
net/http.serverHandler.ServeHTTP(0xc0004f2000, 0xcbfa88, 0xc000287340, 0xc000542c00)
    /usr/local/go/src/net/http/server.go:2887 +0xa3 fp=0xc000239ba0 sp=0xc000239b70 pc=0x7089a3
net/http.(*conn).serve(0xc0000e6280, 0xcc16c0, 0xc00043cbc0)
    /usr/local/go/src/net/http/server.go:1952 +0x8cd fp=0xc000239fc8 sp=0xc000239ba0 pc=0x703ecd
runtime.goexit()
    /usr/local/go/src/runtime/asm_amd64.s:1371 +0x1 fp=0xc000239fd0 sp=0xc000239fc8 pc=0x46e4c1
created by net/http.(*Server).Serve
    /usr/local/go/src/net/http/server.go:3013 +0x39b

goroutine 1 [IO wait]:
internal/poll.runtime_pollWait(0x7f81ef9f5538, 0x72, 0x0)
    /usr/local/go/src/runtime/netpoll.go:222 +0x55
internal/poll.(*pollDesc).wait(0xc0004af098, 0x72, 0x0, 0x0, 0xbb73a7)
    /usr/local/go/src/internal/poll/fd_poll_runtime.go:87 +0x45
internal/poll.(*pollDesc).waitRead(...)
......

看到fatal error: concurrent map writes的相关错误,是因为线程不安全,可以考虑加互斥锁,或者将packageList改为sync.map类型去处理,保证线程的安全性。然后packageList里[]uint切边也存在安全性问题,可以加入任务队列去处理它,改造后的代码如下:

/**
 * 微博抢红包
 * 两个步骤
 * 1 抢红包,设置红包总金额,红包个数,返回抢红包的地址
 * curl "http://localhost:8080/set?uid=1&money=100&num=100"
 * 2 抢红包,先到先得,随机得到红包金额
 * curl "http://localhost:8080/get?id=1&uid=1"
 * 注意:
 * 线程不安全1,红包列表 packageList map 的并发读写会产生异常
 * 测试方法: wrk -t10 -c10 -d5  "http://localhost:8080/set?uid=1&money=100&num=10"
 * fatal error: concurrent map writes
 * 线程不安全2,红包里面的金额切片 packageList map[uint32][]uint 并发读写不安全,虽然不会报错
 */
package main

import (
    "fmt"
    "github.com/kataras/iris/v12"
    "github.com/kataras/iris/v12/mvc"
    "log"
    "math/rand"
    "os"
    "sync"
    "time"
)

// 文件日志
var logger *log.Logger
// 当前有效红包列表,int64是红包唯一ID,[]uint是红包里面随机分到的金额(单位分)
//var packageList map[uint32][]uint = make(map[uint32][]uint)
var packageList *sync.Map = new(sync.Map)
var chTasks chan task = make(chan task)

func main() {
    app := newApp()
    app.Run(iris.Addr(":8080"))
}

// 初始化Application
func newApp() *iris.Application {
    app := iris.New()
    mvc.New(app.Party("/")).Handle(&lotteryController{})

    initLog()
    go fetchPackageMoney()
    return app
}

// 初始化日志
func initLog() {
    f, _ := os.Create("/var/log/lottery_demo.log")
    logger = log.New(f, "", log.Ldate|log.Lmicroseconds)
}

// 单线程死循环,专注处理各个红包中金额切片的数据更新(移除指定位置的金额)
func fetchPackageMoney() {
    for {
        t := <-chTasks
        // 分配的随机数
        r := rand.New(rand.NewSource(time.Now().UnixNano()))
        id := t.id
        l, ok := packageList.Load(id)
        if ok && l != nil {
            list := l.([]uint)
            // 从红包金额中随机得到一个
            i := r.Intn(len(list))
            money := list[i]
            //if i == len(list) - 1 {
            //  packageList[uint32(id)] = list[:i]
            //} else if i == 0 {
            //  packageList[uint32(id)] = list[1:]
            //} else {
            //  packageList[uint32(id)] = append(list[:i], list[i+1:]...)
            //}
            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 {
                //delete(packageList, uint32(id))
                packageList.Delete(uint32(id))
            }
            // 回调channel返回
            t.callback <- money
        } else {
            t.callback <- 0
        }
    }
}
// 任务结构
type task struct {
    id uint32
    callback chan uint
}

// 抽奖的控制器
type lotteryController struct {
    Ctx iris.Context
}

// 返回全部红包地址
// GET http://localhost:8080/
func (c *lotteryController) Get() map[uint32][2]int {
    rs := make(map[uint32][2]int)
    //for id, list := range packageList {
    //  var money int
    //  for _, v := range list {
    //      money += int(v)
    //  }
    //  rs[id] = [2]int{len(list),money}
    //}
    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
}

// 发红包
// GET http://localhost:8080/set?uid=1&money=100&num=100
func (c *lotteryController) GetSet() string {
    uid, errUid := c.Ctx.URLParamInt("uid")
    money, errMoney := c.Ctx.URLParamFloat64("money")
    num, errNum := c.Ctx.URLParamInt("num")
    if errUid != nil || errMoney != nil || errNum != nil {
        return fmt.Sprintf("参数格式异常,errUid=%s, errMoney=%s, errNum=%s\n", errUid, errMoney, errNum)
    }
    moneyTotal := int(money * 100)
    if uid < 1 || moneyTotal < num || num < 1 {
        return fmt.Sprintf("参数数值异常,uid=%d, money=%d, num=%d\n", uid, money, num)
    }
    // 金额分配算法
    leftMoney := moneyTotal
    leftNum := num
    // 分配的随机数
    r := rand.New(rand.NewSource(time.Now().UnixNano()))
    // 随机分配最大比例
    rMax := 0.55
    if num >= 1000 {
        rMax = 0.01
    }else if num >= 100 {
        rMax = 0.1
    } else if num >= 10 {
        rMax = 0.3
    }
    list := make([]uint, num)
    // 大循环开始,只要还有没分配的名额,继续分配
    for leftNum > 0 {
        if leftNum == 1 {
            // 最后一个名额,把剩余的全部给它
            list[num-1] = uint(leftMoney)
            break
        }
        // 剩下的最多只能分配到1分钱时,不用再随机
        if leftMoney == leftNum {
            for i:=num-leftNum; i < num ; i++ {
                list[i] = 1
            }
            break
        }
        // 每次对剩余金额的1%-55%随机,最小1,最大就是剩余金额55%(需要给剩余的名额留下1分钱的生存空间)
        rMoney := int(float64(leftMoney-leftNum) * rMax)
        m := r.Intn(rMoney)
        if m < 1 {
            m = 1
        }
        list[num-leftNum] = uint(m)
        leftMoney -= m
        leftNum--
    }
    // 最后再来一个红包的唯一ID
    id := r.Uint32()
    //packageList[id] = list
    packageList.Store(id, list)
    // 返回抢红包的URL
    return fmt.Sprintf("/get?id=%d&uid=%d&num=%d\n", id, uid, num)
}

// 抢红包
// GET http://localhost:8080/get?id=1&uid=1
func (c *lotteryController) GetGet() string {
    uid, errUid := c.Ctx.URLParamInt("uid")
    id, errId := c.Ctx.URLParamInt("id")
    if errUid != nil || errId != nil {
        return fmt.Sprintf("参数格式异常,errUid=%s, errId=%s\n", errUid, errId)
    }
    if uid < 1 || id < 1 {
        return fmt.Sprintf("参数数值异常,uid=%d, id=%d\n", uid, id)
    }
    //list, ok := packageList[uint32(id)]
    l, ok := packageList.Load(uint32(id))
    if !ok {
        return fmt.Sprintf("红包不存在,id=%d\n", id)
    }
    list := l.([]uint)
    if len(list) < 1 {
        return fmt.Sprintf("红包不存在,id=%d\n", id)
    }
    // 更新红包列表中的信息(移除这个金额),构造一个任务
    callback := make(chan uint)
    t := task{id: uint32(id), callback: callback}
    // 把任务发送给channel
    chTasks <- t
    // 回调的channel等待处理结果
    money := <- callback
    if money <= 0 {
        fmt.Println(uid, "很遗憾,没能抢到红包")
        return fmt.Sprintf("很遗憾,没能抢到红包\n")
    } else {
        fmt.Println(uid, "抢到一个红包,金额为:", money)
        logger.Printf("weiboReadPacket success uid=%d, id=%d, money=%d\n", uid, id, money)
        return fmt.Sprintf("恭喜你抢到一个红包,金额为:%d\n", money)
    }
}

重启,再次设置红包:

[root@localhost Work]# curl "http://localhost:8080/set?uid=1&money=10000&num=100000"
/get?id=3595445127&uid=1&num=100000

压测:

[root@localhost Work]# /usr/local/bin/wrk -t10 -c10 -d5  "http://localhost:8080/get?id=3595445127&uid=1"
Running 5s test @ http://localhost:8080/get?id=3595445127&uid=1
  10 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     2.17ms    2.79ms  35.62ms   88.36%
    Req/Sec   700.46    158.28     1.27k    66.80%
  35116 requests in 5.07s, 5.33MB read
Requests/sec:   6932.81
Transfer/sec:      1.05MB

可以看到有35116个请求,也就是有35116个人抢了红包,查看剩余:

[root@localhost Work]# curl "http://localhost:8080/"
{
 "3595445127": [
  64884,
  649010
 ]
}

还有64884个红包,64884+35116刚好为100000

优化:
上述队列任务是单个队列进行的,可以将它改成16个队列去处理,修改后的代码如下:

/**
 * 微博抢红包
 * 两个步骤
 * 1 抢红包,设置红包总金额,红包个数,返回抢红包的地址
 * curl "http://localhost:8080/set?uid=1&money=100&num=100"
 * 2 抢红包,先到先得,随机得到红包金额
 * curl "http://localhost:8080/get?id=1&uid=1"
 * 注意:
 * 线程不安全1,红包列表 packageList map 的并发读写会产生异常
 * 测试方法: wrk -t10 -c10 -d5  "http://localhost:8080/set?uid=1&money=100&num=10"
 * fatal error: concurrent map writes
 * 线程不安全2,红包里面的金额切片 packageList map[uint32][]uint 并发读写不安全,虽然不会报错
 * 优化 channel 的吞吐量,启动多个处理协程来执行 channel 的消费
 */
package main

import (
    "fmt"
    "github.com/kataras/iris/v12"
    "github.com/kataras/iris/v12/mvc"
    "log"
    "math/rand"
    "os"
    "sync"
    "time"
)

// 文件日志
var logger *log.Logger
// 当前有效红包列表,int64是红包唯一ID,[]uint是红包里面随机分到的金额(单位分)
//var packageList map[uint32][]uint = make(map[uint32][]uint)
var packageList *sync.Map = new(sync.Map)
//var chTasks chan task = make(chan task)
const taskNum = 16
var chTaskList []chan task = make([]chan task, taskNum)

func main() {
    app := newApp()
    app.Run(iris.Addr(":8080"))
}

// 初始化Application
func newApp() *iris.Application {
    app := iris.New()
    mvc.New(app.Party("/")).Handle(&lotteryController{})

    initLog()
    for i:=0; i<taskNum; i++ {
        chTaskList[i] = make(chan task)
        go fetchPackageMoney(chTaskList[i])
    }
    return app
}

// 初始化日志
func initLog() {
    f, _ := os.Create("/var/log/lottery_demo.log")
    logger = log.New(f, "", log.Ldate|log.Lmicroseconds)
}

// 单线程死循环,专注处理各个红包中金额切片的数据更新(移除指定位置的金额)
func fetchPackageMoney(chTasks chan task) {
    for {
        t := <-chTasks
        // 分配的随机数
        r := rand.New(rand.NewSource(time.Now().UnixNano()))
        id := t.id
        l, ok := packageList.Load(id)
        if ok && l != nil {
            list := l.([]uint)
            // 从红包金额中随机得到一个
            i := r.Intn(len(list))
            money := list[i]
            //if i == len(list) - 1 {
            //  packageList[uint32(id)] = list[:i]
            //} else if i == 0 {
            //  packageList[uint32(id)] = list[1:]
            //} else {
            //  packageList[uint32(id)] = append(list[:i], list[i+1:]...)
            //}
            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 {
                //delete(packageList, uint32(id))
                packageList.Delete(uint32(id))
            }
            // 回调channel返回
            t.callback <- money
        } else {
            t.callback <- 0
        }
    }
}
// 任务结构
type task struct {
    id uint32
    callback chan uint
}

// 抽奖的控制器
type lotteryController struct {
    Ctx iris.Context
}

// 返回全部红包地址
// GET http://localhost:8080/
func (c *lotteryController) Get() map[uint32][2]int {
    rs := make(map[uint32][2]int)
    //for id, list := range packageList {
    //  var money int
    //  for _, v := range list {
    //      money += int(v)
    //  }
    //  rs[id] = [2]int{len(list),money}
    //}
    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
}

// 发红包
// GET http://localhost:8080/set?uid=1&money=100&num=100
func (c *lotteryController) GetSet() string {
    uid, errUid := c.Ctx.URLParamInt("uid")
    money, errMoney := c.Ctx.URLParamFloat64("money")
    num, errNum := c.Ctx.URLParamInt("num")
    if errUid != nil || errMoney != nil || errNum != nil {
        return fmt.Sprintf("参数格式异常,errUid=%s, errMoney=%s, errNum=%s\n", errUid, errMoney, errNum)
    }
    moneyTotal := int(money * 100)
    if uid < 1 || moneyTotal < num || num < 1 {
        return fmt.Sprintf("参数数值异常,uid=%d, money=%d, num=%d\n", uid, money, num)
    }
    // 金额分配算法
    leftMoney := moneyTotal
    leftNum := num
    // 分配的随机数
    r := rand.New(rand.NewSource(time.Now().UnixNano()))
    // 随机分配最大比例
    rMax := 0.55
    if num >= 1000 {
        rMax = 0.01
    }else if num >= 100 {
        rMax = 0.1
    } else if num >= 10 {
        rMax = 0.3
    }
    list := make([]uint, num)
    // 大循环开始,只要还有没分配的名额,继续分配
    for leftNum > 0 {
        if leftNum == 1 {
            // 最后一个名额,把剩余的全部给它
            list[num-1] = uint(leftMoney)
            break
        }
        // 剩下的最多只能分配到1分钱时,不用再随机
        if leftMoney == leftNum {
            for i:=num-leftNum; i < num ; i++ {
                list[i] = 1
            }
            break
        }
        // 每次对剩余金额的1%-55%随机,最小1,最大就是剩余金额55%(需要给剩余的名额留下1分钱的生存空间)
        rMoney := int(float64(leftMoney-leftNum) * rMax)
        m := r.Intn(rMoney)
        if m < 1 {
            m = 1
        }
        list[num-leftNum] = uint(m)
        leftMoney -= m
        leftNum--
    }
    // 最后再来一个红包的唯一ID
    id := r.Uint32()
    //packageList[id] = list
    packageList.Store(id, list)
    // 返回抢红包的URL
    return fmt.Sprintf("/get?id=%d&uid=%d&num=%d\n", id, uid, num)
}

// 抢红包
// GET http://localhost:8080/get?id=1&uid=1
func (c *lotteryController) GetGet() string {
    uid, errUid := c.Ctx.URLParamInt("uid")
    id, errId := c.Ctx.URLParamInt("id")
    if errUid != nil || errId != nil {
        return fmt.Sprintf("参数格式异常,errUid=%s, errId=%s\n", errUid, errId)
    }
    if uid < 1 || id < 1 {
        return fmt.Sprintf("参数数值异常,uid=%d, id=%d\n", uid, id)
    }
    //list, ok := packageList[uint32(id)]
    l, ok := packageList.Load(uint32(id))
    if !ok {
        return fmt.Sprintf("红包不存在,id=%d\n", id)
    }
    list := l.([]uint)
    if len(list) < 1 {
        return fmt.Sprintf("红包不存在,id=%d\n", id)
    }
    // 更新红包列表中的信息(移除这个金额),构造一个任务
    callback := make(chan uint)
    t := task{id: uint32(id), callback: callback}
    // 把任务发送给channel
    chTasks := chTaskList[id % taskNum]
    chTasks <- t
    // 回调的channel等待处理结果
    money := <- callback
    if money <= 0 {
        fmt.Println(uid, "很遗憾,没能抢到红包")
        return fmt.Sprintf("很遗憾,没能抢到红包\n")
    } else {
        fmt.Println(uid, "抢到一个红包,金额为:", money)
        logger.Printf("weiboReadPacket success uid=%d, id=%d, money=%d\n", uid, id, money)
        return fmt.Sprintf("恭喜你抢到一个红包,金额为:%d\n", money)
    }
}
上一篇 下一篇

猜你喜欢

热点阅读