架構(gòu)拆解:Redis預(yù)減庫存與Kafka異步削峰實(shí)戰(zhàn))
秒殺系統(tǒng)幾乎是所有頭部互聯(lián)網(wǎng)公司 Java 后端面試?yán)锏膲狠S題也是我這些年面試候選人時(shí)最愛聊的話題之一。你簡(jiǎn)歷上只要寫了高并發(fā)、微服務(wù)這幾個(gè)詞十有八九會(huì)被問“設(shè)計(jì)一個(gè)秒殺系統(tǒng)你會(huì)怎么做”或者更直接“Redis 和數(shù)據(jù)庫庫存不一致怎么辦”“MQ 消息積壓了怎么處理”。這套“Spring Boot Kafka Redis”的組合在電商秒殺場(chǎng)景里已經(jīng)成了事實(shí)標(biāo)配背后的原理和應(yīng)用細(xì)節(jié)卻往往被一筆帶過。這篇文章我把這套體系拆開揉碎講一遍包括業(yè)務(wù)層面的設(shè)計(jì)思路、技術(shù)層面的原理剖析還有我在模擬項(xiàng)目里一點(diǎn)點(diǎn)調(diào)出來的代碼實(shí)現(xiàn)和踩坑記錄。無論你是正要準(zhǔn)備面試還是已經(jīng)在負(fù)責(zé)秒殺相關(guān)系統(tǒng)的開發(fā)這份整理都能直接用上。1. 秒殺系統(tǒng)的核心挑戰(zhàn)與技術(shù)選型思路1.1 電商秒殺到底難在哪里先說業(yè)務(wù)本身。秒殺的定義就是在極短時(shí)間內(nèi)、有限庫存下大量用戶同時(shí)搶購?fù)患唐贰1热缒呈謾C(jī)品牌限量發(fā)售 200 臺(tái)開搶瞬間可能有 20 萬人同時(shí)發(fā)起請(qǐng)求。這樣量級(jí)的流量直接打到單機(jī) MySQL 上數(shù)據(jù)庫 10 秒內(nèi)必掛。我把秒殺系統(tǒng)的難點(diǎn)歸納成三個(gè)維度超高并發(fā)流量瞬時(shí) QPS 可能達(dá)到 10 萬以上系統(tǒng)需要在上萬并發(fā)連接下保持可用。超賣防控100 個(gè)庫存賣出 120 單就是重大生產(chǎn)事故。超賣不僅涉及資損還會(huì)引發(fā)輿論風(fēng)險(xiǎn)屬于絕對(duì)不能踩的紅線。數(shù)據(jù)一致性庫存扣減、訂單生成、支付回調(diào)這些環(huán)節(jié)不能因?yàn)楦卟l(fā)就亂了套。傳統(tǒng)做法是把所有請(qǐng)求都直接打到數(shù)據(jù)庫上用 MySQL 的行鎖去控制庫存但數(shù)據(jù)庫每秒能承受的事務(wù)量級(jí)有限一般單庫也就幾千根本無法承接十萬級(jí)并發(fā)。這里面最核心的問題是——為什么要讓所有流量都打到最脆弱的環(huán)節(jié)架構(gòu)設(shè)計(jì)的本質(zhì)任務(wù)就是讓大部分流量在到達(dá)數(shù)據(jù)庫之前就被攔截或消化掉。1.2 為什么偏偏是這三個(gè)組件先給出一套我自己用得很順的分層模型接入層負(fù)責(zé)限流、風(fēng)控、用戶身份校驗(yàn)。緩存層用 Redis 承擔(dān)庫存預(yù)減和熱點(diǎn)數(shù)據(jù)讀取。異步緩沖層用 Kafka 承接削峰后的下單請(qǐng)求。存儲(chǔ)層MySQL 只處理真正需要落庫的數(shù)據(jù)。這套分層里每個(gè)組件各司其職Spring Boot提供了一套成熟的開發(fā)框架把 HTTP 接口、依賴注入、自動(dòng)配置都做好了讓我能集中精力寫核心業(yè)務(wù)代碼而不是花時(shí)間搭骨架。Redis被放在數(shù)據(jù)庫前面是因?yàn)樗淖x寫性能極高同時(shí)它支持 Lua 腳本原子執(zhí)行。庫存預(yù)減操作在 Redis 里完成可以扛住絕大多數(shù)并發(fā)流量。Kafka在這里扮演泄洪閘的角色。Redis 預(yù)減庫存成功后不直接寫數(shù)據(jù)庫而是先把消息發(fā)到 Kafka由消費(fèi)者異步創(chuàng)建訂單、扣減 DB 庫存。峰值流量被削平了數(shù)據(jù)庫的寫壓力就變得可控。這套架構(gòu)有一個(gè)核心假設(shè)最終一致性是被接受的。秒殺場(chǎng)景里用戶關(guān)心的是“我搶沒搶到”只要在幾秒內(nèi)給出結(jié)果不需要像金融轉(zhuǎn)賬那樣強(qiáng)同步。這套組件的另一個(gè)好處是故障隔離。Redis 掛了只影響秒殺入口Kafka 堆積了訂單也不會(huì)把數(shù)據(jù)庫打崩。每個(gè)環(huán)節(jié)都有一定的緩沖余量這在真實(shí)生產(chǎn)環(huán)境中非常重要。2. 核心技術(shù)細(xì)節(jié)與原理剖析2.1 Redis 預(yù)減庫存把壓力擋在最前面預(yù)減庫存的核心思想是先在一個(gè)高速的存儲(chǔ)里把“誰能買”這件事定了再讓被選中的人去完成真正的訂單創(chuàng)建。Redis 因?yàn)閮蓚€(gè)特點(diǎn)成為這個(gè)場(chǎng)景的完美人選一是基于內(nèi)存單線程模型下讀寫延遲極低二是支持 Lua 腳本原子性執(zhí)行徹底避免了并發(fā)扣減時(shí)的競(jìng)爭(zhēng)問題。我一般用一段 Lua 腳本來做預(yù)減。腳本先把商品的剩余庫存讀出來如果小于購買數(shù)量直接返回失敗如果充足就對(duì)庫存做扣減并返回成功。因?yàn)?Redis 是單線程執(zhí)行腳本的一段腳本在執(zhí)行期間不會(huì)被其他命令打斷所以多個(gè)請(qǐng)求同時(shí)來時(shí)它們是天然串行的不會(huì)出現(xiàn)“超賣”的問題。實(shí)際生產(chǎn)環(huán)境里還需要給庫存 key 設(shè)置合理的過期時(shí)間防止 Redis 內(nèi)存長期堆積無用數(shù)據(jù)。我一般設(shè)置為活動(dòng)結(jié)束后 1 小時(shí)這樣既能覆蓋退款、補(bǔ)償?shù)群罄m(xù)流程又不至于長期占用內(nèi)存。注意Redis 預(yù)減庫存是“預(yù)扣”數(shù)據(jù)庫的扣減才是“最終憑證”。在秒殺成功后如果用戶不支付庫存還需要回補(bǔ)。這里可以有兩種策略——超時(shí)自動(dòng)取消訂單并回補(bǔ) Redis 庫存或者在支付環(huán)節(jié)再去扣一次 DB 庫存。我在項(xiàng)目里采用的是前者邏輯更閉環(huán)。2.2 Kafka 異步削峰把瞬時(shí)洪峰變成平緩溪流如果 Redis 預(yù)減完成后直接把訂單寫入 MySQL數(shù)據(jù)庫依然會(huì)被瞬時(shí)打爆。因?yàn)?Redis QPS 能做到十萬MySQL 卻只有幾千這中間必須有一個(gè)緩沖地帶。Kafka 的定位就是這個(gè)緩沖。Kafka 之所以在大數(shù)據(jù)量下表現(xiàn)優(yōu)異核心在于它的設(shè)計(jì)理念順序?qū)懘疟P而不是隨機(jī)寫磁盤順序?qū)懙乃俣冗h(yuǎn)超隨機(jī)寫。通過分區(qū)實(shí)現(xiàn)并行消費(fèi)一個(gè) Topic 分成多個(gè) Partition消費(fèi)者可以并行處理不同分區(qū)的數(shù)據(jù)吞吐量隨分區(qū)數(shù)擴(kuò)容。消息持久化寫入 Kafka 后消息不會(huì)丟失消費(fèi)者宕機(jī)后恢復(fù)還能繼續(xù)消費(fèi)為異步流程提供了可靠性保障。我把下單消息發(fā)送到 Kafka 后接口層就可以直接返回“搶購成功請(qǐng)等待支付”。用戶的感知是流暢的后端則按自己的節(jié)奏慢慢處理訂單創(chuàng)建。2.3 數(shù)據(jù)庫最終一致性避免超賣的最后防線Redis 預(yù)減庫存和 DB 庫存扣減是兩套數(shù)據(jù)它們天然存在不一致的窗口。但只要流程設(shè)計(jì)正確是可以做到最終一致的。我的設(shè)計(jì)是Redis 預(yù)減成功只能說明“用戶獲得了購買資格”真正的庫存扣減發(fā)生在 Kafka 消費(fèi)者創(chuàng)建訂單時(shí)。DB 層用一句帶條件的 SQL 來扣減庫存——UPDATE product SET stock stock - 1 WHERE id ? AND stock 0。這句 SQL 利用數(shù)據(jù)庫行鎖保證同一商品在同一時(shí)刻只有一個(gè)事務(wù)能修改庫存從物理層面避免了超賣。在消息異步處理過程中如果消費(fèi)者處理失敗可以重試。如果反復(fù)重試仍然失敗需要把消息轉(zhuǎn)入死信 Topic再通過定時(shí)任務(wù)補(bǔ)償。這些都在代碼層面做了兜底保證 DBA 層面的數(shù)據(jù)最終不會(huì)多扣或少扣。這里有一個(gè)容易犯的錯(cuò)誤認(rèn)為只要 Redis 扣減正確DB 扣不扣都無所謂。實(shí)際上Redis 只是分布式緩存數(shù)據(jù)可能因宕機(jī)丟失。DB 才是最終數(shù)據(jù)的權(quán)威來源。兩個(gè)系統(tǒng)的數(shù)據(jù)必須對(duì)上賬依賴對(duì)賬腳本定期掃描差異。從“強(qiáng)一致性”到“最終一致性”的思維轉(zhuǎn)變是秒殺系統(tǒng)架構(gòu)設(shè)計(jì)的核心也是面試官最想從你嘴里聽到的關(guān)鍵詞之一。3. 實(shí)操過程與代碼實(shí)現(xiàn)下面這套代碼是一個(gè)完整的秒殺核心流程我從一個(gè)模擬項(xiàng)目里抽取出來的已經(jīng)去掉業(yè)務(wù)細(xì)節(jié)保留了最核心的主鏈路。3.1 整體項(xiàng)目結(jié)構(gòu)seckill-demo/ ├── pom.xml ├── src/main/java/com/demo/seckill/ │ ├── controller/SeckillController.java │ ├── service/SeckillService.java │ ├── service/OrderService.java │ ├── mq/SeckillProducer.java │ ├── mq/OrderConsumer.java │ ├── config/RedisConfig.java │ └── mapper/ProductMapper.java └── src/main/resources/ ├── application.yml └── lua/stock.lua3.2 秒殺接口前置校驗(yàn)與限流第一個(gè)入口是 Controller。這里做的三件事是校驗(yàn)用戶登錄態(tài)、準(zhǔn)備 Redis key、調(diào)用預(yù)減庫存邏輯。RestController RequestMapping(/api/seckill) public class SeckillController { Autowired private SeckillService seckillService; PostMapping(/{skuId}) public SeckillResult doSeckill(PathVariable Long skuId, RequestParam Long userId) { // 前置校驗(yàn)用戶是否登錄、是否在黑名單中 if (userId null) { return SeckillResult.fail(請(qǐng)先登錄); } // 限流每個(gè)用戶每秒最多請(qǐng)求3次 boolean allowed seckillService.rateLimit(userId); if (!allowed) { return SeckillResult.fail(請(qǐng)求過于頻繁); } return seckillService.startSeckill(skuId, userId); } }限流是一個(gè)極其重要的前置能力。我見過不少系統(tǒng)連最基本的按用戶限流都沒有做一個(gè)用戶用腳本幾十毫秒發(fā)一次請(qǐng)求庫存瞬間被腳本黨搶光。限流最輕量的實(shí)現(xiàn)是用 Redis 計(jì)數(shù)器固定窗口 1 秒內(nèi)限制 N 次。更復(fù)雜的滑動(dòng)窗口、令牌桶算法適合放到網(wǎng)關(guān)層這里 Redis 版足夠。3.3 Redis 預(yù)減庫存的 Lua 腳本實(shí)現(xiàn)這是整套系統(tǒng)的核心我把 Lua 腳本單獨(dú)放在 resources 目錄。腳本接收三個(gè)參數(shù)商品庫存 key、購買數(shù)量、用戶標(biāo)識(shí)。腳本內(nèi)部完成庫存檢查與扣減。-- KEYS[1]: 庫存 key -- ARGV[1]: 購買數(shù)量 -- ARGV[2]: 用戶唯一標(biāo)識(shí) local stock redis.call(GET, KEYS[1]) if not stock then return -1 end if tonumber(stock) tonumber(ARGV[1]) then return 0 end redis.call(DECRBY, KEYS[1], ARGV[1]) -- 記錄已搶購用戶用于冪等判斷 redis.call(SADD, seckill_users_ .. KEYS[1], ARGV[2]) return 1Java 側(cè)調(diào)用Service public class SeckillService { Autowired private StringRedisTemplate redisTemplate; Autowired private SeckillProducer seckillProducer; private static final DefaultRedisScriptLong SECKILL_SCRIPT; static { SECKILL_SCRIPT new DefaultRedisScript(); SECKILL_SCRIPT.setLocation(new ClassPathResource(lua/stock.lua)); SECKILL_SCRIPT.setResultType(Long.class); } public SeckillResult startSeckill(Long skuId, Long userId) { String stockKey seckill:stock: skuId; String userKey seckill:user: skuId; // 冪等校驗(yàn)同一用戶不能重復(fù)搶 Boolean isMember redisTemplate.opsForSet().isMember(userKey, userId.toString()); if (Boolean.TRUE.equals(isMember)) { return SeckillResult.fail(該商品已搶購請(qǐng)勿重復(fù)操作); } // 執(zhí)行 Lua 預(yù)減庫存 Long result redisTemplate.execute(SECKILL_SCRIPT, Collections.singletonList(stockKey), 1, userId.toString()); if (result null || result -1) { return SeckillResult.fail(活動(dòng)不存在或已結(jié)束); } if (result 0) { return SeckillResult.fail(商品已售罄); } // 預(yù)扣成功發(fā)送 MQ 消息異步創(chuàng)建訂單 seckillProducer.sendSeckillMessage(skuId, userId); return SeckillResult.success(搶購成功請(qǐng)盡快支付); } }這段代碼的關(guān)鍵在于 Lua 腳本把“檢查庫存”和“扣減庫存”合成了一個(gè)原子操作。如果不這樣做檢查完發(fā)現(xiàn)庫存還有下一秒另一個(gè)請(qǐng)求把庫存扣光了當(dāng)前請(qǐng)求還會(huì)繼續(xù)執(zhí)行——這就是典型的超賣 bug。我建議把冪等校驗(yàn)放在 Lua 腳本內(nèi)部用 SADD 命令保證一個(gè)用戶只能搶一次避免 Redis 網(wǎng)絡(luò)開銷之外的多余事務(wù)。上面代碼里我用 Set 做了第一層校驗(yàn)Lua 內(nèi)再做二次校驗(yàn)雙保險(xiǎn)。3.4 Kafka 生產(chǎn)者與消費(fèi)者的落地細(xì)節(jié)生產(chǎn)者負(fù)責(zé)把預(yù)減成功的數(shù)據(jù)丟給 Kafka。這里要注意選對(duì) key保證同一個(gè)商品的消息進(jìn)入同一個(gè)分區(qū)才能做到局部順序消費(fèi)。Component public class SeckillProducer { Autowired private KafkaTemplateString, String kafkaTemplate; public void sendSeckillMessage(Long skuId, Long userId) { String message skuId : userId; // 以 skuId 為 key同一商品的訂單進(jìn)入同一分區(qū)保持順序 kafkaTemplate.send(seckill_orders, skuId.toString(), message); } }消費(fèi)者一端創(chuàng)建訂單的流程包含幾個(gè)關(guān)鍵步驟接收消息、冪等校驗(yàn)、扣減 DB 庫存、創(chuàng)建訂單。Component public class OrderConsumer { Autowired private OrderService orderService; KafkaListener(topics seckill_orders, groupId seckill_order_group) public void onMessage(ConsumerRecordString, String record) { String[] parts record.value().split(:); Long skuId Long.parseLong(parts[0]); Long userId Long.parseLong(parts[1]); // 1. 前置冪等校驗(yàn)同一個(gè)訂單不能創(chuàng)建兩次 if (orderService.existsOrder(skuId, userId)) { return; } // 2. 扣減 DB 庫存條件更新防止超賣 int rows orderService.deductStock(skuId); if (rows 0) { // 更新失敗庫存不足發(fā)送告警并補(bǔ)償 Redis 庫存 orderService.compensateStock(skuId); return; } // 3. 創(chuàng)建訂單 orderService.createOrder(skuId, userId); } }DB 扣減庫存的 Mapper用了條件更新來兜底超賣Mapper public interface ProductMapper { Update(UPDATE product SET stock stock - 1 WHERE id #{skuId} AND stock 0) int deductStock(Param(skuId) Long skuId); }注意這里返回的rows如果為 0說明數(shù)據(jù)庫判斷庫存已經(jīng)不足降級(jí)方案是把 Redis 里的庫存回補(bǔ)回去。這套回補(bǔ)邏輯我測(cè)試了很多次核心難點(diǎn)在于可能多個(gè)消費(fèi)者同時(shí)失敗回補(bǔ)時(shí)又要保證 Redis 庫存與 DB 庫存的一致性這里我額外加了分布式鎖控制回補(bǔ)并發(fā)度。3.5 消息手動(dòng)提交與失敗重試在實(shí)際生產(chǎn)環(huán)境中Kafka 消費(fèi)者不能使用默認(rèn)的自動(dòng)提交 offset否則會(huì)出現(xiàn)“消息被提交但業(yè)務(wù)處理失敗”的場(chǎng)景。我使用手動(dòng)確認(rèn)模式只有訂單創(chuàng)建成功后才提交 offset。KafkaListener(topics seckill_orders, groupId seckill_order_group) public void onMessage(ConsumerRecordString, String record, Acknowledgment ack) { try { //... 業(yè)務(wù)邏輯 ack.acknowledge(); } catch (Exception e) { // 記錄日志不提交 offset等待消息消費(fèi)超時(shí)后自動(dòng)重平衡 log.error(訂單創(chuàng)建失敗: skuId{}, userId{}, skuId, userId, e); } }這個(gè)細(xì)節(jié)很容易被忽略但它直接影響數(shù)據(jù)的可靠性和最終一致性。3.6 Spring Boot 整合配置application.yml 里最關(guān)鍵的幾項(xiàng)配置spring: redis: host: localhost port: 6379 kafka: bootstrap-servers: localhost:9092 producer: key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.apache.kafka.common.serialization.StringSerializer acks: all retries: 3 consumer: group-id: seckill_order_group enable-auto-commit: false auto-offset-reset: latest key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.apache.kafka.common.serialization.StringDeserializeracks: all表示消息被所有副本接收才算成功retries: 3提高了發(fā)送可靠性。消費(fèi)者開啟enable-auto-commit: false并且配合手動(dòng) ack才能真正做到“不丟消息”。4. 常見問題與排查技巧實(shí)錄4.1 超賣問題排查超賣是最嚴(yán)重的生產(chǎn)事故我把它放在第一位說。模擬超賣時(shí)可以使用 JMeter 或 Gatling 并發(fā)請(qǐng)求 1000 個(gè)用戶搶 100 個(gè)庫存。如果壓測(cè)后發(fā)現(xiàn)訂單數(shù)量超過了庫存數(shù)量排查方向按以下順序進(jìn)行檢查 Redis Lua 腳本是否正確執(zhí)行扣減時(shí)是否先判斷庫存不足再返回失敗。檢查 DB 扣庫存 SQL 是否帶條件WHERE stock 0必不可少。檢查消費(fèi)者是否重復(fù)消費(fèi)沒有冪等設(shè)計(jì)時(shí)同樣的消息被 Kafka 集群重平衡后可能消費(fèi)兩次。排查超賣的最佳方式是在數(shù)據(jù)庫訂單表增加唯一鍵約束比如(sku_id, user_id)唯一索引作為數(shù)據(jù)層面的最后防線。4.2 緩存穿透、擊穿與雪崩這三個(gè)問題是 Redis 場(chǎng)景的經(jīng)典配套問題面試必考生產(chǎn)必踩。穿透用戶不斷請(qǐng)求數(shù)據(jù)庫中不存在的商品 ID導(dǎo)致 Redis 查不到每次都要打到 DBDB 壓力巨大。解決方案是布隆過濾器攔截非法 ID或者將空結(jié)果緩存幾秒。擊穿某個(gè)熱點(diǎn) key 在過期瞬間大量請(qǐng)求全部打到 DB。解決方案是熱點(diǎn) key 不設(shè)置過期時(shí)間或者使用互斥鎖只允許一個(gè)請(qǐng)求去加載數(shù)據(jù)。雪崩大量 key 在同一時(shí)間過期導(dǎo)致 DB 請(qǐng)求量瞬間飆高。解決方案是設(shè)置過期時(shí)間時(shí)加入隨機(jī)值避免集中淘汰。我在秒殺場(chǎng)景里重點(diǎn)關(guān)注擊穿問題。秒殺商品是天然的熱點(diǎn) key數(shù)量有限且訪問集中。處理方案很簡(jiǎn)單秒殺商品的活動(dòng)信息在活動(dòng)期間永不過期庫存數(shù)據(jù)由 Lua 腳本和 DB 回寫聯(lián)動(dòng)更新。4.3 Kafka 消息積壓與重復(fù)消費(fèi)消費(fèi)者處理不過來了Topic 里的 Lag 持續(xù)增長這是最讓人頭疼的問題。排查和解決我總結(jié)成四步先看消費(fèi)者的處理耗時(shí)是否因?yàn)?DB 鎖等待、外部接口超時(shí)導(dǎo)致消費(fèi)變慢。增加分區(qū)數(shù)和消費(fèi)者實(shí)例數(shù)Kafka 的分區(qū)數(shù)是并發(fā)消費(fèi)的上限分區(qū)不夠加再多的消費(fèi)者也沒用。批量消費(fèi)優(yōu)化把單條處理改成批量處理減少網(wǎng)絡(luò) IO 和 DB 連接的建立次數(shù)。臨時(shí)擴(kuò)容緊急情況可以新建一個(gè)臨時(shí) Topic用多倍的消費(fèi)者實(shí)例去消費(fèi)原 Topic 里積壓的消息處理完成后再切回來。重復(fù)消費(fèi)的解決辦法是冪等設(shè)計(jì)。最輕量的是用數(shù)據(jù)庫唯一索引插入重復(fù)訂單時(shí)直接失敗然后 catch 后做“已存在”處理。我在訂單表里加了(sku_id, user_id)唯一索引重復(fù)消費(fèi)時(shí)插入會(huì)報(bào)錯(cuò)但邏輯上可以把它當(dāng)成一次正常的重復(fù)提交忽略掉。4.4 問題速查表問題現(xiàn)象可能原因排查命令/思路解決方案訂單數(shù)超過庫存數(shù)Lua 腳本未原子執(zhí)行 / DB 扣減無條件檢查 Lua 腳本和 UPDATE SQL預(yù)減腳本原子化 DB 條件更新Redis 庫存變成負(fù)數(shù)回補(bǔ)庫存邏輯錯(cuò)誤 / 扣減前未判斷MGET seckill:stock:*查看腳本內(nèi)先判斷再扣減回補(bǔ)加鎖Kafka Lag 持續(xù)變大消費(fèi)者數(shù)量不足 / 處理邏輯慢kafka-consumer-groups.sh --describe增加分區(qū)與消費(fèi)者批量消費(fèi)用戶重復(fù)搶購成功冪等設(shè)計(jì)缺失檢查 Set 和唯一索引Redis Set 記錄 數(shù)據(jù)庫唯一索引消息丟失生產(chǎn)者 acks0 / 消費(fèi)者自動(dòng)提交查看日志中是否有發(fā)送失敗acksall 手動(dòng) ack5. 面試官追問方向與應(yīng)答要點(diǎn)5.1 高頻基礎(chǔ)問題把 Spring Boot Kafka Redis 這套方案講完面試官通常不會(huì)就此打住而是順著往下挖原理。我在多次面試交流里發(fā)現(xiàn)問得最密集的有這幾個(gè)Redis 是單線程的為什么還這么快關(guān)鍵回答點(diǎn)是基于內(nèi)存、核心數(shù)據(jù)結(jié)構(gòu)高效、IO 多路復(fù)用、單線程避免上下文切換和鎖競(jìng)爭(zhēng)。注意不要回答“Redis 所有版本都是單線程”從 6.0 開始網(wǎng)絡(luò)模型也支持多線程了但是命令執(zhí)行還是單線程這個(gè)概念要講清楚。Kafka 為什么吞吐量遠(yuǎn)高于傳統(tǒng) MQ關(guān)鍵回答點(diǎn)是順序?qū)懘疟P、零拷貝、分區(qū)并行、批量發(fā)送??梢詫?duì)比 RabbitMQ 基于 AMQP 協(xié)議要處理更多復(fù)雜語義吞吐量天花板自然更低。為什么不用 Redis 直接把訂單存了關(guān)鍵回答點(diǎn)是Redis 是緩存可靠性不夠數(shù)據(jù)重啟可能丟失。訂單是核心業(yè)務(wù)數(shù)據(jù)必須有持久化保障。剛好回答這個(gè)問題能順勢(shì)引出 Kafka 的持久化機(jī)制。有一個(gè)容易踩的坑面試官問“Redis 和數(shù)據(jù)庫一致性怎么保證”你的回答里千萬不要說“用 Redis 存數(shù)據(jù)數(shù)據(jù)庫只做備份”這會(huì)讓面試官覺得你缺少分布式系統(tǒng)的基本素養(yǎng)。正確思路是強(qiáng)調(diào) Redis 預(yù)扣、DB 落庫、對(duì)賬補(bǔ)償?shù)淖罱K一致性鏈路。5.2 進(jìn)階場(chǎng)景推導(dǎo)面試官接下來的問題往往是“如果流量再漲十倍怎么辦”。核心思路是逐層加厚接入層引入負(fù)載均衡器集群水平擴(kuò)容無狀態(tài)服務(wù)節(jié)點(diǎn)。緩存層用 Redis 集群或 Redis 分片擴(kuò)容把熱點(diǎn) key 分散到多節(jié)點(diǎn)。MQ 層增加 Topic 分區(qū)數(shù)掛更多消費(fèi)者實(shí)例。DB 層引入分庫分表把訂單表按用戶維度分片降低單庫壓力。這整套擴(kuò)容思路本質(zhì)上就是從“單點(diǎn)系統(tǒng)”進(jìn)化成“分布式系統(tǒng)”的過程。每一層都無狀態(tài)化每一層都能獨(dú)立擴(kuò)展這個(gè)架構(gòu)演進(jìn)思路是面試時(shí)最有區(qū)分度的部分。5.3 設(shè)計(jì)題應(yīng)答框架如果面試官給一個(gè)全新的秒殺場(chǎng)景讓你現(xiàn)場(chǎng)設(shè)計(jì)不要上來就寫代碼按這個(gè)順序講分析瓶頸流量峰值、庫存量、數(shù)據(jù)一致性要求。分層設(shè)計(jì)每一層負(fù)責(zé)什么。核心機(jī)制預(yù)減庫存、異步削峰、冪等去重、超時(shí)回補(bǔ)。數(shù)據(jù)一致性最終一致性如何保證兜底方案是什么。容量規(guī)劃預(yù)估 QPS計(jì)算 Redis、Kafka、MySQL 的量級(jí)。實(shí)操下來我能明顯感覺到這套設(shè)計(jì)邏輯比堆砌技術(shù)名詞更有說服力。不要一上來就喊 Redis 緩存、Kafka 消峰先把每一步的輸入輸出和邊界條件講清楚面試官才會(huì)覺得你真懂系統(tǒng)。6. 從面試到實(shí)戰(zhàn)的一點(diǎn)心里話我面試過不少候選人能把技術(shù)名詞背得很溜的很多但真正能答好秒殺系統(tǒng)的人都有幾個(gè)共同點(diǎn)知道每個(gè)組件在系統(tǒng)里承擔(dān)什么角色知道為什么用這個(gè)組件而不是另一個(gè)更知道數(shù)據(jù)不一致了怎么兜底。秒殺系統(tǒng)不是技術(shù)的堆砌而是一個(gè)對(duì)一致性、可靠性、高性能要求極高的綜合工程。Redis 解決了性能問題Kafka 解決了流量緩沖問題Spring Boot 解決了工程化的問題而真正讓系統(tǒng)跑穩(wěn)的是你對(duì)每一步異常情況都做好了預(yù)案。把上面這套設(shè)計(jì)思路理解透、代碼調(diào)過、問題踩過再遇到這類問題不管是面試還是實(shí)戰(zhàn)你都心里有底。