圖文詳解:電商大促期間,如何設計"秒殺"架構支撐百萬級并發請求?
今天咱們再來聊個面試場景題中的熱門話題 — 秒殺架構如何扛住百萬流量。
其實搭建秒殺架構不難,難的是:當瞬時流量遠超日常 10-100 倍,如何在快速響應與系統穩定之間找到平衡?
下面就跟隨牛哥一起看下 "一個又快又穩的秒殺架構,是怎么搭出來的”。
先拆需求,別上來就談技術
面試時被問到秒殺架構,面試官第一個想知道的就是:你能不能把模糊的高并發拆解成可落地的具體指標?
其實拆解并不難,我們分成兩個維度切入 — 從用戶視角拆業務,從技術視角定指標。
1. 從用戶視角拆業務
從用戶點擊 “立即搶購” 到收到成功短信,整個過程能拆成 6 個關鍵環節:
圖片
以上每個環節環環相扣:資格沒通過,就不用查庫存;庫存不夠,就不用創建訂單。只有把流程拆細了,才能知道哪里該加攔截、哪里該做異步。
2. 從技術視角定指標
聊完用戶視角,再來看看技術視角。就像醫生看病要測體溫,系統設計也得有健康指標:
2.1 并發能力指標
雙 11 級別的秒殺,峰值 QPS 得扛住 100 萬,核心 TPS 至少得 5 萬,不然點擊就卡頓。
圖片
2.2 穩定性指標
用戶點搶購后,99% 的請求得在 200ms 內有回應。而且秒殺這種核心場景,故障恢復時間RTO必須 ≤ 5分鐘,畢竟不能讓用戶干等半小時吧?
圖片
2.3 數據一致性指標
庫存得 100% 準確,超賣 1 件都可能引發大量投訴。支付和訂單狀態也得同步快,延遲不能超 10 秒。
2.4 安全指標
防刷量得攔住 99% 以上的腳本,不然黃牛 10 秒就搶光 5000 件庫存。黑名單用戶必須 100% 攔住,絕對不能讓他們再參與。
圖片
以上四個指標為秒殺類系統搭建了技術層面的健康衡量標準,后續設計也要以這些量化指標為基準。
架構搭建:秒殺系統的四層骨架
需求明確后,就可以搭架構了。接入層、流量削峰層、業務邏輯層、數據層這四層,每層各司其職,缺一不可。
1. 接入層:先把垃圾流量攔在門外
用戶的請求第一個到的就是接入層,這里要是沒攔住無效流量,后面的數據庫、業務服務很快就會垮。
那該怎么選接入層的技術方案?
通常選用 「CDN + APISIX 網關」 組合。因為CDN能扛靜態資源流量,APISIX 比 Nginx 更靈活,適合秒殺這種需要頻繁調整規則的場景。
圖片
下面就來拆解這個組合:
1.1 CDN 加速
首先是靜態資源全量加速,商品圖、活動文案、倒計時動畫這些靜態內容,全扔到阿里云CDN。在北京訪問,就從北京的 CDN 節點拿數據,在上海訪問,就從上海的 CDN 節點拿數據,就近響應。
圖片
其次是設計地域路由,華北用戶優先走華北網關、華東用戶走華東網關,避免跨地域傳輸帶來的延遲損耗。
圖片
除此,還要注意 CDN 緩存的時效性:秒殺開始前 1 小時,先提前將商品詳情頁等靜態頁面 “預熱” 到全國 CDN 節點,確保活動開啟時用戶能瞬間加載;活動結束后,立即給頁面設置緩存過期,避免用戶后續訪問時看到舊的庫存信息。
圖片
1.2 APISIX 精細化限流
用 APISIX 的令牌桶插件,可根據不同維度精準控制流量,避免 “一刀切” 式限流影響正常用戶體驗。
比如單 IP 每秒最多發 5 個請求,單用戶 1 分鐘最多點 10 次,沒登錄的直接攔住。
圖片
2. 流量削峰層:把"流量尖峰"壓平
秒殺流量就像海嘯,前 10 秒可能集中迸發 80% 的請求,直接打給業務系統,數據庫肯定扛不住。
這一層的作用就是「緩沖 + 排隊」,把 "10 秒 100 萬請求" 變成 "10 分鐘 100 萬請求",讓系統慢慢處理。
圖片
主要通過以下三個手段實現:
2.1 消息隊列緩沖:流量的第一道減壓閥
用戶下單后的請求先丟進 RocketMQ,業務服務按自己的能力慢慢消費。
這里為什么選RocketMQ 而不是 Kafka?
因為它支持事務消息,能保證下單和扣庫存同步,還能處理失敗的請求,比 Kafka 更適合業務場景。但要注意,消息入隊≠秒殺成功,必須告訴用戶 "正在排隊,別著急"。
圖片
2.2 用戶排隊機制:讓等待可視化
光有 MQ 緩沖還不夠,得讓用戶知道自己排在哪?要等多久?
排隊機制用Redis的List結構做排隊隊列,用戶請求進來時,會按以下步驟處理排隊邏輯:
1)加入排隊隊列
# 將用戶ID加入商品的排隊隊列
LPUSH seckill:queue:{productId} {userId}2) 查詢排隊位置
# 查詢當前隊列長度(用戶排隊位置)
LLEN seckill:queue:{productId}3) 反饋排隊狀態
計算預估等待時間,返回給前端 "當前排第 58 位,預計等待 2 分鐘" 的提示。
這種明確的排隊反饋能讓用戶知曉等待狀態,有效減少因焦慮導致的重復刷新行為,可降低 30% 的無效請求。
2.3 MQ 積壓處理:超預期時的應急方案
最后 MQ 積壓也是個大問題,需要提前制定規則。例如制定如下規則:
平時消費組只開 1 個,要是單個 MQ 分區積壓超 10 萬條,就立刻加 2 個臨時消費組,專門分流處理下單這類核心請求。
圖片
同時暫停 "秒殺成功通知" 這類非核心消費,把所有資源讓給核心流程。
3. 業務邏輯層:高效處理核心流程
業務邏輯層堪稱整個秒殺系統的“大腦”,既要處理核心業務邏輯,又得兼顧速度、準確性和穩定性。
我們選擇 Spring Cloud Alibaba 作為技術底座,這套生態把Nacos服務發現、Sentinel熔斷降級、Dubbo RPC調用等核心能力都打包整合了,省去了自己拼湊組件的麻煩;
圖片
再搭配 Caffeine 本地緩存,存儲用戶等級、資格狀態這類高頻訪問的小數據。
具體怎么設計呢?
首先是服務獨立拆分 — 我們把秒殺業務拆成三個微服務。各自獨立部署、單獨擴容:
- 第一個是資格校驗服務:專門負責檢查用戶是否登錄、是否在黑名單、是否已經參與過秒殺;
- 第二個庫存扣減服務:專注處理Redis預扣庫存和MySQL最終確認扣減的邏輯;
- 第三個訂單生成服務:負責創建訂單、對接支付渠道等后續流程。
圖片
這樣拆分的好處很明顯:就算訂單服務臨時出問題,資格校驗和庫存扣減服務還能正常工作,不會出現 "一掛全掛" 的連鎖故障。
其次是內存快速校驗:把用戶等級、歷史購買記錄等資格校驗規則,全放進 Caffeine 本地緩存。
緩存的 Key 設計成「用戶ID + 商品ID」,value 直接存「是否有資格」的值,過期時間設為30分鐘。
緩存代碼示例:
// 使用Caffeine更新本地緩存(用戶秒殺資格)
caffeineCache.put(
"user:12345:product:67890:qualification"
, true); // true表示用戶12345對商品67890有秒殺資格
// 從Caffeine緩存查詢用戶秒殺資格
booleanhasQualification= caffeineCache.getIfPresent(
"user:12345:product:67890:qualification"
);查詢時先查本地緩存,查不到再按 "Redis→數據庫" 的順序校驗,三級緩存下來,平均耗時大大提升。
最后是熱點隔離:像 1 元秒殺手機這種爆款商品,請求流量大,必須單獨處理。
比如其他商品用 20 臺服務器支撐,爆款就配 50 臺,讓熱門請求和普通請求分開處理,避免一個商品拖垮整個系統。
圖片
4. 數據層:支撐高讀寫
數據層是“彈藥庫”,既要扛住高讀寫,又不能出錯。這一層用 Redis Cluster 存熱數據,MySQL 存冷數據,Sharding-JDBC 做分庫分表,分工明確。
4.1 Redis 存熱數據
實時庫存、用戶排隊位置、資格校驗結果全放Redis。用 3 主 3 從、16 個分片的架構,單個分片 QPS 能到 5 萬,16 個就是 80 萬,足夠扛秒殺。
圖片
4.2 MySQL 存冷數據
MySQL 存訂單、支付記錄這些要長久保存的數據,用 Sharding-JDBC 按商品 ID 哈希分為 8 個庫,每個庫再分 16 張表,總共 128 張表。
圖片
這樣單表數據量能控制在 50 萬以內,要是不分表,單表超 1000 萬就慢了,查詢速度提升 5 倍。
4.3 讀寫分離
MySQL 主庫只負責創訂單、扣庫存等寫操作,從庫負責查訂單、查支付狀態等讀操作。用 Sharding-JDBC 的插件,讀請求自動分給從庫,主庫壓力能降 40%。
圖片
但主從同步有 1-3 秒延遲,你剛下單可能查不到訂單,所以得提示 “訂單創建中,10 秒后再查"。
通過這四層配合,既扛住了高并發,又保證了數據準確與用戶體驗。
關鍵細節:這些坑一定要避開
架構搭好了,還得填“細節坑”。秒殺出問題,往往不是架構不對,而是細節沒處理好,以下幾個細節不僅是面試高頻考點,也是線上事故的重災區。
細節1:防超賣
面試官常問:“怎么保證絕對不超賣?” 這問題看似簡單,實則藏著并發編程的“魔鬼細節”。
超賣的根源是多個線程同時讀庫存、扣庫存,導致數據不一致。
解決方案是選擇 「Redis預扣+MySQL樂觀鎖」。
1.1 Redis 預扣庫存
秒殺開始前,先把庫存加載到Redis;用戶下單時,再用Lua腳本原子扣減庫存,避免并發問題。
Lua 腳本示例:
-- 1. 秒殺開始前加載庫存到Redis
-- KEYS[1] = "seckill:stock:{productId}"(庫存Key)
-- ARGV[1] = 初始庫存數量(如1000)
redis.call("HSET", KEYS[1], "available", ARGV[1])
-- 2. 用戶下單時原子扣減庫存(與加載使用相同的KEYS[1])
-- KEYS[1] = "seckill:stock:{productId}"(庫存Key)
-- ARGV[2] = 購買數量(如1)
local available = redis.call("HGET", KEYS[1], "available")
ifnot available ortonumber(available) < tonumber(ARGV[2]) then
return0-- 庫存不足,返回0
end
redis.call("HINCRBY", KEYS[1], "available", -tonumber(ARGV[2])) -- 扣減庫存
return1-- 扣減成功,返回1為什么用Lua腳本?因為它能保證多個Redis命令的原子性執行,避免中間被其他請求打斷。這是防超賣的第一道防線。
1.2 MySQL 樂觀鎖最終確認
Redis扣減成功后,得落庫才算數。庫存表設計時加個version字段,扣減時對比版本號:
SQL 示例:
-- 扣減邏輯:僅當版本號匹配、庫存充足時才更新,同時版本號自增
UPDATE seckill_stock
SET
available_stock = available_stock -1,
version = version +1
WHERE
product_id = #{productId}
AND available_stock >=1-- 確保庫存足夠
AND version = #{version}; -- 樂觀鎖版本匹配執行后看影響行數:
- 若影響行數 = 1 表示 MySQL 庫存扣減成功,流程正常推進;
- 若影響行數 = 0 表示其他請求已搶先扣減庫存,需回滾 Redis 庫存:
細節2:緩存優化
緩存是秒殺的性能引擎,但用不好就成定時炸彈。最常見的緩存問題有:穿透、擊穿和雪崩,下面講講怎么解決。
2.1 緩存穿透
緩存穿透表現為:
當用戶請求不存在的商品ID 時,緩存與數據庫均無數據,請求會持續穿透到數據庫。
可以通過攔截無效請求解決緩存穿透,核心手段是「布隆過濾器 + 緩存空值」雙重攔截:
- 布隆過濾器前置過濾:秒殺開始前,將所有參與秒殺的商品ID加載到布隆過濾器。請求進來時先過過濾器,若商品ID不存在,直接返回 "商品不存在",從源頭攔截無效請求;
圖片
- 緩存空值兜底:考慮到布隆過濾器有約 0.1% 的誤判率,所以要緩存空值兜底,如緩存 productId = 999999 的值為 null,并設置1分鐘過期時間,避免同類無效請求在 1 分鐘內反復穿透數據庫,同時也減少 Redis 空值緩存的資源占用
圖片
2.2 緩存擊穿
緩存擊穿表現為:
某個爆款商品的緩存 key 突然過期,數十萬請求會瞬間涌向數據庫。
我們要做的就是 "守護" 熱點key,用「分布式互斥鎖 + 熱點Key永不過期」應對:
- 分布式鎖控制查詢:借助 Redisson 實現可靠的分布式鎖,第一個請求拿到鎖后查詢數據庫并更新緩存,這時候其他請求處于等待的狀態,避免多個請求并發查庫。Java 代碼示例:
RLocklock= redissonClient.getLock("lock:product:" + productId);
try {
// 5秒內嘗試獲取鎖,拿到鎖后30秒自動釋放(防止死鎖)
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
Productproduct= productMapper.selectById(productId);
// 更新緩存,設置1小時過期(僅非熱點Key用,熱點Key后續特殊處理)
redisTemplate.opsForValue().set("product:" + productId, product, 1, TimeUnit.HOURS);
return product;
} else {
// 未拿到鎖,重試讀取緩存(等待其他請求更新后再查)
return redisTemplate.opsForValue().get("product:" + productId);
}
} finally {
// 確保鎖釋放,避免內存泄漏
if (lock.isHeldByCurrentThread()) lock.unlock();
}- 熱點Key永不過期:對爆款商品這類熱點Key,代碼層不設置主動過期時間,而是用定時任務每30分鐘異步查詢數據庫更新緩存,從根本上避免過期瞬間的流量沖擊。
2.3 緩存雪崩
緩存雪崩則表現為:
大量商品緩存 key 在同一時間過期,數據庫會被集中請求壓垮。
圖片
為了避免大量請求同時間過期,我們通過「過期時間隨機化 + 本地緩存兜底」化解:
- 過期時間隨機化:給每個緩存的基礎過期時間加上 ± 10分鐘的隨機值,讓緩存失效時間分散,避免集中過期;
圖片
- 本地緩存兜底:即便 Redis 緩存過期,通過應用本地的 Caffeine 緩存兜底,仍能直接返回數據,無需請求 Redis。
細節3:分布一致性
分布式系統里,絕對一致是不可能的,網絡延遲、節點故障都會導致數據暫時對不上,關鍵是要 「最終一致」,別出現 “付了錢沒訂單”“扣了庫存沒下單” 的情況。
為實現這一目標,我們主要通過輕量化 TCC 事務、消息隊列最終一致、實時對賬任務三種方案分層保障
3.1 輕量化 TCC 事務:保障短事務
秒殺下單是典型的短事務,流程簡單、執行時間短,用輕量化的 TCC(Try-Confirm-Cancel)事務再合適不過:
- Try 階段:不直接修改核心數據,只做資格校驗和預扣 Redis 庫存
- Confirm 階段:若 Try 階段無問題,正式執行核心操作 — 從 MySQL 中扣減實際庫存,同時生成訂單數據
- Cancel 階段:若 Try 階段后出現異常,如用戶支付超時、庫存不足,則執行回滾操作 — 回滾 Redis 庫存、刪排隊記錄。
不過 TCC 的難點在于要寫很多補償代碼,比如 Cancel 失敗了要設置重試確保最終回滾。
3.2 消息隊列:應對斷連情況
秒殺高峰期可能出現服務斷連,此時單靠 TCC 難以保障數據一致,需借助 RocketMQ 的事務消息能力,確保訂單創建與庫存扣減的最終同步:
訂單創建后,先發一條事務消息到 RocketMQ,庫存服務讀取消息后執行「扣庫存」操作。要是扣庫存失敗,消息會自動重試 3 次,3次都失敗后就進入死信隊列,人工處理。
圖片
這樣能確保「訂單創建」 和 「庫存扣減」 要么都成功,要么都失敗,不會出現數據不一致。
3.3 實時對賬任務:最后一道防線
即使有 TCC 和事務消息,仍可能因極端場景出現 「Redis 庫存與 MySQL 庫存不一致」 的情況。因此需要一套實時對賬任務,修正數據偏差:
每分鐘觸發一次對賬任務,對比 Redis 與 MySQL 的庫存差異:
圖片
- 若差值在合理范圍(如±1),視為正常延遲,無需處理;
- 若差值超過10,則以 MySQL 數據為準修復 Redis 庫存,確保雙端數據最終一致。
優化方向
優化不是炫技,而是解決用戶痛點、降低成本,"讓用戶搶得爽、讓公司少花錢" 這才是優化的價值,我們從三個優化方向切入:
優化1:無效請求提前過濾
秒殺場景下,90%的請求都是無效的,比如重復點擊、沒資格、庫存不足,提前過濾能“減負增效”。因此需要在接入層、應用層各加過濾規則:
過濾階段 | 規則示例 | 過濾效果 |
接入層 | 單IP每秒>5請求、User-Agent異常(腳本標識) | 擋掉30%無效請求 |
應用層 | 未登錄用戶、黑名單用戶、已秒殺用戶 | 擋掉50%無效請求 |
以「已秒殺用戶過濾」 為例,我們用 Redis 的 Set 結構記錄參與過的用戶,用戶每次下單前,先通過SISMEMBER命令判斷是否在 Set 中,若存在,直接返回 "您已參與過本次秒殺",僅這一條規則就能讓重復下單請求降低 80%。
優化2:異步處理提速
用戶下單后,發短信、推App通知、更新用戶積分,這些操作用戶不關心實時性,完全可以異步處理。
我們用 RocketMQ 的普通消息隊列(非事務消息),下單成功后直接向 RocketMQ 發送一條普通消息,后臺服務根據自身處理能力逐步消費。
圖片
優化3:跟著流量彈性擴容
云時代了,還手動擴容就太原始了。用 K8s 的 HPA 自動擴縮容,資源跟著流量變:秒殺前 10 分鐘,HPA 會自動擴到 10 個實例;活動結束 5 分鐘,CPU 降下來了,又縮回 2 個。
圖片
既保證性能,又不浪費資源,每月能省 40% 的服務器成本。
除此以外,監控和容災也必不可少,“三分技術,七分運維”。因此需要通過全鏈路監控、混沌工程演練、降級預案三者結合筑牢系統穩定性。
總結:秒殺架構的設計心法
回顧整個秒殺架構設計,我提煉出了四個核心原則:
1. 流量攔截要“前置”:CDN擋靜態、網關限流量、排隊篩用戶,把無效請求擋在越上游越好,別讓它們“走到數據庫門口才被攔下”。
2. 數據一致是“底線”:Redis原子扣減、MySQL樂觀鎖、TCC事務、實時對賬,這四重保障缺一不可,超賣1件,對用戶來說就是“整個活動不可信”。
3. 監控容災“不偷懶”:全鏈路監控、混沌演練、降級預案,這三件事做扎實了,線上出問題也能兜底。
4. 持續優化“不停步”:沒有“一勞永逸”的秒殺系統,流量變了、業務變了,架構就得跟著變。去年的方案今年可能就不適用,持續迭代,才能真正做好秒殺。
最后想說,秒殺架構要把用戶體驗放在第一位,把數據安全當作底線,這樣設計出來的系統,才能真正扛住“雙11”的流量洪峰,也才能在面試中“打動面試官”。































