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.
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" 0HTTP 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/ticketOn 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.
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.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.
