Designing a High-Concurrency Ticket Spike System with Go, Nginx, and Redis
This article explains how to build a high‑concurrency ticket‑seckill system using Go, Nginx weighted load balancing, and Redis, covering architecture design, local and remote stock deduction, Lua scripting for atomic operations, and performance testing with ApacheBench to achieve thousands of requests per second without overselling.
During holiday travel peaks, millions of users compete for train tickets, creating a massive QPS challenge similar to a global flash‑sale system. The article examines the 12306 ticketing service architecture and demonstrates how to design a scalable spike system that can handle 1 million concurrent users buying 10 000 tickets.
1. Large‑Scale Architecture – A distributed cluster with multiple load‑balancing layers (OSPF, LVS, Nginx) spreads traffic across servers. A simple diagram illustrates the flow from user request to backend services.
1.1 Load Balancing Overview – Three common methods are described:
OSPF: internal gateway protocol that can perform equal‑cost multi‑path routing.
LVS: IP‑level load balancer that distributes connections across a server pool.
Nginx: HTTP reverse proxy supporting round‑robin, weighted round‑robin, and IP‑hash.
1.2 Nginx Weighted Round‑Robin Demo
# Configuration of weighted upstream
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;
}
}Requests are distributed according to the configured weights, which matches the observed request counts (100, 200, 300, 400) in the test.
2. Spike System Design Options
The article compares three ordering strategies:
Order‑then‑deduct : create an order first, then reduce inventory. Guarantees no oversell but incurs heavy DB I/O and risks “no‑pay” loss.
Pay‑then‑deduct : wait for payment before reducing stock. Prevents loss but can cause oversell under extreme concurrency.
Pre‑deduct (reserve) stock : reserve inventory first, generate orders asynchronously, and release reserved stock after a timeout. This reduces DB pressure and handles high QPS.
Pre‑deduct is chosen as the most practical solution.
3. Stock Deduction Techniques
Local in‑memory stock is used to avoid frequent DB writes. Each server holds a portion of the total inventory and updates a local counter:
func (spike *LocalSpike) LocalDeductionStock() bool {
spike.LocalSalesVolume = spike.LocalSalesVolume + 1
return spike.LocalSalesVolume < spike.LocalInStock
}If local stock is insufficient, a remote deduction is performed in Redis using an atomic Lua script:
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 0Redis stores the total inventory and sold count in a hash (e.g., ticket_hash_key), initialized with:
hmset ticket_hash_key "ticket_total_nums" 10000 "ticket_sold_nums" 04. Go Implementation
Initialization ( init()) sets local stock, Redis keys, and a channel used as a lightweight distributed lock:
func init() {
localSpike = LocalSpike{LocalInStock: 150, LocalSalesVolume: 0}
remoteSpike = RemoteSpikeKeys{SpikeOrderHashKey: "ticket_hash_key", TotalInventoryKey: "ticket_total_nums", QuantityOfOrderKey: "ticket_sold_nums"}
redisPool = remoteSpike.NewPool()
done = make(chan int, 1)
done <- 1
}The HTTP handler processes a purchase request, performs local and remote deductions atomically, returns JSON success/failure, logs the result, and releases the channel lock:
func handleReq(w http.ResponseWriter, r *http.Request) {
redisConn := redisPool.Get()
LogMsg := ""
<-done
if localSpike.LocalDeductionStock() && remoteSpike.RemoteDeductionStock(redisConn) {
RespJson(w, 1, "抢票成功", nil)
LogMsg = LogMsg + "result:1,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
} else {
RespJson(w, -1, "已售罄", nil)
LogMsg = LogMsg + "result:0,localSales:" + strconv.FormatInt(localSpike.LocalSalesVolume, 10)
}
done <- 1
writeLog(LogMsg, "./stat.log")
}The server is started on a configurable port (e.g., :3005).
4.4 Performance Test
Using ApacheBench ( ab -n 10000 -c 100 http://127.0.0.1:3005/buy/ticket) the system handled ~4 000 requests per second on a low‑end Mac, with zero failures and latency around 23 ms per request. Log output shows the gradual increase of local sales until stock is exhausted.
5. Summary
The article demonstrates a practical high‑concurrency spike system that avoids database bottlenecks by combining local in‑memory stock, Redis atomic operations, and weighted Nginx load balancing. It highlights two key lessons: distributing load across many servers and leveraging asynchronous, lock‑free designs (e.g., Go goroutines, epoll‑based servers) to fully utilize CPU resources.
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.
Big Data Technology & Architecture
Wang Zhiwu, a big data expert, dedicated to sharing big data technology.
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.
