Designing a High‑Concurrency Ticket Spike System: Architecture, Load Balancing, and Go Implementation
This article explores the design of a high‑concurrency train‑ticket flash‑sale system, covering distributed load‑balancing architectures, OSPF/LVS/Nginx strategies, pre‑deduction inventory techniques, Go‑based HTTP services, Redis Lua scripts, and performance testing with AB, demonstrating how to achieve stable, scalable ticket‑purchasing under massive traffic.
During holidays, millions of users in China compete for train tickets, creating a scenario where the 12306 service must handle QPS levels beyond any typical flash‑sale system.
The author studies 12306’s backend architecture and proposes a simulated example that can serve 1 million users buying 10 000 tickets while maintaining stability.
Large‑Scale High‑Concurrency System Architecture
Distributed clusters with multiple layers of load balancers (OSPF, LVS, Nginx) provide high availability and evenly distribute traffic across servers.
OSPF (Open Shortest Path First) is an interior gateway protocol that builds a link‑state database and computes shortest‑path trees, allowing up to six equal‑cost paths for load balancing.
LVS (Linux Virtual Server) uses IP load‑balancing and content‑based request distribution to create a virtual high‑performance server pool.
Nginx offers three load‑balancing methods: round‑robin, weighted round‑robin, and IP‑hash round‑robin.
Nginx Weighted Round‑Robin Configuration
#配置负载均衡
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;
}
}AB testing shows request distribution matches the configured weights (100, 200, 300, 400 requests respectively).
Spike System Design Choices
The ticket‑spike process involves three stages: order creation, inventory deduction, and payment. Different ordering of these stages leads to trade‑offs between database I/O pressure and risk of overselling or underselling.
Three strategies are discussed:
Order‑then‑deduct (creates DB writes for every request, high I/O).
Pay‑then‑deduct (prevents underselling but can cause overselling under high concurrency).
Pre‑deduction (reserve inventory locally, then asynchronously create orders).
Pre‑deduction avoids frequent DB writes by keeping a local stock counter and only interacting with Redis for a global inventory check.
Local Stock Deduction Code (Go)
package localSpike
//本地扣库存,返回bool值
func (spike *LocalSpike) LocalDeductionStock() bool {
spike.LocalSalesVolume = spike.LocalSalesVolume + 1
return spike.LocalSalesVolume < spike.LocalInStock
}Remote Stock Deduction with Redis Lua Script
package remoteSpike
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
}Redis is initialized with a hash storing total tickets (e.g., 10 000) and sold count (0).
hmset ticket_hash_key "ticket_total_nums" 10000 "ticket_sold_nums" 0HTTP Service and Request Handling (Go)
package main
func main() {
http.HandleFunc("/buy/ticket", handleReq)
http.ListenAndServe(":3005", nil)
}
func handleReq(w http.ResponseWriter, r *http.Request) {
redisConn := redisPool.Get()
<-done // channel as distributed lock
if localSpike.LocalDeductionStock() && remoteSpike.RemoteDeductionStock(redisConn) {
util.RespJson(w, 1, "抢票成功", nil)
LogMsg = "result:1,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
} else {
util.RespJson(w, -1, "已售罄", nil)
LogMsg = "result:0,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
}
done <- 1
writeLog(LogMsg, "./stat.log")
}Channel‑based locking ensures atomicity between local and remote deductions.
Performance Testing
Using ApacheBench (ab) with 10 000 requests and concurrency 100, the single‑node service processes over 4 000 requests per second, with average latency ~23 ms.
ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticketLog output shows successful deductions until local stock is exhausted, then “已售罄”.
Conclusion
The article demonstrates a practical high‑concurrency spike system that avoids database bottlenecks by combining local in‑memory stock, Redis‑based global inventory, weighted load balancing, and Go’s lightweight concurrency, achieving stable performance and fault tolerance.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Big Data Technology Architecture
Exploring Open Source Big Data and AI Technologies
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
