轉載自公衆號【碼農突圍】
轉載自公衆號【碼農突圍】
12306 搶票,極限併發帶來的思考
大型高併發系統架構
負載均衡簡介
輪詢
加權輪詢
IP Hash 輪詢
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;
}
}
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)
}
秒殺搶購系統選型
下單減庫存
在極限併發情況下,任何一個內存操作的細節都至關影響性能,尤其像創建訂單這種邏輯,一般都需要存儲到磁盤數據庫的,對數據庫的壓力是可想而知的。
如果用戶存在惡意下單的情況,只下單不支付這樣庫存就會變少,會少賣很多訂單,雖然服務端可以限制 IP 和用戶的購買訂單數量,這也不算是一個好方法。
支付減庫存
預扣庫存
扣庫存的藝術
代碼演示
初始化工作
//localSpike包結構體定義
package localSpike
type LocalSpike struct {
LocalInStock int64
LocalSalesVolume int64
}
...
//remoteSpike對hash結構的定義和redis連接池
package remoteSpike
//遠程訂單存儲健值
type RemoteSpikeKeys struct {
SpikeOrderHashKey string //redis中秒殺訂單hash結構key
TotalInventoryKey string //hash結構中總訂單庫存key
QuantityOfOrderKey string //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
}
本地扣庫存和統一扣庫存
//本地扣庫存,返回bool值
func (spike *LocalSpike) LocalDeductionStock() bool{
spike.LocalSalesVolume = spike.LocalSalesVolume + 1
return spike.LocalSalesVolume < spike.LocalInStock
}
......
const LuaScript = `
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 {
return false
}
return result != 0
}
響應用戶信息
...
func main() {
http.HandleFunc( "/buy/ticket", handleReq)
http.ListenAndServe( ":3005", nil)
}
//處理請求函數,根據請求將響應結果信息寫入日誌
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)
}
單機服務壓測
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 127.0.0.1 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests
Server Software:
Server Hostname: 127.0.0.1
Server Port: 3005
Document Path: /buy/ticket
Document Length: 29 bytes
Concurrency Level: 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
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 8 14.7 6 223
Processing: 2 15 17.6 11 232
Waiting: 1 11 13.5 8 225
Total: 7 23 22.8 18 239
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)
...
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
...
總結回顧
本文分享自微信公衆號 - 架構真經(gentoo666)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。