How to Build a Million‑User Ticket Spike System: Insights from 12306

During holidays, millions of users scramble for train tickets, overwhelming the 12306 system; this article dissects its high‑concurrency architecture, explains load‑balancing layers, demonstrates Nginx weighted round‑robin configuration, and provides a Go‑based spike simulation with Redis stock management and performance results.

Liangxu Linux
Liangxu Linux
Liangxu Linux
How to Build a Million‑User Ticket Spike System: Insights from 12306

Problem Statement

Every Chinese holiday, billions of people try to buy train tickets the moment they are released, creating a classic "flash‑sale" scenario where the ticket‑selling service must handle extreme QPS (queries per second). The author investigates how the 12306 platform sustains such load and shares a simulated implementation that can serve one million users buying ten thousand tickets simultaneously.

Load‑Balancing Overview

The high‑concurrency architecture relies on multiple layers of load balancing to distribute traffic across a distributed cluster. Three common techniques are introduced:

OSPF (Open Shortest Path First) – an interior gateway protocol that builds a link‑state database and computes shortest‑path trees, allowing up to six equal‑cost paths for load distribution.

LVS (Linux Virtual Server) – an IP‑level load‑balancing cluster that forwards requests to a pool of real servers.

Nginx – a high‑performance HTTP reverse proxy that supports round‑robin, weighted round‑robin, and IP‑hash scheduling.

The article focuses on Nginx weighted round‑robin because it lets each backend server receive traffic proportional to its assigned weight.

Nginx Weighted Round‑Robin Configuration

# Configure load balancing
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;
    }
}

Four local HTTP services listen on ports 3001‑3004 with weights 1‑4 respectively. Requests are distributed according to these weights, which the author verifies with ab testing.

Architecture Design for Ticket Spike

The core challenge is to prevent overselling while handling massive concurrency. Three ordering strategies are compared:

Order‑then‑deduct‑stock – create an order first, then reduce inventory. Simple but incurs heavy DB I/O and risks malicious users creating unpaid orders.

Pay‑then‑deduct‑stock – wait for payment before reducing stock. This avoids overselling but blocks the critical path and still suffers from DB contention.

Pre‑deduct‑stock (reserve inventory) – allocate stock in memory before order creation, then asynchronously generate the order. This reduces DB load and speeds up user response.

The final design combines local in‑memory stock with a remote Redis hash that stores total inventory and sold count. Each server keeps a small buffer of tickets to tolerate node failures.

When a request arrives, the server first checks its local stock. If available, it attempts an atomic decrement in Redis via a Lua script. Only when both local and remote deductions succeed does the service return a successful purchase response.

Redis Atomic Decrement (Lua Script)

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

The script guarantees that stock deduction is performed atomically, preventing overselling even under extreme concurrency.

Go Implementation

The author provides a minimal Go service that demonstrates the entire flow:

package main
import (
    "net/http"
    "os"
    "strings"
)
func main() {
    http.HandleFunc("/buy/ticket", handleReq)
    http.ListenAndServe(":3005", nil)
}
func handleReq(w http.ResponseWriter, r *http.Request) {
    // acquire distributed lock via channel (size 1)
    <-done
    if localSpike.LocalDeductionStock() && remoteSpike.RemoteDeductionStock(redisPool.Get()) {
        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")
}
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))
}

Key components include:

Local stock struct LocalSpike with LocalDeductionStock() that increments a sales counter and checks against the in‑memory limit.

Remote stock struct RemoteSpikeKeys that wraps the Redis hash keys and executes the Lua script.

A channel done of size 1 acting as a lightweight distributed lock, ensuring the critical section runs sequentially without heavy mutexes.

Performance Test

The service is stress‑tested with ApacheBench:

ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket

On a modest Mac, the test yields ~4,300 requests per second, average latency ~23 ms, and zero failures. Log output shows successful deductions up to the configured local stock (150 tickets) followed by “已售罄” responses, confirming the system correctly stops sales when inventory is exhausted.

Conclusion

The article demonstrates that a high‑traffic flash‑sale system can avoid costly database I/O by combining:

Three‑tier load balancing (OSPF/LVS/Nginx) to spread traffic across many machines.

Local in‑memory stock for ultra‑fast checks.

Redis as a centralized, atomic stock manager using Lua scripts.

Graceful degradation via buffer stock to tolerate node failures.

These techniques together enable a ticket‑spike service to handle millions of concurrent users while guaranteeing no overselling and minimal latency.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

load balancingredisGohigh concurrencyNGINXticket spike
Liangxu Linux
Written by

Liangxu Linux

Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.