How to Build a Million‑User Ticket Spike System with Load Balancing, Redis, and Go
This article explores the architecture and implementation of a high‑concurrency ticket‑spike system, detailing load‑balancing strategies, in‑memory stock deduction, Redis atomic operations, and Go code examples that enable millions of users to purchase tickets simultaneously without overselling or underselling.
Ticket Spike: Lessons from Extreme Concurrency
During holidays, millions of users compete for train tickets, causing massive spikes that the 12306 service handles with over a million QPS. This article analyzes its backend architecture and demonstrates a prototype that can serve 1 million users buying 10 000 tickets simultaneously.
1. High‑Concurrency System Architecture
Large‑scale systems use distributed clusters, multi‑level load balancing, and disaster‑recovery mechanisms such as dual data centers and node failover to ensure high availability. The diagram below illustrates a simple setup.
1.1 Load‑Balancing Overview
The three layers of load balancing are described: OSPF, LVS, and Nginx weighted round‑robin.
OSPF is 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) provides IP load balancing and hides server failures, forming a high‑performance virtual server.
Nginx offers high‑performance HTTP reverse proxy with round‑robin, weighted round‑robin, and IP‑hash methods; the article focuses on weighted round‑robin configuration and testing.
1.2 Nginx Weighted Round‑Robin Demo
Configuration using the upstream module assigns weights 1‑4 to ports 3001‑3004.
# 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 programs listen on the four ports; the example for port 3001 is shown.
package main
import (
"net/http"
"os"
"strings"
)
func main() {
http.HandleFunc("/buy/ticket", handleReq)
http.ListenAndServe(":3001", nil)
}
// handle request and write log
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, "
"}, "")
fd.Write([]byte(content))
}Using ApacheBench (ab) the four ports receive 100, 200, 300, and 400 requests respectively, matching the configured weights.
2. Spike System Design Choices
To prevent overselling and underselling, three order‑processing strategies are examined: create‑order‑then‑deduct, pay‑then‑deduct, and pre‑deduct inventory.
2.1 Create‑Order‑Then‑Deduct
Creates order first, then reduces stock. Guarantees no oversell but incurs heavy DB I/O and risks undersell if users abandon orders.
2.2 Pay‑Then‑Deduct
Deducts stock after payment, avoiding undersell but can cause oversell under extreme concurrency and still involves DB I/O.
2.3 Pre‑Deduct Inventory
Pre‑deducts stock in Redis, creates orders asynchronously via MQ/Kafka, and uses order expiration to release unused tickets, balancing performance and correctness.
3. The Art of Stock Deduction
Local in‑memory stock deduction combined with remote Redis deduction eliminates frequent DB writes. A cluster of 100 machines each holding 100 tickets can serve 10 000 tickets total while tolerating node failures using buffered “extra” tickets.
Redis stores total inventory and sold count; Lua scripts ensure atomic updates.
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
`4. Code Demonstration
Initialization sets local stock, Redis keys, and a channel‑based lock. The main handler checks both local and remote deductions before responding.
func (spike *LocalSpike) LocalDeductionStock() bool {
spike.LocalSalesVolume++
return spike.LocalSalesVolume < spike.LocalInStock
}
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
}Benchmark with ab shows ~4 300 requests per second on a single low‑end Mac, and logs confirm uniform traffic distribution.
5. Summary
The prototype demonstrates how load balancing, in‑memory stock management, and Redis atomic operations can build a high‑performance spike system that avoids oversell, undersell, and tolerates partial node failures.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
MaGe Linux Operations
Founded in 2009, MaGe Education is a top Chinese high‑end IT training brand. Its graduates earn 12K+ RMB salaries, and the school has trained tens of thousands of students. It offers high‑pay courses in Linux cloud operations, Python full‑stack, automation, data analysis, AI, and Go high‑concurrency architecture. Thanks to quality courses and a solid reputation, it has talent partnerships with numerous internet firms.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
