How to Serve 1 Million Users Buying 10 000 Train Tickets Simultaneously – A High‑Concurrency Architecture Walkthrough
This article analyzes the extreme‑traffic problem of Chinese train ticket sales, presents a multi‑layer load‑balancing architecture with Nginx weighted round‑robin, demonstrates Go and Redis code for local and global stock deduction, and shows benchmark results proving that a single machine can handle over 4 000 requests per second while preventing oversell and few‑sell.
Background
During holidays, millions of users compete for train tickets on the 12306 platform, causing extreme QPS and concurrency. The article analyzes the backend architecture and demonstrates a prototype that can serve 1 million users buying 10 000 tickets.
Large‑Scale Concurrency Architecture
Typical high‑concurrency systems use distributed clusters, multiple layers of load balancers, and disaster‑recovery mechanisms such as dual data centers and node failover to ensure high availability.
Three common load‑balancing techniques are introduced:
OSPF – an interior gateway protocol that builds a link‑state database and can perform equal‑cost multipath routing.
LVS – Linux Virtual Server, an IP‑level load balancer that distributes requests across a pool of servers.
Nginx – a high‑performance HTTP reverse proxy; the article focuses on weighted round‑robin configuration.
Nginx weighted round‑robin demo
The upstream module is used to assign weights 1‑4 to four backend services listening on ports 3001‑3004.
# 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;
}
}Go service for local stock handling
A simple Go HTTP server listens on a port and logs each request.
package main
import (
"net/http"
"os"
"strings"
)
func main() {
http.HandleFunc("/buy/ticket", handleReq)
http.ListenAndServe(":3001", nil)
}
// writeLog writes a message to ./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, "
"}, "")
fd.Write([]byte(content))
}Seckill system design choices
The article compares three stock‑deduction strategies:
Order‑then‑deduct : create order first, then reduce inventory. Guarantees no oversell but incurs heavy DB I/O and suffers from “few‑sell” when users abandon payment.
Pay‑then‑deduct : deduct after payment. Avoids few‑sell but can cause oversell under extreme concurrency.
Pre‑deduct (reserve stock) : reserve inventory first, create order asynchronously, and set an expiration time for unpaid orders. This balances oversell and few‑sell risks.
Local and remote stock deduction
Each machine keeps a local stock counter (e.g., 150 tickets). When a request arrives, the local counter is decremented; if successful, a Redis Lua script atomically decrements the global stock.
package localSpike
func (spike *LocalSpike) LocalDeductionStock() bool {
spike.LocalSalesVolume++
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 total = tonumber(redis.call('HGET', ticket_key, ticket_total_key))
local sold = tonumber(redis.call('HGET', ticket_key, ticket_sold_key))
if total >= sold then
return redis.call('HINCRBY', ticket_key, ticket_sold_key, 1)
end
return 0
`Testing with ApacheBench
Using ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket the single‑machine prototype handled over 4 000 requests per second with zero failures. Log excerpts show the transition from successful sales to “sold out”.
This is ApacheBench, Version 2.3
...
Requests per second: 4275.96 [#/sec] (mean)
Time per request: 23.387 [ms] (mean)
...Conclusion
The prototype demonstrates how load balancing, weighted Nginx, local in‑memory stock, and a Redis‑backed global counter can achieve high throughput while preventing oversell and handling partial machine failures through buffered “extra” tickets.
Key takeaways are the effectiveness of dividing traffic via load balancers and leveraging Go’s native concurrency model together with asynchronous processing.
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.
IT Architects Alliance
Discussion and exchange on system, internet, large‑scale distributed, high‑availability, and high‑performance architectures, as well as big data, machine learning, AI, and architecture adjustments with internet technologies. Includes real‑world large‑scale architecture case studies. Open to architects who have ideas and enjoy sharing.
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.
