The Art of Building a High‑Concurrency Flash‑Sale System

This article dissects the architecture of a massive flash‑sale service like 12306, covering multi‑layer load balancing, Nginx weighted round‑robin, stock‑deduction strategies, a Go‑based implementation with Redis and Lua, and performance results that demonstrate handling millions of concurrent ticket requests.

Architect's Guide
Architect's Guide
Architect's Guide
The Art of Building a High‑Concurrency Flash‑Sale System

The author analyzes the extreme concurrency challenges of a ticket‑flash‑sale system (e.g., 12306) and shares a complete learning case, including code simulation, load‑balancing design, stock‑deduction logic, a Go implementation, and benchmark results.

High‑Concurrency Architecture Overview

Large‑scale high‑concurrency systems are typically deployed as distributed clusters with multiple layers of load balancers and various disaster‑recovery mechanisms (dual data centers, node fault‑tolerance, server backup) to ensure high availability. Traffic is evenly distributed to servers based on their capacity.

Load Balancing Overview

The three layers of load balancing illustrated in the diagram are:

OSPF (Open Shortest Path First) – an interior gateway protocol that builds a link‑state database, calculates shortest‑path trees, and assigns a Cost inversely proportional to interface bandwidth. Paths with equal Cost can perform load balancing across up to six links.

LVS (Linux Virtual Server) – a cluster technology that uses IP load balancing and content‑based request distribution. The scheduler provides high throughput, masks server failures, and presents a virtual high‑availability server.

Nginx – a high‑performance HTTP reverse‑proxy that implements three load‑balancing methods: round‑robin, weighted round‑robin, and IP‑hash.

Nginx Weighted Round‑Robin Demonstration

The weighted round‑robin is configured via the upstream module, assigning different weights to backend servers according to their performance.

#配置负载均衡
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 adding a virtual host entry for www.load_balance.com in /etc/hosts, the author used the ApacheBench (ab) tool to send 100 concurrent requests. The log showed request distribution of 100, 200, 300, and 400 to ports 3001‑3004, matching the configured weights and confirming even, random traffic distribution.

Flash‑Sale Stock‑Deduction Strategies

The core workflow of a ticket‑sale system includes three stages: order creation, inventory deduction, and user payment. The system must prevent overselling, underselling, and ensure that each sold ticket is paid for.

Order‑Then‑Deduct

Typical flow: create order → deduct inventory → wait for payment. This guarantees no oversell because inventory is reduced atomically with order creation. However, it suffers from heavy DB I/O under extreme concurrency and is vulnerable to malicious orders that never pay.

Pay‑Then‑Deduct

Deduct inventory only after payment. While it avoids underselling, it can cause oversell when many orders are created before inventory runs out, and it still incurs high DB I/O.

Pre‑Deduction (Reserve Stock)

Reserve inventory first (pre‑deduction) to guarantee no oversell, then create orders asynchronously via a message queue (e.g., MQ, Kafka). If a user does not pay within a timeout (e.g., five minutes), the reserved stock is released back to the pool.

Local vs. Remote Stock Deduction

In a single‑machine scenario, traditional DB‑backed deduction uses transactions, causing blocking I/O. The author proposes a local‑in‑memory deduction: each machine holds a portion of the total stock (e.g., 100 tickets per machine) and decrements it in memory, avoiding DB hits.

To handle machine failures, a remote unified stock stored in Redis is used. Each machine first deducts locally; if successful, it then atomically decrements the remote stock via a Lua script. Only when both deductions succeed does the system return a successful purchase response.

Buffer Stock Design

Each machine reserves a “buffer” of extra tickets. If a machine crashes, its buffer can be used by other machines, ensuring the system tolerates failures. The buffer size must balance fault tolerance against additional load on Redis.

Go Implementation

The author provides a complete Go implementation that demonstrates the entire flow.

Initialization

Local stock, remote Redis keys, and a connection pool are initialized. A channel of size 1 is 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 // Field for total tickets
    QuantityOfOrderKey string // Field for sold tickets
}

func NewPool() *redis.Pool {
    return &redis.Pool{
        MaxIdle:   10000,
        MaxActive: 12000,
        Dial: func() (redis.Conn, error) {
            c, err := redis.Dial("tcp", ":6379")
            if err != nil { panic(err.Error()) }
            return c, err
        },
    }
}

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
}

Local Stock Deduction

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

The operation on LocalSalesVolume must be protected by a lock; the author uses the channel to serialize access.

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

Before starting the service, the total inventory is seeded in Redis:

hmset ticket_hash_key "ticket_total_nums" 10000 "ticket_sold_nums" 0

HTTP Server and Request Handling

package main

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

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

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

The channel ensures that the critical section (local and remote deduction) is executed sequentially, preventing race conditions.

Performance Test

The service is stress‑tested with ApacheBench:

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

On a low‑spec Mac, the benchmark produced:

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

Log analysis showed that the first 150 requests succeeded (matching the local stock of 150) and subsequent requests returned “已售罄”, confirming correct stock enforcement.

Conclusion

The article demonstrates that a flash‑sale system can achieve high throughput by combining multi‑layer load balancing, weighted Nginx routing, local in‑memory stock deduction, and a remote Redis stock guard with Lua‑based atomic operations. The design avoids heavy database I/O, tolerates node failures via buffer stock, and leverages Go’s native concurrency to fully utilize multi‑core servers.

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 balancingredisgohigh concurrencynginxflash sale
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

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.