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.

Programmer DD
Programmer DD
Programmer DD
How to Build a High‑Concurrency Ticket‑Spike System with Go, Nginx, and Redis

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.

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 balancingGohigh concurrencyNGINXticket spike
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

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.