How to Build a High‑Concurrency Ticket‑Spiking System Like 12306 with Go and Redis

This article dissects the architecture of a 12306‑style ticket‑spiking service, explaining load‑balancing strategies, weighted Nginx routing, local and remote stock deduction using Go and Redis, and demonstrates performance testing that achieves thousands of requests per second while preventing oversell and undersell.

21CTO
21CTO
21CTO
How to Build a High‑Concurrency Ticket‑Spiking System Like 12306 with Go and Redis

During holidays, millions of users scramble for train tickets, creating a massive spike in traffic that challenges any system's QPS limits. The author analyzes the 12306 service architecture and presents a simulated example that can handle 1 million concurrent users buying 10 000 tickets.

1. Large‑Scale High‑Concurrency Architecture

High‑concurrency systems are typically deployed as distributed clusters with multiple layers of load balancers and disaster‑recovery mechanisms (dual data centers, node fault tolerance, server backup) to ensure high availability. The following diagram illustrates a simple layout:

1.1 Load‑Balancing Overview

The three common load‑balancing methods are:

OSPF – an interior gateway protocol that builds a link‑state database and can perform load balancing across up to six equal‑cost paths.

LVS (Linux Virtual Server) – a cluster technology that distributes requests to multiple servers and masks server failures.

Nginx – a high‑performance HTTP reverse proxy that supports round‑robin, weighted round‑robin, and IP‑hash strategies. The article focuses on weighted round‑robin.

1.2 Nginx Weighted Round‑Robin Demo

The upstream module is used to assign weights to four backend ports (3001‑3004) with weights 1, 2, 3, 4 respectively.

# Configuration for 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;
    }
}

2. Seckill System Design Choices

The core workflow involves three stages: order creation, inventory deduction, and payment. Three strategies are compared:

Order‑then‑deduct : Create order first, then deduct inventory. Guarantees no oversell but incurs heavy DB I/O and risks undersell if users abandon orders.

Pay‑then‑deduct : Deduct after payment. Prevents undersell but can cause oversell under extreme concurrency.

Pre‑deduct (reserve stock) : Reserve inventory first, create order asynchronously, and release stock if payment times out. This balances performance and correctness.

3. Inventory Deduction Techniques

Local in‑memory stock is used to avoid frequent DB writes. Each server holds a portion of the total tickets; when local stock is exhausted, a buffered reserve from Redis compensates for failed servers.

3.1 Local Stock Deduction (Go)

package localSpike
// Local stock deduction, returns bool
func (spike *LocalSpike) LocalDeductionStock() bool {
    spike.LocalSalesVolume = spike.LocalSalesVolume + 1
    return spike.LocalSalesVolume < spike.LocalInStock
}

3.2 Remote Stock Deduction with 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 (RemoteSpikeKeys *RemoteSpikeKeys) RemoteDeductionStock(conn redis.Conn) bool {
    lua := redis.NewScript(1, LuaScript)
    result, err := redis.Int(lua.Do(conn, RemoteSpikeKeys.SpikeOrderHashKey, RemoteSpikeKeys.TotalInventoryKey, RemoteSpikeKeys.QuantityOfOrderKey))
    if err != nil {
        return false
    }
    return result != 0
}

4. Full Go Implementation

4.1 Initialization

package main
import (
    "net/http"
    "os"
    "strings"
)
func init() {
    // initialize local and remote structures, Redis pool, channel for lock
    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 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")
}

4.3 Logging

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

5. Performance Testing

Using ApacheBench (ab) with 10 000 requests and concurrency 100, the single‑machine service handled over 4 200 requests per second with zero failures. Log excerpts show successful and failed attempts as the local stock was exhausted.

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

... (benchmark output omitted for brevity) ...
Requests per second:    4275.96 [#/sec] (mean)
Time per request:       23.387 ms (mean)

6. Summary

The demonstrated system achieves high‑throughput ticket seckill by:

Layered load balancing to distribute traffic across many servers.

Local in‑memory stock deduction to avoid DB I/O.

Redis‑based remote stock deduction with Lua for atomicity.

Buffered “buffer stock” to tolerate server failures.

Go’s native concurrency model to efficiently handle thousands of simultaneous requests.

These techniques collectively prevent oversell and undersell while maintaining scalability and fault tolerance.

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.

BackendRedisGohigh-concurrencyload-balancing
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

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.