概述

业务难点

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

技术挑战

  • 网络并发编程,数据读写的并发安全性问题
  • 高效的抽奖和发奖,提高并发和性能
  • 系统优化,怎么把Redis更好的利用起来

Go的优势

  • 高并发,Go协程优于Php多进程、Java多线程模式
  • 高性能,编译后的二进制优于Php解释型、Java虚拟机
  • 高效网络模型,epoll优于Php的BIO、Java的NIO

抽奖一:年会抽奖

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/*
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
}
}

并发测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/*
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)
}

抽奖三:微信摇一摇

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
/**
微信摇一摇
基础功能:
/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

出现了超发的问题!!!线程不安全

问题解决:加锁

1
2
3
4
5
6
7
8
9
10
11
12
13
// 并发锁
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
......

抽奖四:支付宝集福卡

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
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

抽奖五:微博抢红包

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
设置红包: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()
1
2
// var packetList map[uint32][]uint = make(map[uint32][]uint)
var packetList *sync.Map = new(sync.Map)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	//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
})
1
2
// packetList[id] = list
packetList.Store(id, list)
1
2
3
//list, ok := packetList[uint32(id)]
list1, ok := packetList.Load(uint32(id))
list := list1.([]int)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//更新红包列表中的信息
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切片的线程安全性做出优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
/**
设置红包: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

将单任务消息队列改造为多任务消息队列

1
2
3
4
// var chTasks chan task = make(chan task)

const taskNum = 16
var chTaskList []chan task = make([]chan task, taskNum)
1
2
3
4
5
6
7
8
9
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
}
1
2
3
//发送任务
chTasks := chTaskList[id%taskNum]
chTasks <- t
1
2
3
func fetchPackagelistMoney(chTasks chan task) {
......
}

优化4

可以考虑将packetList也改造为一个16个的。

抽奖六:抽奖大转盘

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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
}
}

并发压力测试

设置一下奖品的概率

1
{100, 1000, 0, 9999, 1000},

压力测试:wrk -t10 -c10 -d5 http://localhost:8080/prize

对中奖条数进行count,查看控制台count:

发现多了6条数据,说明并发不安全!

加锁

1
2
3
4
5
var mu sync.Mutex = sync.Mutex{}

mu.Lock()
prizeRate.Left -= 1
mu.Unlock()

使用原子整型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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. 数据库连接:

    1
    root:12345678@(127.0.0.1:3306)/lucky_wheel?charset=utf8
  2. ```shell
    cd /private/var/www/go/src/github.com/go-xorm/cmd/xorm

    1
    2
    3
    4

    3. ```shell
    ./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限制,一天内抽奖次数的限制,大奖的限制
    • 用户等级限制,新用户机会更多还是老用户机会更多?(老用户)
  • 黑名单和白名单的规则,作用和目的是什么呢?
  • 为了区分用户价值,要怎么来设计运营策略和规则呢?

完整代码

https://github.com/liuwqTech/lottery_lucky_wheel(记得star~)