How to Build a Million‑User Ticket Spike System with Nginx Load Balancing and Redis
This article explores the design of a high‑concurrency ticket‑spike system, covering multi‑layer load balancing, weighted Nginx routing, pre‑deduction inventory strategies using Redis, Go implementation details, and performance testing that demonstrates handling millions of requests while preventing oversell and ensuring high availability.
Introduction
During holidays, people rush to buy train tickets, creating extreme concurrency that can cause the ticket‑selling service to become unavailable within seconds.
High‑Concurrency Architecture
The 12306 service endures the highest QPS of any flash‑sale system, handling millions of simultaneous requests. A typical solution distributes traffic across a distributed cluster, adds multiple layers of load balancing, and provides various disaster‑recovery mechanisms such as dual data centers and node failover.
Load‑Balancing Methods
Three common Nginx load‑balancing strategies are used:
Round‑Robin
Weighted Round‑Robin
IP‑Hash Round‑Robin
Weighted round‑robin is demonstrated with a concrete Nginx configuration that assigns different weights to four backend ports (1, 2, 3, 4).
Ticket‑Spike System Design
Three basic stages are required for a ticket‑spike system: order creation, inventory deduction, and user payment. Directly performing these steps in a single transaction leads to heavy database I/O and risks over‑selling or under‑selling tickets, especially under extreme concurrency.
Pre‑Deduction (Buffer) Strategy
To avoid database bottlenecks, the system first reserves inventory locally in memory (pre‑deduction). If the local stock is sufficient, the request proceeds; otherwise it fails immediately. After a successful local deduction, a remote Redis hash is updated atomically using a Lua script to guarantee that total sales never exceed total inventory.
Implementation in Go
package main
import (
"net/http"
"os"
"strings"
)
func main() {
http.HandleFunc("/buy/ticket", handleReq)
http.ListenAndServe(":3001", nil)
}
func handleReq(w http.ResponseWriter, r *http.Request) {
// request handling logic
}
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 Go program initializes local stock, connects to a Redis pool (using Redigo), defines a Lua script for atomic remote deduction, and runs an HTTP server that processes ticket‑buy requests. A channel of size 1 acts as a lightweight distributed lock to serialize access to shared counters.
Performance Test
Using ApacheBench (ab) with 10 000 total requests and 100 concurrent connections, the service achieved approximately 4 300 requests per second, an average latency of 23 ms, and a uniform distribution of traffic across the weighted backends (weights 1‑4). The log file confirmed correct stock accounting.
ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticketTakeaways
Distribute traffic with multi‑layer load balancing to reduce per‑node load.
Use in‑memory pre‑deduction and Redis atomic operations to avoid costly database I/O.
Leverage Go’s native concurrency (goroutines, channels) for high‑throughput request handling.
Design buffer stock in Redis to tolerate node failures without causing under‑selling.
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.
21CTO
21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.
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.
