Go商城秒杀系统
仓库地址:https://github.com/qingbo1011/iris-seckill
参考学习视频:全流程开发 GO实战电商网站高并发秒杀系统
更好的学习课程:Gin+Vue+微服务打造秒杀商城-Go语言
主要学习一下秒杀的思路,不用太拘泥于项目接口细节。因为这个项目不是前后端分离的项目,视频的内容比较老,所以我也做了一些改进。
技术栈
- Iris
- gorm
- MySQL
- Redis
- RabbitMQ
- go
html/template
包 - 一致性Hash算法
iris框架用起来,感觉还是没有gin舒服,gin的风格更加贴切go。
gorm选择的版本是gorm v1。后续可以升级到gorm v2。
go
html/template
包参考笔记:Template
关于Iris的教程,直接去看官网。官方提供的文档真的是非常非常好,强烈推荐!
Iris安装
Iris的安装有点小坑,必须要手动go get github.com/kataras/iris/v12@master
,如果只靠goland的Sync,下载下来的包是不完整的。
路由分析
通过controller如何分析出路由呢,以GET请求,/product/all
为例:
在main.go的注册路由器中:
在controller/product.go
中:
所以这个接口的cURL是:
1 | curl --location --request GET 'http://127.0.0.1:8080/product/all' |
这种的,该怎么说呢😯😥😔
而且本项目并不是前后端分离架构,所以接口返回的不是JSON串而是html
。
注意事项
- 在修改这里,我都是根据产品的名称进行修改的。(因为根据产品id,需要从html中解析。不想浪费太多的时间在这里,注重秒杀逻辑就好。)所以在修改时注意不要修改产品名称!
- gorm使用v1,以后可以升级为v2
- 暂时没有加分页查询
- 订单相关的
GetAllOrderInfo
api没有做好(主要是看不懂他那个map[int]map[string]string
到底是要返回个啥?) - 在前台的product controller的
GetDetail
中,这里是直接把product_id写死了(视频中也是这样,因为我们关注的是秒杀功能) - 登录可完善:结合redis
- 跨平台交叉编译(在 Mac、Linux、Windows 下Go交叉编译)(注意windows下面 powershell不行,要cmd)
Wrk压测工具介绍
wrk - a HTTP benchmarking tool
github:https://github.com/wg/wrk
Wrk只支持类Unix系统,能用少量的线程测大量的连接。
Ubuntu/Debian下安装,依次执行如下命令:
1
2
3
4
5
6 sudo apt-get install build-essential libssl-dev git -y
git clone https://github.com/wg/wrk.git wrk
cd wrk
make
# 将可执行文件移动到 /usr/local/bin 位置
sudo cp wrk /usr/local/binCentOS / RedHat / Fedora下安装,依次执行如下命令:
1
2
3
4
5
6
7 sudo yum groupinstall 'Development Tools'
sudo yum install -y openssl-devel git
git clone https://github.com/wg/wrk.git wrk
cd wrk
make
# 将可执行文件移动到 /usr/local/bin 位置
sudo cp wrk /usr/local/bin验证是否安装成功:
wrk -v
执行压测命令:
1 wrk -t12 -c400 -d30s --latency http://www.baidu.com命令说明:使用12个线程400个连接,对
http://www.baidu.com
进行了30秒的压测,并要求在压测结果中输出响应延迟信息。执行上面的压测命令,30 秒压测过后,生成如下压测报告:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 Running 30s test @ http://www.baidu.com
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 386.32ms 380.75ms 2.00s 86.66%
Req/Sec 17.06 13.91 252.00 87.89%
Latency Distribution
50% 218.31ms
75% 520.60ms
90% 955.08ms
99% 1.93s
4922 requests in 30.06s, 73.86MB read
Socket errors: connect 0, read 0, write 0, timeout 311
Requests/sec: 163.76
Transfer/sec: 2.46MB我们来具体说一说,报告中各项指标都代表什么意思:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 Running 30s test @ http://www.baidu.com (压测时间30s)
12 threads and 400 connections (共12个测试线程,400个连接)
(平均值) (标准差) (最大值)(正负一个标准差所占比例)
Thread Stats Avg Stdev Max +/- Stdev
(延迟)
Latency 386.32ms 380.75ms 2.00s 86.66%
(每秒请求数)
Req/Sec 17.06 13.91 252.00 87.89%
Latency Distribution (延迟分布)
50% 218.31ms
75% 520.60ms
90% 955.08ms
99% 1.93s
4922 requests in 30.06s, 73.86MB read (30.06s内处理了4922个请求,耗费流量73.86MB)
Socket errors: connect 0, read 0, write 0, timeout 311 (发生错误数)
Requests/sec: 163.76 (QPS 163.76,即平均每秒处理请求数为163.76)
Transfer/sec: 2.46MB (平均每秒流量2.46MB)可以看到,压测报告还是非常直观的!
(标准差如果太大说明样本本身离散程度比较高,有可能系统性能波动较大。)
秒杀逻辑和优化
最基础的版本中,存在问题如下:
秒杀分布式架构设计
总体来说分为两步:
- 筛选有效流量
- 异步处理数据
前端优化
- 页面静态化(通过代码实现,在项目中通过go语言在front/web/GetGenerateHtmlcontroller/product下的
generateStaticHtml()
函数)
CDN
服务端优化
后端优化:
- 突破Session限制
- 分布式接口实现
- 解决接口超卖问题
- 引入RabbitMQ实现秒杀队列
优化思路:
- 系统特征:高并发,大流量
- 优化方向:提高网站性能,保护数据库
- 具体措施:静态化、分布式、消息队列
优化架构:
突破Session限制
- Cookie替代Session集群
- 登录代码重构
代码体现在:
- 新增
util/encrypt.go
的加密解密方法 - 重构登录逻辑
/front/web/controller/user.go
下的PostLogin
用户登陆接口- 弃用session,存储cookie
分布式接口实现
- 引入分布式及代码架构调整
- 一致性Hash原理
- 分布式存储实现(突破Redis瓶颈限制)
一致性Hash算法:
- 用途:快速定位资源,均匀分布
- 场景:分布式存储,分布式缓存,负载均衡
原理:
解决服务器较少情况下的数据倾斜问题:
代码体现在:
- 在
util/
下新增过滤器filter.go
- 在根目录下新增
validate.go
- 在
util/
下新增consistent.go
,实现一致性Hash算法
分布式存储实现(突破Redis瓶颈限制)
Redis使用方式:
-
单机或者主从:单击QPS 8w左右
-
Redis Cluster集群:QPS能达千万以上
-
Redis分布式:QPS能达千万以上
代码体现在:
- 在根目录下新建
getOne.go
压力测试结果:27w QPS
引入RabbitMQ
这里使用Simple模式进行学习(Go操作RabbitMQ)
代码体现在:
- 在
front/web/controller/
下的product.go
中,修改GetOrder
的下订单逻辑 - 新建
mq/rabbit/
下的message.go
- 在根目录下新建
consumer.go
rabbit-生产者-消费者
rabbit.go
代码如下:
1 | package simple |
rabbit.go
和Go操作RabbitMQ笔记中的差别:
1
2
3
4
5
6
7
8
9
10 // 2.消费消息
msgs, err := r.channel.Consume(
queue.Name, // 队列名称
"", // 用来区分多个消费者
false, // 是否自动应答
false, // 是否独有
false, // 设置为true,表示不能将同一个Connection中生产者发送的消息传递给这个Connection中的消费者
false, // 队列是否阻塞
nil, // 额外的属性
)
- autoAck选择为false,表示手动应答
1
2
3
4
5
6
7
8
9
10 // 3.启用协程处理消息
forever := make(chan bool) // 开个channel阻塞住,让开启的协程能一直跑着
go func() {
for delivery := range msgs {
// 消息逻辑处理,可以自行设计逻辑
fmt.Println("Received a message:", string(delivery.Body))
...
delivery.Ack(false)
}
}()
- autoAck选择为false,这里一定要设置
delivery.Ack(false)
!如果为true表示确认所有未确认的消息;为false则表示确认当前消息。
1
2
3
4
5
6 // 消费者流量控制
r.channel.Qos(
1, // 当前消费者一次能接受的最大消息数量
0, // 服务器传递的最大容量(以八位字节为单位)
false, // 如果设置为true 对全局channel可用
)
- 别忘了消费者流控,防止暴库。
生产者部分代码:
front/
下的main.go,指定了生产者产生的消息publish到sec-kill
队列中:
1 | rabbitmq := simple.NewRabbitMQSimple("sec-kill") |
在front/web/controller/
下的product.go
中,修改GetOrder
的下订单逻辑:
1 | // GetOrder 下订单 |
消费者部分代码:
在根目录下新建consumer.go
:
1 | package main |
实现效果
先把front/
下的main.go
跑起来(消息的生产者),然后访问http://127.0.0.1:8082/product/order?productID=2
,多刷新几次(消息的积累),然后运行根目录下的consumer.go
。会发现消息队列中挤压的消息被消费了,而且数据库中的产品数量productNum也相应减少了。
安全优化
- 前端页面限流
- 服务端防止for循环请求
- 其他安全建议
- 添加图像验证
- 添加自定义限流,如漏桶策略和令牌桶策略(Gin限流 笔记)
- 添加隐藏秒杀接口地址
心得
iris框架用来,感觉还是没有gin舒服。gin还是更符合go的风格,更贴近go的哲学。这次使用iris主要还是想开开眼界。
项目主要理解秒杀逻辑,对其他接口就比较“不拘小节”。