千万级别高并发"秒杀"架构设计
1、大型高并发系统架构
1.1 负载均衡简介
1.2 Nginx加权轮询的演示
#配置负载均衡
upstream load_rule {
server 127.0.0.1:3001 weight=1;
server 127.0.0.1:3002 weight=2;
server 127.0.0.1:3003 weight=3;
server 127.0.0.1:3004 weight=4;
}
...
server {
listen 80;
server_name load_balance.com www.load_balance.com;
location / {
proxy_pass http://load_rule;
}
}
package main
import(
"net/http"
"os"
"strings"
)
func main() {
http.HandleFunc("/buy/ticket", handleReq)
http.ListenAndServe(":3001", nil)
}
//处理请求函数,根据请求将响应结果信息写入日志
func handleReq(w http.ResponseWriter, r *http.Request) {
failedMsg := "handle in port:"
writeLog(failedMsg, "./stat.log")
}
//写入日志
func writeLog(msg string, logPath string) {
fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
defer fd.Close()
content := strings.Join([]string{msg, "\r\n"}, "3001")
buf := []byte(content)
fd.Write(buf)
}
ab -n 1000-c 100 http://www.load_balance.com/buy/ticket
2、秒杀抢购系统选型
2.1 下单减库存
2.2 支付减库存
2.3 预扣库存
3、扣库存的艺术
4、代码演示
4.1 初始化工作
...
//localSpike包结构体定义
package localSpike
type LocalSpikestruct{
LocalInStock int64
LocalSalesVolume int64
}
...
//remoteSpike对hash结构的定义和redis连接池
package remoteSpike
//远程订单存储健值
type RemoteSpikeKeysstruct{
SpikeOrderHashKeystring//redis中秒杀订单hash结构key
TotalInventoryKeystring//hash结构中总订单库存key
QuantityOfOrderKeystring//hash结构中已有订单数量key
}
//初始化redis连接池
func NewPool() *redis.Pool{
return&redis.Pool{
MaxIdle: 10000,
MaxActive: 12000, // max number of connections
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", ":6379")
if err != nil{
panic(err.Error())
}
return c, err
},
}
}
...
func init() {
localSpike = localSpike2.LocalSpike{
LocalInStock: 150,
LocalSalesVolume: 0,
}
remoteSpike = remoteSpike2.RemoteSpikeKeys{
SpikeOrderHashKey: "ticket_hash_key",
TotalInventoryKey: "ticket_total_nums",
QuantityOfOrderKey: "ticket_sold_nums",
}
redisPool = remoteSpike2.NewPool()
done= make(chan int, 1)
done<- 1
}
4.2 本地扣库存和统一扣库存
package localSpike
//本地扣库存,返回bool值
func (spike *LocalSpike) LocalDeductionStock() bool{
spike.LocalSalesVolume= spike.LocalSalesVolume+ 1
return spike.LocalSalesVolume< spike.LocalInStock
}
package remoteSpike
......
constLuaScript= `
local ticket_key = KEYS[1]
local ticket_total_key = ARGV[1]
local ticket_sold_key = ARGV[2]
local ticket_total_nums = tonumber(redis.call('HGET', ticket_key, ticket_total_key))
local ticket_sold_nums = tonumber(redis.call('HGET', ticket_key, ticket_sold_key))
-- 查看是否还有余票,增加订单数量,返回结果值
if(ticket_total_nums >= ticket_sold_nums) then
return redis.call('HINCRBY', ticket_key, ticket_sold_key, 1)
end
return 0
`
//远端统一扣库存
func (RemoteSpikeKeys*RemoteSpikeKeys) RemoteDeductionStock(conn redis.Conn) bool{
lua := redis.NewScript(1, LuaScript)
result, err := redis.Int(lua.Do(conn, RemoteSpikeKeys.SpikeOrderHashKey, RemoteSpikeKeys.TotalInventoryKey, RemoteSpikeKeys.QuantityOfOrderKey))
if err != nil{
returnfalse
}
return result != 0
}
hmset ticket_hash_key "ticket_total_nums"10000"ticket_sold_nums"0
4.3 响应用户信息
package main
...
func main() {
http.HandleFunc("/buy/ticket", handleReq)
http.ListenAndServe(":3005", nil)
}
package main
//处理请求函数,根据请求将响应结果信息写入日志
func handleReq(w http.ResponseWriter, r *http.Request) {
redisConn := redisPool.Get()
LogMsg:= ""
<-done
//全局读写锁
if localSpike.LocalDeductionStock() && remoteSpike.RemoteDeductionStock(redisConn) {
util.RespJson(w, 1, "抢票成功", nil)
LogMsg= LogMsg+ "result:1,localSales:"+ strconv.FormatInt(localSpike.LocalSalesVolume, 10)
} else{
util.RespJson(w, -1, "已售罄", nil)
LogMsg= LogMsg+ "result:0,localSales:"+ strconv.FormatInt(localSpike.LocalSalesVolume, 10)
}
done<- 1
//将抢票状态写入到log中
writeLog(LogMsg, "./stat.log")
}
func writeLog(msg string, logPath string) {
fd, _ := os.OpenFile(logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
defer fd.Close()
content := strings.Join([]string{msg, "\r\n"}, "")
buf := []byte(content)
fd.Write(buf)
}
4.4 单机服务压测
ab -n 10000-c 100 http://127.0.0.1:3005/buy/ticket
ThisisApacheBench, Version2.3<$Revision: 1826891 $>
Copyright1996AdamTwiss, ZeusTechnologyLtd, http://www.zeustech.net/
Licensed to TheApacheSoftwareFoundation, http://www.apache.org/
Benchmarking127.0.0.1(be patient)
Completed1000 requests
Completed2000 requests
Completed3000 requests
Completed4000 requests
Completed5000 requests
Completed6000 requests
Completed7000 requests
Completed8000 requests
Completed9000 requests
Completed10000 requests
Finished10000 requests
ServerSoftware:
ServerHostname: 127.0.0.1
ServerPort: 3005
DocumentPath: /buy/ticket
DocumentLength: 29 bytes
ConcurrencyLevel: 100
Time taken for tests: 2.339 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 1370000 bytes
HTML transferred: 290000 bytes
Requests per second: 4275.96[#/sec] (mean)
Time per request: 23.387[ms] (mean)
Time per request: 0.234[ms] (mean, across all concurrent requests)
Transfer rate: 572.08[Kbytes/sec] received
ConnectionTimes(ms)
min mean[+/-sd] median max
Connect: 0814.76223
Processing: 21517.611232
Waiting: 11113.58225
Total: 72322.818239
Percentage of the requests served within a certain time (ms)
50% 18
66% 24
75% 26
80% 28
90% 33
95% 39
98% 45
99% 54
100% 239(longest request)
//stat.log
...
result:1,localSales:145
result:1,localSales:146
result:1,localSales:147
result:1,localSales:148
result:1,localSales:149
result:1,localSales:150
result:0,localSales:151
result:0,localSales:152
result:0,localSales:153
result:0,localSales:154
result:0,localSales:156
...
5、总结回顾
来源:http://suo.im/4qmhop
作者:绘你一世倾城@猎豹移动
文章不错?点个【在看】吧! 👇