How to Build a Million‑User Ticket‑Spike System with Nginx, Go, and Redis
This article explains the design of a high‑concurrency train‑ticket flash‑sale system, covering distributed load‑balancing, Nginx weighted round‑robin, local and remote stock deduction, Go implementation, Redis atomic scripts, and performance testing with ApacheBench.
Background
During holidays, millions of users in major Chinese cities compete for train tickets, creating extreme spikes in traffic that challenge the 12306 ticketing service. The author studies the 12306 architecture and builds a simplified example that can handle 1 million concurrent users buying 10 000 tickets while keeping the service stable.
High‑Concurrency Architecture
Large‑scale systems use distributed clusters with multiple layers of load balancers and disaster‑recovery mechanisms (dual data centers, node fault‑tolerance) to ensure high availability. The traffic is evenly distributed across servers, but each server still faces very high QPS.
Load‑Balancing Overview
Three common load‑balancing methods are described:
Round Robin
Weighted Round Robin
IP Hash
The article demonstrates Nginx weighted round robin by assigning different weights to four local HTTP services (ports 3001‑3004) and verifying the request distribution with ApacheBench.
Nginx Weighted Round Robin Configuration
# upstream 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;
}
}After configuring the hosts file to map www.load_balance.com to the local machine, the author runs ab -n 1000 -c 100 http://www.load_balance.com/buy/ticket and observes request counts of 100, 200, 300, and 400 for the four ports, matching the configured weights.
Order Processing Flow
The ticket‑sale process consists of three stages: order creation, inventory deduction, and payment. The article evaluates three sequencing strategies and highlights the problems of high‑frequency DB writes and potential overselling when users create orders without paying.
Stock Deduction Strategies
Three approaches are compared:
Order‑then‑Deduct – creates an order first, then reduces stock; incurs heavy DB I/O.
Pay‑then‑Deduct – waits for payment before reducing stock; can cause overselling under extreme concurrency.
Pre‑Deduct (Reservation) – reserves stock first, creates the order asynchronously, and restores stock if payment does not occur within a timeout.
The pre‑deduct method is chosen because it avoids frequent DB I/O and prevents both overselling and underselling.
Local Stock Deduction
package localSpike
// Local stock deduction, returns bool
func (spike *LocalSpike) LocalDeductionStock() bool {
spike.LocalSalesVolume = spike.LocalSalesVolume + 1
return spike.LocalSalesVolume < spike.LocalInStock
}Each server holds a portion of the total inventory in memory, allowing fast stock checks without DB access.
Remote Stock Deduction with Redis
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 (r *RemoteSpikeKeys) RemoteDeductionStock(conn redis.Conn) bool {
lua := redis.NewScript(1, LuaScript)
result, err := redis.Int(lua.Do(conn, r.SpikeOrderHashKey, r.TotalInventoryKey, r.QuantityOfOrderKey))
if err != nil {
return false
}
return result != 0
}Redis stores the total inventory and sold count in a hash. The Lua script guarantees atomic check‑and‑increment, preventing race conditions.
Service Initialization
// Initialize local stock, Redis pool, and channel lock
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
}Request Handling
func handleReq(w http.ResponseWriter, r *http.Request) {
redisConn := redisPool.Get()
var logMsg string
<-done
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")
}The channel acts as a lightweight distributed lock, ensuring that stock deduction operations are executed sequentially.
Performance Test
Using ApacheBench ( ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket) on a low‑spec Mac, the service handled over 4 000 requests per second with zero failures. Log analysis confirmed that request distribution matched the weighted configuration and that Redis remained stable.
Conclusions
The prototype demonstrates that:
Layered load balancing distributes traffic to many machines, reducing per‑node load.
Pre‑deducting stock in memory and confirming it with Redis avoids costly DB I/O.
Using Go’s concurrency primitives (goroutine, channel) and Redis Lua scripts provides atomicity and high throughput.
Proper buffer stock on each node tolerates server failures without losing sales.
These techniques together enable a ticket‑spike system to support extreme concurrency while preventing overselling and underselling.
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.
ITPUB
Official ITPUB account sharing technical insights, community news, and exciting events.
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.
