概述
业务难点
- 抽奖的业务需求,既复杂又多变
- 奖品类型和概率设置
- 如何公平的抽奖,安全的发奖?
技术挑战
- 网络并发编程,数据读写的并发安全性问题
- 高效的抽奖和发奖,提高并发和性能
- 系统优化,怎么把Redis更好的利用起来
Go的优势
- 高并发,Go协程优于Php多进程、Java多线程模式
- 高性能,编译后的二进制优于Php解释型、Java虚拟机
- 高效网络模型,epoll优于Php的BIO、Java的NIO
抽奖一:年会抽奖
代码
/*
curl http://localhost:8080/
curl --data "users=wenqiang,wenqiang2,wenqiang3" 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
}
func newApp() *iris.Application {
app := iris.New()
mvc.New(app.Party("/")).Handle(&lotteryController{})
return app
}
func (c *lotteryController) Get() string {
count := len(userList)
return fmt.Sprintf("当前总共参与抽奖的用户数:%d\n", count)
}
// POST http://localhost:8080/import params: users
func (c *lotteryController) PostImport() string {
strUsers := c.Ctx.FormValue("users")
users := strings.Split(strUsers, ",")
mu.Lock()
defer mu.Unlock()
count1 := len(users)
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, count1)
}
// GET http://localhost:8080/lucky
func (c *lotteryController) GetLucky() string {
mu.Lock()
defer mu.Unlock()
count := len(userList)
if count > 1 {
seed := time.Now().UnixNano()
index := rand.New(rand.NewSource(seed)).Int31n(int32(count))
user := userList[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]
count -= 1
userList = userList[0:0]
return fmt.Sprintf("当前中奖用户:%s,剩余用户数:%d\n", user, count)
} else {
return fmt.Sprintf("已经没有参与用户,请先通过 /import 导入用户\n")
}
}
func main() {
app := newApp()
userList = []string{}
mu = sync.Mutex{}
err := app.Run(iris.Addr(":8080"))
if err != nil {
fmt.Println("app.Run err:", err)
return
}
}
并发测试
package main
import (
"fmt"
"net/http"
"net/url"
"strconv"
"sync"
"testing"
)
func TestMVC(t *testing.T) {
var wg sync.WaitGroup
count := 0
for i := 0; i < 1000; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
var param []string
param = append(param, "user"+strconv.Itoa(i))
_, err := http.PostForm("http://localhost:8080/import", url.Values{"users": param})
if err != nil {
fmt.Println(err)
}
}(i)
count++
}
wg.Wait()
fmt.Println(count)
}
抽奖二:彩票
两种类型:即开即得型(刮刮乐)+ 双色球自选型
/*
1、即开即得型
2、双色球自选型
*/
package main
import (
"fmt"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/mvc"
"math/rand"
"time"
)
type lotteryController struct {
Ctx iris.Context
}
func newApp() *iris.Application {
app := iris.New()
mvc.New(app.Party("/")).Handle(&lotteryController{})
return app
}
func main() {
app := newApp()
err := app.Run(iris.Addr(":8080"))
if err != nil {
fmt.Println("app.Run err:", err)
}
}
// 即开即得型 http://localhost:8080/
func (c *lotteryController) Get() string {
var prize string
seed := time.Now().UnixNano()
code := rand.New(rand.NewSource(seed)).Intn(10)
switch {
case code == 1:
prize = "一等奖"
case code >= 2 && code <= 3:
prize = "二等奖"
case code >= 4 && code <= 6:
prize = "三等奖"
default:
return fmt.Sprintf("尾号为1:获得一等奖\n"+
"尾号为2/3:获得二等奖\n"+
"尾号为4/5/6:获得三等奖\n"+
"code = %d\n"+
"很遗憾,您没有获奖", code)
}
return fmt.Sprintf("尾号为1:获得一等奖\n"+
"尾号为2/3:获得二等奖\n"+
"尾号为4/5/6:获得三等奖\n"+
"code = %d\n"+
"恭喜您获得:%s", code, prize)
}
// 双色球自选型
func (c *lotteryController) GetPrize() string {
seed := time.Now().UnixNano()
r := rand.New(rand.NewSource(seed))
var prize [7]int
// 6个红色球,1-33
for i := 0; i < 6; i++ {
prize[i] = r.Intn(33) + 1
}
// 最后一个蓝色球,1-16
prize[6] = r.Intn(16) + 1
return fmt.Sprintf("今日开奖号码是:%v", prize)
}
抽奖三:微信摇一摇
代码
/**
微信摇一摇
基础功能:
/lucky 只有一个抽奖功能
*/
package main
import (
"fmt"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/mvc"
"log"
"math/rand"
"os"
"time"
)
// 奖品类型,枚举值 iota 从0开始
const (
giftTypeCoin = iota // 虚拟币
giftTypeCoupon // 不同券
giftTypeCouponFix //相同的券
giftTypeRealSmall //实物小奖
giftTypeRealLarge //实物大奖
)
type gift struct {
id int //奖品ID
name string //奖品名称
pic string //奖品图片
link string //奖品链接
gType int //奖品类型
data string //奖品的数据(特定的配置信息)
dataList []string //奖品数据集合(不同的优惠券编码)
total int //总数,0,不限量
left int //剩余的数量
inUse bool //是否在使用中
rate int //中奖概率,万分之N,0-9999
rateMin int //大于等于最小中奖编码
rateMax int //小于中奖编码
}
// 最大的中奖号码
const rateMax = 10000
var logger *log.Logger
// 奖品列表
var giftList []*gift
type lotteryController struct {
Ctx iris.Context
}
// 初始化日志
func initLog() {
f, _ := os.Create("/Users/liuwq/Documents/log/lottery_demo.log")
logger = log.New(f, "", log.Ldate|log.Lmicroseconds)
}
// 初始化奖品列表
func initGift() {
giftList = make([]*gift, 5)
g1 := gift{
id: 1,
name: "iphone 14 Pro Max 512G",
pic: "",
link: "",
gType: giftTypeRealLarge,
data: "",
dataList: nil,
total: 2,
left: 2,
inUse: true,
rate: 1,
rateMin: 0,
rateMax: 0,
}
giftList[0] = &g1
g2 := gift{
id: 2,
name: "苹果充电器",
pic: "",
link: "",
gType: giftTypeRealSmall,
data: "",
dataList: nil,
total: 5,
left: 5,
inUse: true,
rate: 10,
rateMin: 0,
rateMax: 0,
}
giftList[1] = &g2
g3 := gift{
id: 3,
name: "满200-50优惠券",
pic: "",
link: "",
gType: giftTypeCouponFix,
data: "mall-coupon-2023",
dataList: nil,
total: 50,
left: 50,
inUse: true,
rate: 500,
rateMin: 0,
rateMax: 0,
}
giftList[2] = &g3
g4 := gift{
id: 4,
name: "直降优惠券50元",
pic: "",
link: "",
gType: giftTypeCoupon,
data: "",
dataList: []string{"c01", "c02", "c03", "c04", "c05", "c06", "c07", "c08", "c09", "c10"},
total: 10,
left: 10,
inUse: true,
rate: 100,
rateMin: 0,
rateMax: 0,
}
giftList[3] = &g4
g5 := gift{
id: 5,
name: "金币",
pic: "",
link: "",
gType: giftTypeCoin,
data: "200金币",
dataList: nil,
total: 50,
left: 50,
inUse: true,
rate: 5000,
rateMin: 0,
rateMax: 0,
}
giftList[4] = &g5
//数据整理,中奖区间数据
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 newApp() *iris.Application {
app := iris.New()
mvc.New(app.Party("/")).Handle(&lotteryController{})
initLog()
initGift()
return app
}
func main() {
app := newApp()
err := app.Run(iris.Addr(":8080"))
if err != nil {
fmt.Println("app.Run err", 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", count, total)
}
func luckyCode() int32 {
seed := time.Now().UnixNano()
code := rand.New(rand.NewSource(seed)).Int31n(int32(rateMax))
return code
}
// 抽奖 GET:http://localhost:8080/lucky
func (c *lotteryController) GetLucky() map[string]interface{} {
code := luckyCode()
var ok bool
result := make(map[string]interface{})
result["success"] = false
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 giftTypeCoupon:
ok, sendData = sendCoupon(data)
case giftTypeCoin:
ok, sendData = sendCoin(data)
case giftTypeCouponFix:
ok, sendData = sendCouponFix(data)
case giftTypeRealSmall:
ok, sendData = sendRealSmall(data)
case giftTypeRealLarge:
ok, sendData = sendRealLarge(data)
}
if ok {
// 中奖后,成功得到奖品
// 生成中奖记录
saveLuckyData(code, data.id, data.name, data.link, sendData, data.left)
result["success"] = ok
result["id"] = data.id
result["name"] = data.name
result["link"] = data.link
result["data"] = sendData
break
}
}
}
return result
}
// 不同值的优惠券
func sendCoupon(data *gift) (bool, string) {
if data.left > 0 {
// 还有剩余
left := data.left - 1
data.left = left
return true, data.dataList[left]
} 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 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 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, id int, name string, link string, sendData string, left int) {
logger.Printf("lucky, code = %d, gift = %d, name = %s, link = %s, sendData = %s, left = %d\n", code, id, name, link, sendData, left)
}
并发压力测试
手机奖品增加数量为20000个,命中率100%,其余奖品设置为false
压力测试:wrk -t10 -c10 -d5 http://localhost:8080/lucky
看一下日志:wc -l /Users/liuwq/Documents/log/lottery_demo.log
出现了超发的问题!!!线程不安全!
问题解决:加锁。
// 并发锁
var mu sync.Mutex
// 抽奖 GET:http://localhost:8080/lucky
func (c *lotteryController) GetLucky() map[string]interface{} {
mu.Lock()
defer mu.Unlock()
code := luckyCode()
var ok bool
result := make(map[string]interface{})
result["success"] = false
......
抽奖四:支付宝集福卡
代码
package main
import (
"fmt"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/mvc"
"log"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
type gift struct {
id int //奖品ID
name string //奖品名称
pic string //奖品图片
link string //奖品链接
inUse bool //是否在使用中
rate int //中奖概率,万分之N,0-9999
rateMin int //大于等于最小中奖编码
rateMax int //小于中奖编码
}
// 最大的中奖号码
const rateMax = 10
var logger *log.Logger
type lotteryController struct {
Ctx iris.Context
}
// 初始化日志
func initLog() {
f, _ := os.Create("/Users/liuwq/Documents/log/lottery_demo.log")
logger = log.New(f, "", log.Ldate|log.Lmicroseconds)
}
func newApp() *iris.Application {
app := iris.New()
mvc.New(app.Party("/")).Handle(&lotteryController{})
initLog()
return app
}
func main() {
app := newApp()
err := app.Run(iris.Addr(":8080"))
if err != nil {
fmt.Println("app.Run err", err)
}
}
func newGift() *[5]gift {
giftList := new([5]gift)
g1 := gift{
id: 1,
name: "富强福",
pic: "富强福.jpg",
link: "",
inUse: true,
rate: 0,
rateMin: 0,
rateMax: 0,
}
giftList[0] = g1
g2 := gift{
id: 2,
name: "和谐福",
pic: "和谐福.jpg",
link: "",
inUse: true,
rate: 0,
rateMin: 0,
rateMax: 0,
}
giftList[1] = g2
g3 := gift{
id: 3,
name: "友善福",
pic: "友善福.jpg",
link: "",
inUse: true,
rate: 0,
rateMin: 0,
rateMax: 0,
}
giftList[2] = g3
g4 := gift{
id: 4,
name: "爱国福",
pic: "爱国福.jpg",
link: "",
inUse: true,
rate: 0,
rateMin: 0,
rateMax: 0,
}
giftList[3] = g4
g5 := gift{
id: 5,
name: "敬业福",
pic: "敬业福.jpg",
link: "",
inUse: true,
rate: 0,
rateMin: 0,
rateMax: 0,
}
giftList[4] = g5
return giftList
}
func giftRate(rate string) *[5]gift {
giftList := newGift()
rates := strings.Split(rate, ",")
ratesLen := len(rates)
//数据整理,中奖区间数据
rateStart := 0
for i, data := range giftList {
if !data.inUse {
continue
}
grate := 0
if i < ratesLen {
grate, _ = strconv.Atoi(rates[i])
}
giftList[i].rate = grate
giftList[i].rateMin = rateStart
giftList[i].rateMax = rateStart + grate
if giftList[i].rateMax >= rateMax {
giftList[i].rateMax = rateMax
rateStart = 0
} else {
rateStart += grate
}
}
fmt.Printf("giftList = %v \n", giftList)
return giftList
}
// http://localhost:8080/?rate=4,3,2,1,0
func (c *lotteryController) Get() string {
rate := c.Ctx.URLParamDefault("rate", "4,3,2,1,0")
giftList := giftRate(rate)
return fmt.Sprintf("%v\n", giftList)
}
func luckyCode() int32 {
seed := time.Now().UnixNano()
code := rand.New(rand.NewSource(seed)).Int31n(int32(rateMax))
return code
}
// 抽奖 GET:http://localhost:8080/lucky?uid=1&rate=4,3,2,1
func (c *lotteryController) GetLucky() map[string]interface{} {
uid, _ := c.Ctx.URLParamInt("uid")
rate := c.Ctx.URLParamDefault("rate", "4,3,2,1,0")
code := luckyCode()
result := make(map[string]interface{})
giftList := giftRate(rate)
for _, data := range giftList {
if !data.inUse {
continue
}
if data.rateMin <= int(code) && data.rateMax >= int(code) {
// 中奖了,抽奖编码在奖品编码范围内
sendData := data.pic
// 中奖后,成功得到奖品
// 生成中奖记录
saveLuckyData(code, data.id, data.name, data.link, sendData)
result["success"] = true
result["uid"] = uid
result["id"] = data.id
result["name"] = data.name
result["link"] = data.link
result["data"] = sendData
break
}
}
return result
}
func saveLuckyData(code int32, id int, name string, link string, sendData string) {
logger.Printf("lucky, code = %d, gift = %d, name = %s, link = %s, sendData = %s\n", code, id, name, link, sendData)
}
并发压力测试
因为集福卡不存在共享变量,也就没有并发安全性问题,giftList 每次都是 new 一个。
压力测试:wrk -t10 -c10 -d5 http://localhost:8080/lucky
抽奖五:微博抢红包
代码
/**
设置红包:http://localhost:8080/set?uid=1&money=100&num=100
抢红包:http://localhost:8080/get?uid=1&id=131220125
并发压力测试:wrk -t10 -c10 -d5 "http://localhost:8080/set?uid=1&money=100&num=100"
*/
package main
import (
"fmt"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/mvc"
"math/rand"
"time"
)
// 红包列表
var packetList map[uint32][]uint = make(map[uint32][]uint)
type lotteryController struct {
Ctx iris.Context
}
func newApp() *iris.Application {
app := iris.New()
mvc.New(app.Party("/")).Handle(&lotteryController{})
return app
}
func main() {
app := newApp()
err := app.Run(iris.Addr(":8080"))
if err != nil {
fmt.Println("app.Run err:", err)
return
}
}
// 返回全部红包地址 http://localhost:8080/
func (c *lotteryController) Get() map[uint32][2]int {
rs := make(map[uint32][2]int)
for id, list := range packetList {
var money int
for _, v := range list {
money += int(v)
}
rs[id] = [2]int{len(list), money}
}
return rs
}
// 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=%d, errMoney=%d, errNum=%d\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)
}
// 金额分配算法
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)
leftMoney := moneyTotal
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--
}
//红包的唯一ID
id := r.Uint32()
packetList[id] = list
//返回抢红包的URL
return fmt.Sprintf("/get?id=%d&uid=%d&num=%d", id, uid, num)
}
// 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("")
}
if uid < 1 || id < 1 {
return fmt.Sprintf("")
}
list, ok := packetList[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 {
packetList[uint32(id)] = list[:i]
} else if i == 0 {
packetList[uint32(id)] = list[1:]
} else {
packetList[uint32(id)] = append(list[:i], list[i+1:]...)
}
} else {
delete(packetList, uint32(id))
}
return fmt.Sprintf("恭喜你抢到一个红包,金额为:%d", money)
}
并发压力测试
压力测试:wrk -t10 -c10 -d5 http://localhost:8080/set?uid=1&money=100&num=100
可以看到控制台报错了~
优化1
是因为针对map这种数据结构,本身就是不安全的。
问题解决:
- 采用互斥锁
- 采用同步方法
sync.Map()
// var packetList map[uint32][]uint = make(map[uint32][]uint)
var packetList *sync.Map = new(sync.Map)
//for id, list := range packetList {
// var money int
// for _, v := range list {
// money += int(v)
// }
// rs[id] = [2]int{len(list), money}
//}
packetList.Range(func(key, value any) 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
})
// packetList[id] = list
packetList.Store(id, list)
//list, ok := packetList[uint32(id)]
list1, ok := packetList.Load(uint32(id))
list := list1.([]int)
//更新红包列表中的信息
if len(list) > 1 {
if i == len(list)-1 {
//packetList[uint32(id)] = list[:i]
packetList.Store(uint32(id), list[:i])
} else if i == 0 {
//packetList[uint32(id)] = list[1:]
packetList.Store(uint32(id), list[1:])
} else {
//packetList[uint32(id)] = append(list[:i], list[i+1:]...)
packetList.Store(uint32(id), append(list[:i], list[i+1:]...))
}
} else {
//delete(packetList, uint32(id))
packetList.Delete(uint32(id))
}
改造完成!!
优化2
接下来,针对list切片的线程安全性做出优化:
/**
设置红包:http://localhost:8080/set?uid=1&money=100&num=100
抢红包:http://localhost:8080/get?uid=1&id=131220125
并发压力测试:wrk -t10 -c10 -d5 "http://localhost:8080/set?uid=1&money=100&num=100"
*/
package main
import (
"fmt"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/mvc"
"math/rand"
"sync"
"time"
)
// 任务结构
type task struct {
id uint32
callback chan int
}
// 红包列表
var packetList *sync.Map = new(sync.Map)
var chTasks chan task = make(chan task)
type lotteryController struct {
Ctx iris.Context
}
func newApp() *iris.Application {
app := iris.New()
mvc.New(app.Party("/")).Handle(&lotteryController{})
go fetchPackagelistMoney()
return app
}
func main() {
app := newApp()
err := app.Run(iris.Addr(":8080"))
if err != nil {
fmt.Println("app.Run err:", err)
return
}
}
// 返回全部红包地址 http://localhost:8080/
func (c *lotteryController) Get() map[uint32][2]int {
rs := make(map[uint32][2]int)
packetList.Range(func(key, value any) 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
}
// 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=%d, errMoney=%d, errNum=%d\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)
}
// 金额分配算法
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)
leftMoney := moneyTotal
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--
}
//红包的唯一ID
id := r.Uint32()
// packetList[id] = list
packetList.Store(id, list)
//返回抢红包的URL
return fmt.Sprintf("/get?id=%d&uid=%d&num=%d", id, uid, num)
}
// 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=%d, errId=%d\n", errUid, errId)
}
if uid < 1 || id < 1 {
return fmt.Sprintf("参数数值异常,uid=%d, id=%d\n", uid, id)
}
list1, ok := packetList.Load(uint32(id))
list := list1.([]int)
if !ok || len(list) < 1 {
return fmt.Sprintf("红包不存在,id=%d\n", id)
}
//构造一个抢红包任务
callBack := make(chan int)
t := task{
id: uint32(id),
callback: callBack,
}
//发送任务
chTasks <- t
//接收返回结果
money := <-callBack
if money <= 0 {
return "很遗憾,么有抢到红包!\n"
} else {
return fmt.Sprintf("恭喜你抢到一个红包,金额为:%d", money)
}
}
func fetchPackagelistMoney() {
for {
t := <-chTasks
id := t.id
l, ok := packetList.Load(uint32(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 {
packetList.Store(uint32(id), list[:i])
} else if i == 0 {
packetList.Store(uint32(id), list[1:])
} else {
packetList.Store(uint32(id), append(list[:i], list[i+1:]...))
}
} else {
packetList.Delete(uint32(id))
}
t.callback <- money
} else {
t.callback <- 0
}
}
}
优化3
将单任务消息队列改造为多任务消息队列
// var chTasks chan task = make(chan task)
const taskNum = 16
var chTaskList []chan task = make([]chan task, taskNum)
func newApp() *iris.Application {
app := iris.New()
mvc.New(app.Party("/")).Handle(&lotteryController{})
for i := 0; i < taskNum; i++ {
chTaskList[i] = make(chan task)
go fetchPackagelistMoney(chTaskList[i])
}
return app
}
//发送任务
chTasks := chTaskList[id%taskNum]
chTasks <- t
func fetchPackagelistMoney(chTasks chan task) {
......
}
优化4
可以考虑将packetList也改造为一个16个的。
抽奖六:抽奖大转盘
代码
package main
import (
"fmt"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/mvc"
"math/rand"
"strings"
"time"
)
type Prate struct {
Rate int // 万分之N的中奖概率
Total int // 总数量限制,0表示无限
CodeA int // 中奖概率起始编码(包含)
CodeB int // 中奖概率终止编码(包含)
Left int // 剩余数
}
// 奖品列表
var prizeList []string = []string{
"一等奖,火星单程船票一张",
"二等奖,凉飕飕南极双人旅一次",
"三等奖,银色 iphone 14 Pro Max 512G 一部",
"", //代表没有中奖
}
// 奖品的中奖概率设置,与上面的prizeList对应的设置
var rateList []Prate = []Prate{
Prate{1, 1, 0, 0, 1},
Prate{2, 2, 1, 2, 2},
Prate{5, 10, 3, 5, 10},
Prate{100, 0, 0, 9999, 0},
}
// 抽奖的控制器
type lotteryController struct {
Ctx iris.Context
}
func newApp() *iris.Application {
app := iris.New()
mvc.New(app.Party("/")).Handle(&lotteryController{})
return app
}
func main() {
app := newApp()
err := app.Run(iris.Addr(":8080"))
if err != nil {
fmt.Println("app.Run err:", err)
return
}
}
// 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"))
}
func (c *lotteryController) GetDebug() string {
return fmt.Sprintf("获奖概率: %v\n", rateList)
}
func (c *lotteryController) GetPrize() string {
// 第一步,抽奖,根据随机数匹配奖品
seed := time.Now().UnixNano()
r := rand.New(rand.NewSource(seed))
code := r.Intn(10000)
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 {
//无限量奖品
return myprize
} else if prizeRate.Left > 0 {
prizeRate.Left -= 1
log.Println("奖品:", myprize)
return myprize
} else {
myprize = "很遗憾,再来一次吧~"
return myprize
}
}
并发压力测试
设置一下奖品的概率
{100, 1000, 0, 9999, 1000},
压力测试:wrk -t10 -c10 -d5 http://localhost:8080/prize
对中奖条数进行count,查看控制台count:
发现多了6条数据,说明并发不安全!
加锁
var mu sync.Mutex = sync.Mutex{}
mu.Lock()
prizeRate.Left -= 1
mu.Unlock()
使用原子整型
Left *int32 // 剩余数
var left = int32(1000)
var rateList []Prate = []Prate{
Prate{100, 1000, 0, 9999, &left},
}
//第二步,中奖,开始要发奖
if prizeRate.Total == 0 {
//无限量奖品
return myprize
} else if *prizeRate.Left > 0 {
left := atomic.AddInt32(prizeRate.Left, -1)
if left >= 0 {
log.Println("奖品:", myprize)
count++
fmt.Println("count = ", count)
return myprize
}
}
myprize = "很遗憾,再来一次吧~"
return myprize
系统设计和架构设计
需求整理和提炼
前端页面的需求
- 交互效果,大转盘的展示
- 用户登录,每天抽奖次数限制
- 获奖提示和中奖列表
后端接口的需求
- 奖品列表、抽奖、中奖列表
- 抽奖接口需要满足高性能和高并发要求
- 安全抽奖,奖品不能超发,合理均匀发放
后台管理的需求
- 基本数据管理:奖品、优惠券、用户、IP黑名单、中奖记录
- 实时更新奖品信息,更新奖品库存,奖品中奖周期等
- 后台定时任务,生成发奖计划,填充奖品池
用户操作和业务流程
用户操作步骤
奖品状态变化
抽奖业务流程
数据库设计
数据表
奖品表
优惠券表
抽奖记录表
用户(黑名单)表
IP黑名单表
用户每日次数表
缓存设计
设计和利用缓存
- 目标:提高系统性能,减少数据库依赖
- 原则:平衡好“系统性能、开发时间、复杂度”
- 方向:数据读多写少,数据量有限,数据分散
使用Redis缓存的地方
- 奖品:数量少,更新频率低,最佳的全量缓存对象
- 优惠券:一次性导入,优惠券编码缓存为set类型
- 中奖记录:读写差不多,可以缓存部分统计数据,如:最新中奖记录,最近大奖发放记录等
- 用户黑名单:读多写少,可以按照uid散列
- IP黑名单,类似用户黑名单,可以按照IP散列
- 用户每日参与次数:读和写次数差异没有黑名单那么明显,缓存后的收益不明显
系统架构设计
网络架构图
系统架构图
项目框架和核心代码
数据模型的生成(xorm-cmd)
-
数据库连接:
root:12345678@(127.0.0.1:3306)/lucky_wheel?charset=utf8 -
cd /private/var/www/go/src/github.com/go-xorm/cmd/xorm -
./xorm reverse mysql 数据库账号:密码@(127.0.0.1:3306)/数据库名?charset=utf8 templates/goxorm # 在models目录就是生成的数据模型文件
核心的dao和service类
dao面向数据库,service面向数据服务
- dao的基础方法:Get、GetAll、CountAll、Search、Delete、Update、Create
- service的基础方法:Get、GetAll、CountAll、Search、Delete、Update、Create
- 特殊方法:例如根据ip查找之类的
用户登陆和退出
基于Cookie的用户状态
- ObjLoginuser 登录用户对象
- 登录用户对象与Cookie的读写
- Cookie的安全校验值,不能被篡改
Redis缓存优化
- 性能,大量数据都可以缓存,大量的读取直接通过Redis
- 原子性操作,redis中的数据递增、递减操作,优于MySQL中的操作
- Redis缓存属于新增加的冗余数据,要注意数据更新保持一致性
奖品的发奖计划数据维护
精确到每分钟的发奖计划
- utils.ResetGiftPrizeData 重新计算奖品的发奖计划数据
- 发奖周期内每天,每分钟的发奖数量的概率是相同的
- 一天中不同时段需要根据预设的概率,发放奖品的数量不同
自动填充奖品池的服务
根据发奖计划填充奖品池
- utils.DistributionGiftPool 既要更新奖品池,又要更新发奖计划数据
- incrGiftPool 并发的增加奖品池库存,二次补充库存
- 自动填充奖品池的服务
奖品池与发奖计划的实现
总结
- 发奖计划,让奖品的数量合理的分配到发奖周期内
- 每天不同时段的时段根据访问量来设置概率更加合理
- 根据发奖计划填充奖品池,保证奖品可以有节奏的发放
如何设置奖品更加合理
更详细的奖品属性
- 数量,关于不限量奖品,因为每次中奖都能发奖,性能会差些
- 概率,预估每天的参与人数,根据总体奖品数量设置概率
- 位置排序,中奖概率大的放在前面,如果中奖编码区间会重叠,则范围小的放在前面
完全的随机还是人为控制发放的节奏
- 同一个IP得到多次大奖,同一个用户得到多次大奖,合理吗?(用户黑名单、IP黑名单)
- 奖品集中在一个较短的时段内全部发放出去,合理吗?(发奖计划、奖品池)
- 所有用户获得大奖的概率一样,合理吗?(用户等级)
更多的运营策略
抽奖的运营策略
- 精准的发放大奖,设置有效时间,限定用户等级,特定用户?
- 虚拟券与电商的优惠券结合,与游戏推广码结合?
- 抽奖需要消耗虚拟币或者其他虚拟积分?
可以引入thrift框架
通过thrift实现rpc的服务端和客户端
- 封装传输和序列化协议,让不同语言的系统像本地代码一样调用
- 定义thrift,生成各语言的代码,实现Go服务端和其他客户端代码
- Go和其他语言都基于thrift框架编程,减少联调和定制的开发时间
问题和思考
架构方面
-
简单描述下抽奖系统的网络架构?
- 客户端以及网络连接,服务端负载均衡,服务端应用服务器,数据库,缓存,存储服务
-
从用户的访问开始,到得到抽奖的返回结构,会经历怎样的一个网络请求?
-
在系统架构分层设计方面,会分为哪几层,分别有哪些内容呢?
- 业务层、框架层(web框架、rpc框架等)、服务层(第三方服务)、资源层(数据库、缓存、存储等)
并发安全性方面
- 防重入问题,一个用户每天只有一次抽奖机会,怎么保证不不超过唯一的一次抽奖呢?
- 奖品库存更新问题,大量的用户并发请求抽奖,中奖后奖品的库存数量扣减,怎么保证库存扣减准确无误,不出现多扣或者少扣呢?
- 优惠券发放问题,不同的优惠券,怎么保证抽奖时,一个优惠券只能发送给一个用户,而不会出现同一个优惠券发给了多个用户?
抽奖系统的难点方面
-
基于数据库的抽奖系统,性能方面会有很大的瓶颈,那么使用Redis缓存来做优化的时候,会怎么来设计Redis缓存的使用呢?什么情况合适,什么情况不合适使用缓存呢?
- 作为一个整体系统,后台管理工作量大,数据库、缓存,多份数据的维护,增加了复杂度、开发难度和工作量
- 读多写少,适合用缓存
-
抽奖的时候,中奖概率是随机匹配,怎么提高抽奖效率?
- 随机以及奖品匹配
-
发奖的时候,为了保证公平性,在各个时段都能均匀的发奖,要怎么来设计和实现呢?
- 引入奖品池,按照预计的节奏发奖
运营策略方面
-
针对用户和请求,会考虑哪些情况和限制规则呢?
- 用户限制,一天内抽奖次数,大奖不要重复给到同一个人
- IP限制,一天内抽奖次数的限制,大奖的限制
- 用户等级限制,新用户机会更多还是老用户机会更多?(老用户)
-
黑名单和白名单的规则,作用和目的是什么呢?
-
为了区分用户价值,要怎么来设计运营策略和规则呢?
完整代码
github.com/liuwqTech/l…(记得star~)