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.

ITPUB
ITPUB
ITPUB
How to Build a Million‑User Ticket‑Spike System with Nginx, Go, and Redis

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.

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.

RedisGoSystem Designhigh 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.