How to Build a High‑Concurrency Ticket‑Spike System with Go, Nginx, and Redis
This article dissects the extreme concurrency challenges of China's 12306 ticket‑spike scenario, presents a layered load‑balancing architecture, compares order‑processing strategies, and provides a complete Go‑based prototype with Nginx weighted routing and Redis atomic stock deduction, complete with performance testing and key takeaways.
12306 Ticket Spike Challenge
During holidays, millions of users compete for train tickets on China’s 12306 platform, creating extreme QPS that can reach millions. This article analyzes the backend architecture and demonstrates a prototype that can handle 1 million concurrent users buying 10 000 tickets.
Large‑Scale High‑Concurrency Architecture
Typical high‑concurrency systems use distributed clusters, multiple layers of load balancers, and fault‑tolerance mechanisms such as dual data centers and node redundancy. The diagram below illustrates a simple architecture.
1.1 Load Balancing Overview
The three‑tier load‑balancing path includes OSPF, LVS, and Nginx. OSPF is an interior gateway protocol that builds a link‑state database and can perform equal‑cost multipath routing. LVS (Linux Virtual Server) provides IP‑level load balancing and failover. Nginx offers HTTP reverse‑proxy load balancing with round‑robin, weighted round‑robin, and IP‑hash strategies.
1.2 Nginx Weighted Round‑Robin Demo
Configure an upstream with different weights for four backend services listening on ports 3001‑3004.
# 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;
}
}2. Spike System Design Choices
Three typical order‑processing flows are examined: (1) create order then deduct inventory, (2) deduct after payment, and (3) pre‑deduct inventory and generate orders asynchronously. The third approach avoids heavy database I/O and reduces the risk of overselling or underselling.
2.1 Local Stock Deduction
Each server keeps a local stock counter in memory. When a request arrives, the counter is incremented; if it exceeds the local limit, the request fails.
func (spike *LocalSpike) LocalDeductionStock() bool {
spike.LocalSalesVolume++
return spike.LocalSalesVolume < spike.LocalInStock
}2.2 Remote Unified Stock Deduction
Redis stores the global stock hash. A Lua script atomically checks the remaining tickets and increments the sold count.
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
`2.3 Service Initialization
Initialize local stock, Redis keys, and a single‑capacity channel to act as a lightweight lock.
func init() {
localSpike = LocalSpike{LocalInStock: 150}
remoteSpike = RemoteSpikeKeys{
SpikeOrderHashKey: "ticket_hash_key",
TotalInventoryKey: "ticket_total_nums",
QuantityOfOrderKey: "ticket_sold_nums",
}
redisPool = NewPool()
done = make(chan int, 1)
done <- 1
}2.4 HTTP Handler
The handler obtains a Redis connection, performs local and remote deductions, returns JSON success or sold‑out messages, and logs the result.
func handleReq(w http.ResponseWriter, r *http.Request) {
redisConn := redisPool.Get()
<-done
if localSpike.LocalDeductionStock() && remoteSpike.RemoteDeductionStock(redisConn) {
RespJson(w, 1, "抢票成功", nil)
} else {
RespJson(w, -1, "已售罄", nil)
}
done <- 1
}3. Performance Test
Using ApacheBench (ab) with 10 000 requests and concurrency 100, the single‑machine prototype processes about 4 300 requests per second, with average latency around 23 ms. Log analysis confirms uniform request distribution and correct stock updates.
4. Takeaways
Load balancing distributes traffic and isolates failures.
Leveraging Go’s goroutine model and channel‑based locking yields efficient concurrent processing.
Pre‑deducting stock locally and synchronizing with Redis avoids costly database I/O while guaranteeing no oversell or undersell, even when some nodes fail.
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.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
