How to Build a Million‑User Ticket‑Snatching System with Nginx, Go, and Redis

This article explores the design of a high‑concurrency train‑ticket flash‑sale system, covering load‑balancing strategies, weighted Nginx configuration, in‑memory pre‑deduction, Redis Lua scripts, and Go implementation, and demonstrates its performance with real‑world stress testing.

21CTO
21CTO
21CTO
How to Build a Million‑User Ticket‑Snatching System with Nginx, Go, and Redis

Introduction

During holidays, users in major Chinese cities face the problem of抢火车票 (snatching train tickets). The 12306 service handles millions of QPS, making it a classic extreme‑concurrency scenario.

System Architecture

High‑concurrency systems are typically deployed as distributed clusters with multiple layers of load balancing (OSPF, LVS, Nginx) and disaster‑recovery mechanisms.

Load‑Balancing Overview

Three common methods are round‑robin, weighted round‑robin, and IP‑hash. The article demonstrates Nginx weighted round‑robin 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; }
}

Ticket‑Snatching Logic

The process consists of three stages: create order, deduct inventory, and payment. Direct DB transactions are too slow for flash sales, so the article proposes a pre‑deduction (预扣库存) strategy using local memory and a remote Redis hash.

Local Stock Deduction

func (spike *LocalSpike) LocalDeductionStock() bool {
    spike.LocalSalesVolume++
    return spike.LocalSalesVolume < spike.LocalInStock
}

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
`

Both deductions must succeed; otherwise the request is rejected as “sold out”.

Implementation in Go

func handleReq(w http.ResponseWriter, r *http.Request) {
    redisConn := redisPool.Get()
    <-done
    if localSpike.LocalDeductionStock() && remoteSpike.RemoteDeductionStock(redisConn) {
        util.RespJson(w, 1, "抢票成功", nil)
    } else {
        util.RespJson(w, -1, "已售罄", nil)
    }
    done <- 1
}

Testing

Using ApacheBench (ab) with 10 000 requests and concurrency 100, the single‑machine prototype handled about 4 300 requests per second, with response times around 23 ms.

Conclusion

The combination of load‑balancing, in‑memory pre‑deduction, and Redis atomic Lua scripts provides a scalable ticket‑snatching system that avoids DB bottlenecks, tolerates node failures, and maintains inventory consistency.

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.

load balancingRedisGohigh concurrencyNGINXticketing system
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

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.