微博抢红包
在测试项目目录下创建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)
}
}
网友评论