How to Handle 1 Million Users Buying 10,000 Train Tickets Simultaneously

This article analyzes the architecture, load‑balancing strategies, stock‑deduction techniques, and Go/Redis implementation needed to support a million concurrent users purchasing ten thousand train tickets without overselling or underselling.

Architecture Digest
Architecture Digest
Architecture Digest
How to Handle 1 Million Users Buying 10,000 Train Tickets Simultaneously

1. Large‑Scale High‑Concurrency Architecture

To serve millions of users during peak travel periods, the system adopts a distributed cluster with multiple layers of load balancers and disaster‑recovery mechanisms such as dual data centers and node failover, ensuring high availability.

The traffic is evenly distributed across servers using three common load‑balancing methods:

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

LVS (Linux Virtual Server) – a cluster technology that uses IP load balancing and content‑based request distribution, automatically masking server failures.

Nginx – a high‑performance HTTP reverse proxy that supports round‑robin, weighted round‑robin, and IP‑hash algorithms. The article provides a weighted round‑robin configuration example.

2. Seckill System Design Choices

The core workflow of a ticket‑seckill system involves three stages: order creation, stock deduction, and user payment. Three ordering strategies are examined:

2.1 Order‑Then‑Deduct

Creating the order first and then deducting stock guarantees no oversell but incurs heavy database I/O and suffers from “no‑pay” abuse, where malicious users create orders without paying, causing stock loss.

2.2 Pay‑Then‑Deduct

Deducting stock only after payment avoids “no‑pay” loss but can lead to oversell under extreme concurrency because many orders may be created before stock reaches zero.

2.3 Pre‑Deduct (Reserve Stock)

Reserve stock is deducted immediately, guaranteeing no oversell, while order creation is performed asynchronously via a message queue (e.g., Kafka). Orders have a limited validity period (e.g., five minutes); expired orders release their reserved stock back to the pool.

3. Stock Deduction Techniques

Pre‑deduction is identified as the most reasonable approach. The article explores where to store stock and how to keep deduction fast and correct under high load.

In a single‑machine low‑concurrency scenario, stock deduction typically involves a database transaction, which is too slow for a seckill system.

To improve performance, the design moves stock to local memory ("local stock") and performs deduction there. After a successful local deduction, a remote unified stock stored in Redis is also decremented. If both succeed, the user receives a success response.

Redis is chosen for the unified stock because of its sub‑millisecond latency and ability to handle >100 k QPS. A buffer of extra tickets is reserved on each machine to tolerate node failures; the buffer size must be balanced against Redis load.

4. Go Code Demonstration

4.1 Initialization

The program initializes local stock, a Redis connection pool (using redigo), and a channel of size 1 that acts as a lightweight distributed lock.

#... (initialization snippet omitted for brevity) ...

4.2 Local and Remote Stock Deduction

Local deduction simply increments a counter and checks it against the in‑memory stock limit.

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

Remote deduction uses a Lua script executed atomically in Redis to compare total stock and sold count, then increment the sold count if stock remains.

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 (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.3 HTTP Handler

The HTTP endpoint "/buy/ticket" obtains a Redis connection, acquires the channel lock, performs both local and remote deductions, returns a JSON success or sold‑out message, logs the result, and releases the lock.

package main
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, "Ticket purchase successful", nil)
        LogMsg = "result:1,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
    } else {
        util.RespJson(w, -1, "Sold out", nil)
        LogMsg = "result:0,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
    }
    done <- 1
    writeLog(LogMsg, "./stat.log")
}

4.4 Load Testing

ApacheBench (ab) is used to simulate 10 000 requests with a concurrency level of 100. The test on a low‑spec Mac yields ~4 276 requests per second with an average latency of 23 ms and no failed requests.

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

5. Takeaways

The experiment demonstrates that a well‑designed load‑balancing strategy, in‑memory stock handling, asynchronous processing, and a lightweight Go concurrency model can dramatically reduce database I/O and achieve high QPS while preventing both oversell and undersell, even when some nodes fail.

Load balancing distributes traffic so each machine can operate at its peak.

Leveraging Go’s goroutine model and channel‑based locking enables efficient multi‑core utilization.

Redis provides a fast, atomic store for unified stock and can tolerate a reasonable buffer to mask node failures.

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.

BackendDistributed Systemsload balancingredisGohigh concurrencySeckill
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

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.