How to Build a High‑Concurrency Ticket‑Snatching System Like 12306 with Nginx, Redis, and Go

This article dissects the extreme‑load challenges of China’s 12306 train‑ticket service, explains its multi‑layer load‑balancing architecture, and provides a complete Go‑based spike‑system example that uses Nginx weighted round‑robin, Redis atomic stock deduction, and channel‑based locking to safely handle millions of concurrent purchase requests.

ITPUB
ITPUB
ITPUB
How to Build a High‑Concurrency Ticket‑Snatching System Like 12306 with Nginx, Redis, and Go

Problem Background

During holidays, millions of users in China try to buy train tickets on the 12306 platform, causing the ticket‑selling service to face the highest possible QPS in any flash‑sale system. The goal is to keep the service stable when 1 000 000 users simultaneously attempt to purchase 10 000 tickets.

System Architecture Overview

The 12306 backend uses a distributed cluster with three layers of load balancing, high‑availability data centers, and massive concurrency handling. The article reproduces a simplified version of this architecture.

Load‑Balancing Layers

OSPF – an interior gateway protocol that builds a shortest‑path tree and can perform load distribution across up to six equal‑cost links.

LVS (Linux Virtual Server) – IP‑level load balancer that distributes requests among a pool of real servers and masks server failures.

Nginx – HTTP reverse proxy that provides three weighting strategies: round‑robin, weighted round‑robin, and IP‑hash. The article focuses on Nginx weighted 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;
    }
}

The configuration assigns different weights to four local services, which is later verified with an ab load test.

Order‑Processing Strategies

The article compares three typical sequences for a ticket‑sale transaction:

Order‑first, then stock deduction – simple but creates heavy DB I/O and risks over‑selling under extreme load.

Payment‑first, then stock deduction – avoids over‑selling but can cause “oversell” due to many unpaid orders and still incurs high DB pressure.

Pre‑deduction (reserve stock) – reserves inventory before order creation, then creates the order asynchronously. This reduces DB I/O and mitigates over‑sell.

Pre‑deduction is identified as the most suitable for high‑concurrency flash‑sale scenarios.

Local Stock Deduction (In‑Memory)

func (spike *LocalSpike) LocalDeductionStock() bool {
    spike.LocalSalesVolume = spike.LocalSalesVolume + 1
    return spike.LocalSalesVolume < spike.LocalInStock
}

Each server keeps a local stock counter; when the counter exceeds the allocated inventory, the request is rejected.

Remote Stock Deduction (Redis + Lua)

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
}

The Lua script guarantees atomic check‑and‑increment of the sold‑count in a Redis hash.

Service Initialization

Before handling requests, the program initializes:

Local stock (e.g., 150 tickets per instance).

Redis hash keys: ticket_hash_key with fields ticket_total_nums (total inventory) and ticket_sold_nums (sold count).

A Redis connection pool (max idle 10 000, max active 12 000).

A channel of size 1 to act as a distributed lock, ensuring that the critical section of local‑plus‑remote deduction runs sequentially.

Redis Initialization Command

hmset ticket_hash_key "ticket_total_nums" 10000 "ticket_sold_nums" 0

HTTP Request Handling

func handleReq(w http.ResponseWriter, r *http.Request) {
    redisConn := redisPool.Get()
    var logMsg string
    <-done // acquire 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 // release lock
    writeLog(logMsg, "./stat.log")
}

The handler first performs the local stock check, then the remote Redis deduction, and finally returns a JSON response indicating success or sold‑out.

Performance Test

Using ApacheBench:

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

The test on a low‑spec Mac achieved ~4 300 requests per second, with latency around 23 ms per request and zero failures. Log output confirmed that the first 150 requests succeeded (matching the local stock) and subsequent requests were correctly rejected.

Key Takeaways

Load balancing and sharding – distributing traffic across many servers reduces per‑node load and enables the system to handle massive QPS.

In‑memory stock + Redis atomic deduction – avoids frequent DB writes while guaranteeing consistency and preventing over‑sell.

Channel‑based locking in Go – a lightweight way to serialize critical sections without heavy mutexes.

Asynchronous order creation – orders are queued (e.g., via MQ) after stock reservation, improving response time.

Overall, the article provides a practical blueprint for building a high‑throughput, fault‑tolerant ticket‑snatching service that can scale to millions of concurrent users.

Load balancing diagram
Load balancing diagram
Pre‑deduction flow
Pre‑deduction flow
Cluster architecture
Cluster architecture
Buffer stock handling
Buffer stock handling
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.

RedisGohigh concurrencyNGINXticketing system
ITPUB
Written by

ITPUB

Official ITPUB account sharing technical insights, community news, and exciting events.

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.