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

This article explores the architecture and implementation of a high‑concurrency ticket‑spike system inspired by China’s 12306 platform, covering load‑balancing strategies, Nginx weighted round‑robin, local and remote inventory deduction, Go concurrency patterns, Redis atomic operations, and performance testing results.

ITFLY8 Architecture Home
ITFLY8 Architecture Home
ITFLY8 Architecture Home
How to Build a Million-User Ticket Spike System with Go, Nginx, and Redis

12306 Ticket Spike: Lessons from Extreme Concurrency

During holidays, millions of users compete for train tickets, creating a massive QPS that challenges any flash‑sale system. By analyzing the 12306 backend architecture, we illustrate how to design a service that can handle 1 million concurrent users buying 10 000 tickets while remaining stable.

1. Large‑Scale High‑Concurrency Architecture

High‑concurrency systems typically use distributed clusters with multiple load‑balancing layers and disaster‑recovery mechanisms (dual data centers, node fault tolerance, server backup) to ensure high availability. Traffic is balanced across servers according to capacity and configuration.

1.1 Load‑Balancing Overview

The request flow passes through three layers of load balancers:

OSPF (Open Shortest Path First) is an interior gateway protocol that builds a link‑state database and computes shortest‑path trees; it can perform load balancing across equal‑cost paths.

LVS (Linux Virtual Server) provides IP‑level load balancing and hides server failures, forming a high‑performance virtual server.

Nginx, a high‑performance HTTP reverse proxy, supports round‑robin, weighted round‑robin, and IP‑hash load balancing. The example below focuses on weighted round‑robin.

1.2 Nginx Weighted Round‑Robin Demo

The upstream module implements weighted round‑robin. The following configuration assigns weights 1‑4 to four backend services listening on ports 3001‑3004.

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;
    }
}

A simple Go program runs four HTTP services on ports 3001‑3004 to handle ticket‑purchase requests.

package main

import (
    "net/http"
    "os"
    "strings"
)

func main() {
    http.HandleFunc("/buy/ticket", handleReq)
    http.ListenAndServe(":3001", nil)
}

func handleReq(w http.ResponseWriter, r *http.Request) {
    failedMsg := "handle in port:"
    writeLog(failedMsg, "./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, "
"}, "3001")
    buf := []byte(content)
    fd.Write(buf)
}

Using ab for load testing shows that each port receives requests proportional to its weight, confirming the effectiveness of weighted load balancing.

2. Spike System Design Choices

To keep the ticket‑spike service stable under extreme load, we must prevent overselling and underselling while handling the three core stages: order creation, inventory deduction, and payment.

2.1 Order‑Then‑Deduct Inventory

Creating an order first and then deducting inventory guarantees no oversell but incurs heavy DB I/O and risks undersell if users abandon payment.

2.2 Pay‑Then‑Deduct Inventory

Deducting after payment avoids undersell but can cause oversell because many orders may be created before inventory runs out, and it still involves heavy DB I/O.

2.3 Pre‑Deduct Inventory (Best Choice)

Pre‑deducting inventory ensures no oversell; order creation is asynchronous via a message queue (e.g., Kafka). If a user does not pay within a timeout (e.g., five minutes), the order expires and the inventory is returned.

3. The Art of Inventory Deduction

Pre‑deduction is implemented by allocating a portion of inventory to each machine’s memory, reducing the need for frequent DB writes. A cluster of 100 machines each holds 100 tickets, totaling 10 000 tickets.

To handle node failures, each machine also reserves a “buffer” inventory. A centralized Redis store maintains the global inventory; local deductions succeed first, then a remote Redis deduction confirms the transaction. If a node crashes, its buffer inventory can be reclaimed by other nodes.

Redis, with its single‑threaded nature, can handle up to 100 k QPS. The buffer size must be tuned to balance fault tolerance against Redis load.

4. Code Demonstration (Go)

4.1 Initialization

The init function sets local inventory, Redis connection pool, and a channel used as a lightweight distributed lock.

package localSpike

type LocalSpike struct {
    LocalInStock     int64
    LocalSalesVolume int64
}

package remoteSpike

type RemoteSpikeKeys struct {
    SpikeOrderHashKey string // Redis hash key for orders
    TotalInventoryKey string // Total tickets key
    QuantityOfOrderKey string // Sold tickets key
}

func NewPool() *redis.Pool {
    return &redis.Pool{MaxIdle: 10000, MaxActive: 12000, Dial: func() (redis.Conn, error) {
        return redis.Dial("tcp", ":6379")
    }}
}

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
}

4.2 Local and Remote Deduction

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 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
}

4.3 HTTP Service

package main

func main() {
    http.HandleFunc("/buy/ticket", handleReq)
    http.ListenAndServe(":3005", nil)
}

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

4.4 Performance Test

Using ApacheBench ( ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket) the single‑machine service handled over 4 000 requests per second, with latency around 23 ms, demonstrating that the design scales well when replicated across many nodes.

Requests per second:    4275.96 [#/sec] (mean)
Time per request:       23.387 [ms] (mean)
...

5. Summary

The spike system combines load‑balancing, local memory inventory, Redis atomic deduction, and Go’s native concurrency to avoid database bottlenecks, prevent oversell/undersell, and tolerate node failures. Key takeaways are effective load distribution, leveraging asynchronous processing, and maximizing CPU utilization with goroutines.

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.

redisGoticketing system
ITFLY8 Architecture Home
Written by

ITFLY8 Architecture Home

ITFLY8 Architecture Home - focused on architecture knowledge sharing and exchange, covering project management and product design. Includes large-scale distributed website architecture (high performance, high availability, caching, message queues...), design patterns, architecture patterns, big data, project management (SCRUM, PMP, Prince2), product design, and more.

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.