How to Serve 1 Million Users Buying 10 000 Train Tickets Simultaneously – A High‑Concurrency Architecture Walkthrough

This article analyzes the extreme‑traffic problem of Chinese train ticket sales, presents a multi‑layer load‑balancing architecture with Nginx weighted round‑robin, demonstrates Go and Redis code for local and global stock deduction, and shows benchmark results proving that a single machine can handle over 4 000 requests per second while preventing oversell and few‑sell.

IT Architects Alliance
IT Architects Alliance
IT Architects Alliance
How to Serve 1 Million Users Buying 10 000 Train Tickets Simultaneously – A High‑Concurrency Architecture Walkthrough

Background

During holidays, millions of users compete for train tickets on the 12306 platform, causing extreme QPS and concurrency. The article analyzes the backend architecture and demonstrates a prototype that can serve 1 million users buying 10 000 tickets.

Large‑Scale Concurrency Architecture

Typical high‑concurrency systems use distributed clusters, multiple layers of load balancers, and disaster‑recovery mechanisms such as dual data centers and node failover to ensure high availability.

Three common load‑balancing techniques are introduced:

OSPF – an interior gateway protocol that builds a link‑state database and can perform equal‑cost multipath routing.

LVS – Linux Virtual Server, an IP‑level load balancer that distributes requests across a pool of servers.

Nginx – a high‑performance HTTP reverse proxy; the article focuses on weighted round‑robin configuration.

Nginx weighted round‑robin demo

The upstream module is used to assign weights 1‑4 to four backend services listening on ports 3001‑3004.

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

Go service for local stock handling

A simple Go HTTP server listens on a port and logs each request.

package main

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

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

// writeLog writes a message to ./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))
}

Seckill system design choices

The article compares three stock‑deduction strategies:

Order‑then‑deduct : create order first, then reduce inventory. Guarantees no oversell but incurs heavy DB I/O and suffers from “few‑sell” when users abandon payment.

Pay‑then‑deduct : deduct after payment. Avoids few‑sell but can cause oversell under extreme concurrency.

Pre‑deduct (reserve stock) : reserve inventory first, create order asynchronously, and set an expiration time for unpaid orders. This balances oversell and few‑sell risks.

Local and remote stock deduction

Each machine keeps a local stock counter (e.g., 150 tickets). When a request arrives, the local counter is decremented; if successful, a Redis Lua script atomically decrements the global stock.

package localSpike

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 total = tonumber(redis.call('HGET', ticket_key, ticket_total_key))
local sold = tonumber(redis.call('HGET', ticket_key, ticket_sold_key))
if total >= sold then
    return redis.call('HINCRBY', ticket_key, ticket_sold_key, 1)
end
return 0
`

Testing with ApacheBench

Using ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket the single‑machine prototype handled over 4 000 requests per second with zero failures. Log excerpts show the transition from successful sales to “sold out”.

This is ApacheBench, Version 2.3
...
Requests per second:    4275.96 [#/sec] (mean)
Time per request:       23.387 [ms] (mean)
...

Conclusion

The prototype demonstrates how load balancing, weighted Nginx, local in‑memory stock, and a Redis‑backed global counter can achieve high throughput while preventing oversell and handling partial machine failures through buffered “extra” tickets.

Key takeaways are the effectiveness of dividing traffic via load balancers and leveraging Go’s native concurrency model together with asynchronous processing.

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.

Distributed SystemsBackend Developmentload balancingredisGohigh concurrencySeckill
IT Architects Alliance
Written by

IT Architects Alliance

Discussion and exchange on system, internet, large‑scale distributed, high‑availability, and high‑performance architectures, as well as big data, machine learning, AI, and architecture adjustments with internet technologies. Includes real‑world large‑scale architecture case studies. Open to architects who have ideas and enjoy sharing.

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.