实战-企业级抽奖系统 | 青训营

258 阅读16分钟

概述

业务难点

  • 抽奖的业务需求,既复杂又多变
  • 奖品类型和概率设置
  • 如何公平的抽奖,安全的发奖

技术挑战

  • 网络并发编程,数据读写的并发安全性问题
  • 高效的抽奖和发奖,提高并发和性能
  • 系统优化,怎么把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 []stringvar 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 = 10000var 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 = 10var 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)

  1. 数据库连接:

    root:12345678@(127.0.0.1:3306)/lucky_wheel?charset=utf8
    
  2. cd /private/var/www/go/src/github.com/go-xorm/cmd/xorm
    
  3. ./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~)