戰(zhàn):從數(shù)據(jù)結(jié)構(gòu)到高可用架構(gòu)與緩存治理)
說(shuō)個(gè)實(shí)話Redis 是被低估得最厲害的一個(gè)中間件。很多人學(xué)了幾天會(huì)了SET和GET知道五種數(shù)據(jù)類型就以為自己入門(mén)了結(jié)果一到面試被問(wèn)“緩存穿透怎么解決”“主從切換會(huì)不會(huì)丟數(shù)據(jù)”或者一上線就被慢查詢、內(nèi)存暴漲、分布式鎖失效折騰到懷疑人生才發(fā)現(xiàn)自己根本沒(méi)摸到 Redis 的門(mén)。這篇筆記不是給你背面試題的是按我實(shí)際折騰 Redis 的路徑整理的從安裝選型、數(shù)據(jù)類型底層邏輯到高可用架構(gòu)、緩存治理、常見(jiàn)故障排查一次講透。內(nèi)容適用于用 Redis 做緩存、做分布式鎖、做排行榜、做消息隊(duì)列的開(kāi)發(fā)者和運(yùn)維同學(xué)也適合準(zhǔn)備大廠面試、想系統(tǒng)梳理 Redis 知識(shí)體系的人。我盡量用大白話講清楚每個(gè)關(guān)鍵選擇的“為什么”而不是只告訴你“怎么做”。1. 學(xué)習(xí)路線與整體設(shè)計(jì)思路拆解1.1 為什么你該把 Redis 當(dāng)“數(shù)據(jù)結(jié)構(gòu)服務(wù)器”來(lái)學(xué)很多教程開(kāi)場(chǎng)就說(shuō)“Redis 是高性能 KV 緩存數(shù)據(jù)庫(kù)”這個(gè)定位本身沒(méi)錯(cuò)但它直接把你的思路帶窄了。如果只把 Redis 當(dāng)緩存那你永遠(yuǎn)只會(huì)用它來(lái)存字符串遇到實(shí)際需求就想“這玩意兒是不是不適合”——排行榜用 ZSet、去重用 Set、消息隊(duì)列用 List 或 Stream、分布式 ID 用 INCR、附近的人用 GEO這些都不是“緩存”而是數(shù)據(jù)結(jié)構(gòu)能力。我建議把 Redis 理解成“一個(gè)跑在內(nèi)存里的數(shù)據(jù)結(jié)構(gòu)服務(wù)器”。它和 MySQL 最大的區(qū)別不是快而是它的數(shù)據(jù)模型本身就是為特定數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)的你往里面放 String、Hash、List、Set、ZSet它幫你維護(hù)增刪改查、排序、聚合、去重這些能力。學(xué) Redis 的第一原則就是拿到一個(gè)需求先判斷“這個(gè)數(shù)據(jù)結(jié)構(gòu)適不適合用 Redis 表達(dá)”而不是“能不能塞進(jìn)緩存里”。帶著這個(gè)思路學(xué)習(xí)路徑就清晰了先掌握命令和數(shù)據(jù)結(jié)構(gòu)再理解持久化和過(guò)期策略接著上高可用和集群最后回到緩存治理和分布式鎖這些真實(shí)場(chǎng)景。每一步都在解決前一步帶來(lái)的新問(wèn)題是一條完整的鏈路。1.2 按階段拆解基礎(chǔ)、進(jìn)階、實(shí)戰(zhàn)三步走第一階段是基礎(chǔ)目標(biāo)是“會(huì)用”。這個(gè)階段只需要搞定三件事安裝、五種數(shù)據(jù)類型、常用命令。你可以把 Redis 跑起來(lái)然后用redis-cli或者可視化工具比如 Another Redis Desktop Manager手動(dòng)敲命令把每種數(shù)據(jù)類型的增刪改查、過(guò)期時(shí)間、TTL 都過(guò)一遍。這一步不需要背命令大全先把最常用的三十個(gè)命令用到條件反射就行。第二階段是進(jìn)階目標(biāo)是“懂原理”。這個(gè)階段要搞清楚三塊內(nèi)容Redis 為什么快單線程 IO 多路復(fù)用、數(shù)據(jù)怎么不丟RDB 和 AOF、內(nèi)存不夠了怎么辦過(guò)期策略和淘汰策略。這三個(gè)問(wèn)題是面試高頻題也是你以后排查性能問(wèn)題和設(shè)計(jì)緩存方案的理論基礎(chǔ)。很多人在這個(gè)階段容易犯的錯(cuò)是只背概念不結(jié)合命令去驗(yàn)證。比如你背了“Redis 默認(rèn)有 16 個(gè)庫(kù)”但你不知道SELECT 1切庫(kù)之后AOF 重寫(xiě)、RDB 快照到底怎么處理多庫(kù)數(shù)據(jù)你背了“LRU 淘汰”但你不知道 Redis 的 LRU 是近似實(shí)現(xiàn)不是嚴(yán)格 LRU。第三階段是實(shí)戰(zhàn)目標(biāo)是“會(huì)排障”。這一階段要圍繞真實(shí)場(chǎng)景展開(kāi)主從怎么搭、哨兵解決了什么問(wèn)題、集群的槽位是怎么分配的、緩存穿透擊穿雪崩分別怎么應(yīng)對(duì)、分布式鎖怎么實(shí)現(xiàn)才不會(huì)出事故。這個(gè)階段的標(biāo)志是你開(kāi)始能看懂redis.log里的告警信息能根據(jù)INFO命令的輸出判斷系統(tǒng)健康度能在 Redis Desktop Manager 的連接失敗提示里快速定位問(wèn)題。我個(gè)人建議這個(gè)階段配合 Docker 來(lái)做實(shí)驗(yàn)因?yàn)橹鲝摹⑸诒?、集群的搭建?huì)反復(fù)摧毀重建用容器最省心。2. 環(huán)境準(zhǔn)備安裝、選型與可視化工具的坑2.1 Windows 安裝別去官網(wǎng)下“官方 Windows 版”先說(shuō)一個(gè)很多人踩過(guò)的坑Redis 官方是不支持 Windows 的官網(wǎng)只提供 Linux 源碼包。你在網(wǎng)上搜“redis windows 下載”搜到的那些.exe安裝包基本都是第三方移植版或者微軟舊版維護(hù)分支。不是說(shuō)不能用而是要選對(duì)來(lái)源。如果你只是本地學(xué)習(xí)我推薦最常見(jiàn)的方案去 Redis 的 Windows 移植倉(cāng)庫(kù)比如 tporadowski/redis下載 zip 包解壓后直接運(yùn)行redis-server.exe默認(rèn)端口 6379 就能起來(lái)redis-cli.exe就是命令行客戶端特別適合新手。注意兩個(gè)細(xì)節(jié)第一這個(gè)移植版大多停留在 Redis 5.x 或 6.x 版本學(xué)習(xí)和單機(jī)測(cè)試沒(méi)問(wèn)題但它和 Linux 官方版在高版本特性上會(huì)有差異生產(chǎn)環(huán)境千萬(wàn)別用第二啟動(dòng)后進(jìn)程會(huì)一直占著控制臺(tái)窗口關(guān)掉窗口 關(guān)掉服務(wù)學(xué)習(xí)階段無(wú)所謂但你最好知道有redis-server --service-install這種以 Windows 服務(wù)方式運(yùn)行的命令。Linux 上安裝是更正經(jīng)的做法。以 CentOS 和 Ubuntu 為例源碼編譯是標(biāo)準(zhǔn)的安裝路徑下載源碼包、make、make install。編譯之前記得裝好gcc和make否則會(huì)報(bào)一堆讓人頭大的編譯錯(cuò)誤。編譯安裝完Redis 的可執(zhí)行文件會(huì)放在/usr/local/bin配置文件在源碼目錄下的redis.conf。這里有個(gè)常見(jiàn)坑很多人直接./redis-server啟動(dòng)結(jié)果發(fā)現(xiàn)根本沒(méi)有配置文件生效所有的數(shù)據(jù)持久化和后臺(tái)運(yùn)行配置都失效了。正確姿勢(shì)是./redis-server /path/to/redis.conf顯式指定配置文件。2.2 Docker 方式學(xué)習(xí)高可用架構(gòu)最快的路徑如果你要學(xué)主從、哨兵、集群我強(qiáng)烈建議直接用 Docker。手寫(xiě)配置文件、在一臺(tái)機(jī)器上起多個(gè)實(shí)例不是不行但改端口、改 pid 文件、調(diào)日志路徑會(huì)消耗大量耐心。用 Docker 的話一條docker run命令就是一個(gè)獨(dú)立實(shí)例網(wǎng)絡(luò)隔離也是天然的。舉個(gè)例子搭建一主兩從的 Redis 主從結(jié)構(gòu)# 創(chuàng)建自定義網(wǎng)絡(luò)方便容器間通過(guò)容器名互相訪問(wèn) docker network create redis-net # 主節(jié)點(diǎn) docker run -d --name redis-master --network redis-net -p 6379:6379 redis:7.0 redis-server --appendonly yes # 從節(jié)點(diǎn)1 docker run -d --name redis-slave1 --network redis-net -p 6380:6379 redis:7.0 redis-server --appendonly yes --slaveof redis-master 6379 # 從節(jié)點(diǎn)2 docker run -d --name redis-slave2 --network redis-net -p 6381:6379 redis:7.0 redis-server --appendonly yes --slaveof redis-master 6379啟動(dòng)完成后進(jìn)入主節(jié)點(diǎn)容器執(zhí)行INFO replication你就能看到兩個(gè)從節(jié)點(diǎn)已經(jīng)連接上來(lái)。用 Docker 做這種實(shí)驗(yàn)的好處是環(huán)境干凈、銷毀容易、模擬故障特別方便。比如你直接docker stop redis-slave1就能觀察主從斷開(kāi)后主節(jié)點(diǎn)有沒(méi)有報(bào)錯(cuò)、從節(jié)點(diǎn)重連后的數(shù)據(jù)同步情況。注意Docker 方式雖然方便但你要分清兩個(gè)概念——--slaveof這是主從復(fù)制的配置不是集群的配置。主從復(fù)制解決的是“數(shù)據(jù)多副本冗余”集群解決的是“數(shù)據(jù)分片存儲(chǔ)”。很多人把這兩個(gè)搞混后面看集群文檔時(shí)一臉懵。2.3 可視化客戶端Redis Desktop Manager 的替代品Redis Desktop Manager簡(jiǎn)稱 RDM是搜索最多、知名度最高的 Redis 可視化客戶端但它的坑也不少。2022 年起新版 RDM 開(kāi)始收費(fèi)如果你搜“redis desktop manager 免費(fèi)版”或者“redis desktop manager 開(kāi)源舊版”能找到的是最后一個(gè)免費(fèi)開(kāi)源版本大概停在 2022 年 6 月左右。當(dāng)年很多人就在這個(gè)坎上栽了跟頭——下載安裝了新版一打開(kāi)就彈試用提醒還看不到 key 列表的完整功能。我實(shí)際用下來(lái)的推薦是如果只是日常學(xué)習(xí)直接用Another Redis Desktop Manager這是一個(gè)完全開(kāi)源免費(fèi)、持續(xù)維護(hù)的替代品。它的 UI 比舊版 RDM 更現(xiàn)代支持 SSH 隧道、支持可視化查看 stream、支持命令執(zhí)行的實(shí)時(shí)監(jiān)控對(duì)單機(jī)、集群、哨兵模式都有不錯(cuò)的支持。當(dāng)然如果你只有幾臺(tái)服務(wù)器、鍵也不多用命令行的redis-cli其實(shí)完全夠可視化客戶端的核心價(jià)值是讓你看清 key 的分布、內(nèi)存占用和過(guò)期時(shí)間排查問(wèn)題時(shí)特別直觀。3. 五大數(shù)據(jù)類型與底層實(shí)現(xiàn)邏輯3.1 String不只是字符串的三層編碼String 是一個(gè)繞不開(kāi)的基礎(chǔ)但你真不一定了解它。Redis 的 String 除了存文本還能存數(shù)字配合 INCR/DECR 做計(jì)數(shù)器、存二進(jìn)制數(shù)據(jù)比如圖片的字節(jié)流但容易讓你翻車的點(diǎn)在于它的內(nèi)存編碼。Redis 對(duì) String 做了三種內(nèi)部編碼int純數(shù)字、embstr短字符串、raw長(zhǎng)字符串。前兩種字節(jié)數(shù)少存儲(chǔ)緊湊超過(guò) 44 字節(jié)會(huì)自動(dòng)轉(zhuǎn)成raw。這個(gè) 44 字節(jié)不是隨便定的它和 Redis 內(nèi)部redisObject結(jié)構(gòu)的對(duì)齊、內(nèi)存分配器的分配粒度有關(guān)你不需要死記但你得理解Redis 對(duì)短字符串做了極致優(yōu)化所以設(shè)計(jì) key 時(shí)別弄?jiǎng)虞m幾百字節(jié)的 value除非業(yè)務(wù)必須。實(shí)際測(cè)試下來(lái)一個(gè)幾百字節(jié)的 value 比短字符串多占 3~5 倍內(nèi)存這在千萬(wàn)級(jí) key 的緩存里就是幾十 GB 的差距。String 最常見(jiàn)的生產(chǎn)場(chǎng)景有三個(gè)緩存、計(jì)數(shù)器、分布式 ID。計(jì)數(shù)器用INCR時(shí)要留心INCR不準(zhǔn)的坑——這里說(shuō)的“不準(zhǔn)”不是 Redis 算錯(cuò)而是并發(fā)下的計(jì)數(shù)語(yǔ)義問(wèn)題。如果你用INCR做庫(kù)存扣減命令本身是原子的沒(méi)問(wèn)題但如果你先GET再SET這就是經(jīng)典的讀改寫(xiě)競(jìng)態(tài)丟更新就是必然結(jié)果。類似“瀏覽量 1”這種業(yè)務(wù)用INCR沒(méi)問(wèn)題但“基于當(dāng)前值做復(fù)雜計(jì)算再寫(xiě)回”就必須用 Lua 腳本或者加鎖。3.2 Hash對(duì)象類數(shù)據(jù)的“常規(guī)武器”和 Redis 的“乞丐版分片”Hash 在業(yè)務(wù)里最典型的應(yīng)用是存儲(chǔ)對(duì)象。比如一個(gè)用戶信息最笨的存法是SET user:1001 {name:tom,age:18}問(wèn)題是改一個(gè)字段就要整體覆蓋整個(gè) JSON浪費(fèi)網(wǎng)絡(luò)和內(nèi)存。用 Hash 存就變成了HSET user:1001 name tom age 18改哪個(gè)字段就只更新哪個(gè)字段還能單獨(dú)對(duì)字段設(shè)置過(guò)期時(shí)間嗎不行Hash 本身沒(méi)有字段級(jí)別的 TTL這是它和 String 的另一個(gè)取舍。Hash 內(nèi)部編碼有兩種ziplist和hashtable。當(dāng)字段數(shù)量少、字段值小的時(shí)候用ziplist緊湊存儲(chǔ)字段多了或者值大了自動(dòng)轉(zhuǎn)成hashtable。這里有一個(gè)用 Hash 做“小分片”的思路當(dāng)單個(gè) key 的 value 過(guò)大比如一個(gè)大 List 存了百萬(wàn)條記錄會(huì)導(dǎo)致讀寫(xiě)這個(gè) key 的耗時(shí)增加這在 Redis 里叫“大 key 問(wèn)題”。解決方案之一就是把一個(gè)大集合拆成多個(gè)小 Hash。舉例來(lái)說(shuō)如果你要存一個(gè)用戶的行為記錄一天幾十條不要用SET user_actions:1001 大JSON而是按天分片HINCRBY user_actions:20250120:1001 type_click 1這樣每個(gè) key 只負(fù)責(zé)一天的增量數(shù)據(jù)。這個(gè)思路在任何需要給單個(gè) key 瘦身的場(chǎng)景都通用理解它能幫你解決一大批“Redis 查詢?cè)絹?lái)越慢”的問(wèn)題。3.3 List 和 Stream隊(duì)列功能到底用誰(shuí)List 的LPUSHRPOP組合是最簡(jiǎn)單的生產(chǎn)者消費(fèi)者模式配合BLPOP還能實(shí)現(xiàn)阻塞讀很多輕量級(jí)消息隊(duì)列就是用這個(gè)做的。但 List 做隊(duì)列有天然的毛病消息丟失沒(méi)有重試機(jī)制沒(méi)有消費(fèi)確認(rèn)消費(fèi)者崩了就永遠(yuǎn)丟了而且LRANGE翻頁(yè)取數(shù)據(jù)雖然方便但對(duì)長(zhǎng)列表是 O(N) 操作列表一長(zhǎng)就拖垮性能。所以 Redis 5.0 之后引入了 Stream它的設(shè)計(jì)目標(biāo)就是正兒八經(jīng)的消息隊(duì)列支持消費(fèi)者組、支持消息確認(rèn)ACK、支持 PEL待處理列表、支持持久化。如果你在做項(xiàng)目時(shí)需要一個(gè)“能用但不想引入 Kafka/RabbitMQ”的輕量隊(duì)列Stream 幾乎是唯一正解。我自己的建議是隊(duì)列非常簡(jiǎn)單的場(chǎng)景比如事件通知丟了也無(wú)所謂可以用 List要求消息不丟、要消費(fèi)組、要重試用 Stream。面試問(wèn)到“Redis 做消息隊(duì)列”你要能說(shuō)出這個(gè)演進(jìn)關(guān)系比單純背命令值錢(qián)多了。3.4 Set 和 ZSet從去重到排行榜Set 用來(lái)做去重和集合運(yùn)算交集、并集、差集典型場(chǎng)景是抽獎(jiǎng)、標(biāo)簽系統(tǒng)、共同好友。ZSet 比 Set 多了分值排序底層是跳躍表 哈希表跳躍表的時(shí)間復(fù)雜度是 O(log N)所以 ZSet 在百萬(wàn)級(jí)數(shù)據(jù)量下的排序操作依然很快。排行榜、實(shí)時(shí)熱點(diǎn)、延時(shí)隊(duì)列都是 ZSet 的主場(chǎng)。我要提醒一個(gè) ZSet 的坑同樣是ZADD如果你在寫(xiě)入時(shí)不控制 member 的規(guī)模一個(gè) ZSet 里有幾百萬(wàn)個(gè) member后續(xù)的ZRANGE、ZREM雖然單次很快但內(nèi)存占用會(huì)非常嚇人。ZSet 的成員越多跳躍表的指針開(kāi)銷越大單個(gè) member 的額外開(kāi)銷可能有幾十字節(jié)。所以設(shè)計(jì)上要注意排行榜類數(shù)據(jù)要么只保留 Top N用ZREMRANGEBYRANK定期清理要么按業(yè)務(wù)維度拆 key比如按天拆排行榜千萬(wàn)別讓一個(gè) ZSet 無(wú)限增長(zhǎng)。4. 高可用架構(gòu)主從、哨兵與集群4.1 主從復(fù)制REPLICAOF 背后的增量與全量同步主從復(fù)制解決的是“單點(diǎn)故障”問(wèn)題。主節(jié)點(diǎn)可讀可寫(xiě)從節(jié)點(diǎn)默認(rèn)只讀主節(jié)點(diǎn)把寫(xiě)操作同步給從節(jié)點(diǎn)。同步分兩種情況全量同步和增量同步。全量同步發(fā)生在從節(jié)點(diǎn)剛連接主節(jié)點(diǎn)、或者主從數(shù)據(jù)差距過(guò)大時(shí)。流程是從節(jié)點(diǎn)發(fā)送PSYNC命令主節(jié)點(diǎn)執(zhí)行BGSAVE生成 RDB 快照發(fā)給從節(jié)點(diǎn)從節(jié)點(diǎn)清空舊數(shù)據(jù)并加載快照然后主節(jié)點(diǎn)把快照生成期間新增的寫(xiě)命令存在復(fù)制緩沖區(qū)里再發(fā)給從節(jié)點(diǎn)。增量同步靠的是主從之間維護(hù)的復(fù)制偏移量如果偏移量還在緩沖區(qū)范圍內(nèi)只需要同步差額部分如果從節(jié)點(diǎn)斷連太久緩沖區(qū)里的數(shù)據(jù)已經(jīng)覆蓋了就得重新全量同步。這里有一個(gè)隱藏很深的坑如果從節(jié)點(diǎn)斷連時(shí)間很短但主節(jié)點(diǎn)的復(fù)制緩沖區(qū)太小默認(rèn) 1MB可通過(guò)client-output-buffer-limit replica調(diào)大也會(huì)觸發(fā)全量同步。全量同步一旦頻繁發(fā)生主節(jié)點(diǎn)要反復(fù) BGSAVE 傳輸數(shù)據(jù)CPU 和網(wǎng)絡(luò)都會(huì)被打滿導(dǎo)致主節(jié)點(diǎn)阻塞。生產(chǎn)環(huán)境出現(xiàn)“主從反復(fù)全量同步”的情況第一件事就是去看INFO replication里的偏移量差值和緩沖區(qū)大小。4.2 哨兵自動(dòng)故障轉(zhuǎn)移的“哨兵進(jìn)程”主從復(fù)制解決了數(shù)據(jù)冗余但主節(jié)點(diǎn)掛了從節(jié)點(diǎn)不會(huì)自動(dòng)頂上需要人工切換。哨兵Sentinel就是來(lái)解決這個(gè)問(wèn)題的它是一個(gè)獨(dú)立進(jìn)程負(fù)責(zé)監(jiān)控主從節(jié)點(diǎn)的健康狀態(tài)在主節(jié)點(diǎn)掛掉后自動(dòng)從從節(jié)點(diǎn)中選舉出一個(gè)新主節(jié)點(diǎn)并把其他從節(jié)點(diǎn)重新指向新主節(jié)點(diǎn)。但哨兵不是萬(wàn)能的。它最被人質(zhì)疑的點(diǎn)是哨兵本身就是單點(diǎn)雖然可以部署哨兵集群而且故障轉(zhuǎn)移期間存在“腦裂”窗口。經(jīng)典場(chǎng)景主節(jié)點(diǎn)和哨兵之間的網(wǎng)絡(luò)發(fā)生分區(qū)哨兵認(rèn)為主節(jié)點(diǎn)掛了選舉出新的主節(jié)點(diǎn)但原主節(jié)點(diǎn)其實(shí)還活著還能接收寫(xiě)入等網(wǎng)絡(luò)恢復(fù)后原主節(jié)點(diǎn)的數(shù)據(jù)就和新主節(jié)點(diǎn)不一致了這部分丟失的數(shù)據(jù)無(wú)法找回。所以涉及錢(qián)的業(yè)務(wù)比如扣庫(kù)存、交易流水不能只靠 Redis 主從 哨兵保證強(qiáng)一致這是架構(gòu)層面的問(wèn)題。用 Docker 搭建哨兵的時(shí)候最容易被坑的是配置文件里的sentinel monitor master-name host port quorum這個(gè)host不能寫(xiě)127.0.0.1因?yàn)樯诒枰ㄖ捌渌鄙诒汀捌渌睆墓?jié)點(diǎn)連接主節(jié)點(diǎn)。在容器環(huán)境里主機(jī)地址要寫(xiě)容器網(wǎng)絡(luò)中可達(dá)的別名的地址比如redis-master。這個(gè)細(xì)節(jié)我當(dāng)年調(diào)試了一下午白屏的時(shí)間都浪費(fèi)在地址可達(dá)性上。4.3 集群數(shù)據(jù)分片的槽位分配以及為什么集群也有“一票否決”Redis Cluster 和主從哨兵是兩套不同的體系。Cluster 把整個(gè)數(shù)據(jù)空間分成 16384 個(gè)槽位每個(gè)節(jié)點(diǎn)負(fù)責(zé)一部分槽位key 通過(guò)CRC16(key) % 16384計(jì)算屬于哪個(gè)槽。寫(xiě)請(qǐng)求如果落到了非負(fù)責(zé)節(jié)點(diǎn)節(jié)點(diǎn)會(huì)返回MOVED錯(cuò)誤客戶端要跟著重定向。這是 Cluster 和單機(jī)模式最大的體驗(yàn)差異——你的客戶端必須支持集群協(xié)議比如 Jedis 的JedisCluster、Lettuce 的集群模式否則會(huì)出現(xiàn)各種莫名其妙的連接和寫(xiě)入失敗。Cluster 本身自帶主從冗余每個(gè)主節(jié)點(diǎn)可以掛一個(gè)或多個(gè)從節(jié)點(diǎn)主掛了從自動(dòng)頂上。但要注意Cluster 的可用性判定比哨兵更嚴(yán)格如果一個(gè)主節(jié)點(diǎn)和它的所有從節(jié)點(diǎn)同時(shí)掛掉整個(gè)集群就會(huì)進(jìn)入不可用狀態(tài)即使其他槽位的主節(jié)點(diǎn)都活著。這就是少數(shù)派不可訪問(wèn)導(dǎo)致集群不可用的經(jīng)典場(chǎng)景。查詢操作可以通過(guò)CLUSTER SLOTS命令看清楚每個(gè)槽位的負(fù)責(zé)節(jié)點(diǎn)這個(gè)命令排障時(shí)非常好使。Docker 部署集群有幾個(gè)注意點(diǎn)端口映射不能只映射 6379因?yàn)榧和ㄐ乓?16379總線端口不映射會(huì)造成節(jié)點(diǎn)間通信失敗容器重啟后 IP 變化會(huì)讓集群變的不可用解決辦法是給容器設(shè)置固定 IP--ip或使用 Docker Swarm / Kubernetes 的穩(wěn)定 DNS 名稱。這些問(wèn)題在本地實(shí)驗(yàn)時(shí)不明顯但如果你用 KubeSphere 之類的平臺(tái)去搭建 Redis 集群就會(huì)頻繁遇到“集群狀態(tài)正常但客戶端連不上”的怪事排查到最后基本都是網(wǎng)絡(luò)標(biāo)識(shí)變動(dòng)引起的。4.4 從“會(huì)搭”到“會(huì)調(diào)”配置參數(shù)背后的取舍搭建完高可用架構(gòu)你才能真正讀懂配置文件里的各種參數(shù)。比如maxmemory和maxmemory-policy決定了內(nèi)存耗盡后的行為——allkeys-lru是全局近似 LRU 淘汰volatile-ttl只對(duì)設(shè)置了過(guò)期時(shí)間的 key 淘汰。再比如appendonly yes開(kāi)啟 AOF 后appendfsync everysec表示每秒刷盤(pán)一次這是性能和可靠性的折中方案always最安全但性能下降明顯no交給操作系統(tǒng)決定但可能丟失一秒鐘的數(shù)據(jù)。關(guān)于 AOF 和 RDB我的建議是生產(chǎn)環(huán)境兩者都開(kāi)RDB 做快速恢復(fù)的冷備AOF 做災(zāi)備的精確日志。只開(kāi) AOF 的坑是AOF 文件會(huì)越來(lái)越大雖然 rewrite 能瘦身但如果宕機(jī)時(shí) AOF 文件有損壞Redis 可能啟動(dòng)失敗需要先運(yùn)行redis-check-aof --fix修復(fù)。只開(kāi) RDB 的坑更明顯兩次快照之間的數(shù)據(jù)全部丟失。我遇到過(guò)把 RDB 快照間隔設(shè)成 900 秒的項(xiàng)目一次斷電丟了十五分鐘緩存雖然后臺(tái)有 MySQL 兜底但那個(gè)時(shí)間段的請(qǐng)求壓力直接把 DB 打崩了這個(gè)教訓(xùn)說(shuō)明持久化參數(shù)不是隨便填的。5. 緩存治理穿透、擊穿、雪崩與分布式鎖5.1 緩存三大災(zāi)難的應(yīng)對(duì)不是靠 Redis 本身緩存穿透、擊穿、雪崩這三個(gè)問(wèn)題面試必問(wèn)但很多人回答得過(guò)于套路化。我按自己的經(jīng)驗(yàn)說(shuō)說(shuō)區(qū)別和處理優(yōu)先級(jí)。緩存穿透是查一個(gè)根本不存在的數(shù)據(jù)緩存里沒(méi)有數(shù)據(jù)庫(kù)里也沒(méi)有請(qǐng)求直接打到 DB。對(duì) Redis 而言它根本沒(méi)壓力壞的是 DB。常見(jiàn)的解法有三種緩存空值設(shè)置一個(gè)較短的過(guò)期時(shí)間比如 5 分鐘、布隆過(guò)濾器一種空間效率很高的概率型數(shù)據(jù)結(jié)構(gòu)不存在則直接短路、參數(shù)校驗(yàn)攔截非法請(qǐng)求。我個(gè)人覺(jué)得業(yè)務(wù)層參數(shù)校驗(yàn)是最先該做的布隆過(guò)濾器適合 key 基數(shù)大的場(chǎng)景空值緩存適合 key 容易被窮舉暴力的場(chǎng)景。緩存擊穿是某一個(gè)熱點(diǎn) key 過(guò)期瞬間大量請(qǐng)求直接打到 DB。注意是“單個(gè) key”不是“一批 key”。應(yīng)對(duì)的核心思路是“不讓 DB 扛流量”一是互斥鎖只有一個(gè)線程去查 DB 重建緩存其它線程等待或返回默認(rèn)值二是邏輯過(guò)期不讓 key 實(shí)際過(guò)期而是在 value 里塞一個(gè)過(guò)期時(shí)間戳后臺(tái)異步刷新適合對(duì)一致性要求不高的熱點(diǎn)數(shù)據(jù)。緩存雪崩是大量 key 同時(shí)過(guò)期或者 Redis 實(shí)例宕機(jī)導(dǎo)致流量直撲 DB。處理手法過(guò)期時(shí)間加隨機(jī)值比如SETEX key (base random(0,300))、熱點(diǎn) key 預(yù)熱、多級(jí)緩存兜底本地 Caffeine 再擋一層。在 Redis 本身宕機(jī)時(shí)多級(jí)緩存幾乎是唯一能在 Redis 恢復(fù)前保護(hù) DB 的手段否則你再怎么設(shè)計(jì)過(guò)期時(shí)間也擋不住洪水。5.2 分布式鎖SET NX EX 和 Lua 缺一不可說(shuō)到分布式鎖最常見(jiàn)的實(shí)現(xiàn)是基于SET key value NX EX seconds。注意這個(gè)命令必須是一條命令完成不能拆成SETNX再EXPIRE因?yàn)閮蓷l命令之間有間隙進(jìn)程崩了鎖就沒(méi)過(guò)期時(shí)間了死鎖就是這么來(lái)的。然后是釋放鎖很多人寫(xiě)成if (redis.call(GET, KEYS[1]) ARGV[1]) then return redis.call(DEL, KEYS[1]) else return 0 end為什么要用 Lua因?yàn)椤芭袛嘀凳欠袷亲约旱摹焙汀皠h除 key”是兩個(gè)操作拆開(kāi)執(zhí)行中間鎖過(guò)期了別的線程拿到了鎖然后你把這個(gè)別人的鎖刪了。這個(gè) bug 在并發(fā)稍高的場(chǎng)景下必現(xiàn)。用 Lua 把兩步合并成一個(gè)原子操作這是分布式鎖最底層的底線。還有兩個(gè)進(jìn)階問(wèn)題要心里有數(shù)。第一個(gè)是“鎖過(guò)期時(shí)間怎么設(shè)”業(yè)務(wù)執(zhí)行時(shí)間可能超過(guò)鎖的過(guò)期時(shí)間導(dǎo)致鎖被提前釋放解決辦法是“看門(mén)狗”機(jī)制——在持有鎖期間定期續(xù)期但這是 Redisson 這些客戶端庫(kù)實(shí)現(xiàn)的能力自己裸寫(xiě)要慎重。第二個(gè)是“主從切換鎖丟失”的問(wèn)題主節(jié)點(diǎn)拿到了鎖但數(shù)據(jù)還沒(méi)同步到從節(jié)點(diǎn)主節(jié)點(diǎn)掛了從節(jié)點(diǎn)頂上后鎖就沒(méi)了。Redisson 的 RedLock 試圖解決這個(gè)問(wèn)題但代價(jià)是復(fù)雜度陡增而且在大規(guī)模部署下正確性爭(zhēng)議一直存在。我的建議是能用數(shù)據(jù)庫(kù)唯一約束解決的業(yè)務(wù)就別用 Redis 分布式鎖用了就要接受它的“在極端故障下有鎖丟失風(fēng)險(xiǎn)”這個(gè)前提。5.3 序列化Redis 客戶端里最容易被忽視的性能瓶頸Java 用 Redis 時(shí)序列化方式直接影響你能存什么、占多少內(nèi)存。最常用的兩個(gè)StringRedisTemplate是純字符串讀寫(xiě)存 Java 對(duì)象時(shí)你得自己把對(duì)象轉(zhuǎn)成 JSONRedisTemplateGenericJackson2JsonRedisSerializer可以自動(dòng)把對(duì)象序列化成 JSON 存入取出來(lái)時(shí)反序列化回對(duì)象非常方便但代價(jià)是存儲(chǔ)的內(nèi)容里會(huì)帶一段class類型信息占用并不大但是讀出來(lái)的數(shù)據(jù)肉眼看起來(lái)也不是那么干凈。序列化還有一個(gè)隱藏痛點(diǎn)默認(rèn)的JdkSerializationRedisSerializerSpring Data Redis 默認(rèn)配置如果沒(méi)做任何定制是 Java 原生序列化二進(jìn)制格式占用空間大而且存進(jìn)去的東西你在可視化工具里完全看不懂全是\xAC\xED開(kāi)頭的一堆亂碼。我強(qiáng)烈建議任何項(xiàng)目都顯式配置 JSON 序列化方式除非你能忍受排查問(wèn)題時(shí)對(duì)著亂碼猜內(nèi)容。Python 連接 Redis 也有類似問(wèn)題用pickle序列化存進(jìn)去的數(shù)據(jù)別指望redis-cli能人類可讀??缯Z(yǔ)言讀寫(xiě)同一個(gè) key 時(shí)一定要統(tǒng)一序列化方案否則等著你的就是亂碼或者反序列化異常。5.4 Lua 腳本不是炫技是原子性的救命稻草Redis 的 Lua 腳本能力很多初學(xué)者不重視覺(jué)得“我是業(yè)務(wù)開(kāi)發(fā)用不上 Lua”。但實(shí)際上只要你的操作需要“讀當(dāng)前值 - 計(jì)算 - 寫(xiě)回”這樣的組合Lua 就是最優(yōu)雅的原子性保證。前面說(shuō)的分布式鎖釋放邏輯只是一個(gè)例子還有“限流器”INCR后判斷當(dāng)前計(jì)數(shù)是否超限超過(guò)則返回失敗整個(gè)過(guò)程塞進(jìn) Lua 里執(zhí)行就沒(méi)有并發(fā)下超發(fā)的問(wèn)題。Lua 在 Spring 中通常用DefaultRedisScript配合execute調(diào)用在 Python 里是redis.register_script。用 Lua 時(shí)有幾個(gè)細(xì)節(jié)容易踩坑第一Lua 腳本里要訪問(wèn) Redis key 必須通過(guò)KEYS[]數(shù)組傳入不能寫(xiě)在腳本里拼字符串否則 Redis 無(wú)法預(yù)分配槽位第二腳本執(zhí)行期間會(huì)阻塞 Redis千萬(wàn)別寫(xiě)死循環(huán)或者毫秒級(jí)都算慢的重計(jì)算第三腳本的“確定性”要求它不能在執(zhí)行過(guò)程中讀取自身的寫(xiě)入結(jié)果在集群模式下會(huì)直接報(bào)錯(cuò)所以你在集群環(huán)境用 Lua 時(shí)要特別克制盡量只對(duì)單個(gè) key 做操作。6. 從學(xué)習(xí)到面試高頻題、監(jiān)控日志和排障手冊(cè)6.1 高頻面試題我建議你追根溯源別只背答案翻一翻網(wǎng)上流傳的“Redis 面試題”問(wèn)得最多的是這些Redis 為什么快、緩存穿透和擊穿怎么解決、分布式鎖怎么實(shí)現(xiàn)、持久化怎么選、主從和集群的腦裂問(wèn)題。這些問(wèn)題并不難難的是你說(shuō)答案的時(shí)候有沒(méi)有“現(xiàn)場(chǎng)感”。比如“Redis 為什么快”標(biāo)準(zhǔn)答案是“單線程避免上下文切換 基于 IO 多路復(fù)用的事件循環(huán) 純內(nèi)存操作”但你要是再往深走一步“單線程為什么還能這么快IO 多路復(fù)用到底是什么”很多人的回答就開(kāi)始含糊了。我自己的理解是Redis 把每個(gè)連接封裝成一個(gè)文件事件交給內(nèi)核去監(jiān)聽(tīng)讀寫(xiě)事件有事件到達(dá)才觸發(fā)回調(diào)處理這本質(zhì)上和 Nginx 的事件驅(qū)動(dòng)模型類似是把“等待 IO”這件事從線程里解放出去了。你用“餐廳只有一個(gè)服務(wù)員但門(mén)口的鈴一響服務(wù)員才出去接待而不是坐在門(mén)口死等每個(gè)客人”這個(gè)類比就可以跟小白講清楚。再比如“Kubernetes 上能不能搭 Redis”這其實(shí)是一個(gè)信號(hào)題對(duì)方想看你有沒(méi)有意識(shí)到在云原生場(chǎng)景下Redis 這類有狀態(tài)應(yīng)用對(duì)網(wǎng)絡(luò)標(biāo)識(shí)Pod IP、節(jié)點(diǎn)名的依賴很強(qiáng)StatefulSet 是唯一合理的選擇。如果你能說(shuō)出 StatefulSet 的穩(wěn)定網(wǎng)絡(luò)標(biāo)識(shí)和 Redis Cluster 的槽位分配之間是什么關(guān)系面試官對(duì)你的印象會(huì)立刻不一樣。6.2 日志、監(jiān)控和常見(jiàn)錯(cuò)誤每一個(gè)報(bào)錯(cuò)都別放過(guò)排查 Redis 問(wèn)題的第一步是看日志。Docker 部署的實(shí)例用docker logs 容器名Linux 直接看日志文件的路徑在 redis.conf 里的logfile配置項(xiàng)。我最常關(guān)注的日志信息有幾類Background saving terminated by signal表示 BGSAVE 失敗通常和內(nèi)存或文件系統(tǒng)有關(guān)Possible nokey和Cant persist這類表示 RDB/AOF 寫(xiě)入出問(wèn)題要優(yōu)先檢查磁盤(pán)空間df -hMISCONF Redis is configured to save RDB snapshots, but its currently unable to persist on disk這個(gè)經(jīng)典的只讀錯(cuò)誤就是磁盤(pán)寫(xiě)不進(jìn)去導(dǎo)致的。監(jiān)控指標(biāo)要盯四個(gè)connected_clients連接數(shù)used_memory內(nèi)存占用keyspace_hits和keyspace_misses算命中率命中率長(zhǎng)期低于 80% 說(shuō)明緩存設(shè)計(jì)有問(wèn)題rdb_last_bgsave_status和aof_last_write_status持久化狀態(tài)。用redis-cli INFO stats | grep keyspace可以快速查命中率用SLOWLOG GET可以看慢查詢結(jié)合實(shí)際業(yè)務(wù)分析是查還是不查??梢暬ぞ哌B接不上的排查路徑也很有套路基本按這個(gè)順序來(lái)第一步ping通不通第二步 telnet 6379 通不通第三步看服務(wù)端有沒(méi)有設(shè)置bind 127.0.0.1很多默認(rèn)配置只監(jiān)聽(tīng)本機(jī)外部當(dāng)然連不上第四步看有沒(méi)有requirepass認(rèn)證第五步看云廠商安全組/防火墻有沒(méi)有放行 6379。八成問(wèn)題都出在bind和防火墻這兩項(xiàng)。6.3 壓測(cè)、優(yōu)化和一把“性能底牌”最后聊聊性能Redis 本身吞吐很高但別忽略“客戶端導(dǎo)致的問(wèn)題”。在項(xiàng)目中用了KEYS *這種 O(N) 命令在阻塞事件循環(huán)的同時(shí)它可能讓整個(gè) Redis 卡頓到毫秒級(jí)超時(shí)。用SMEMBERS拉取一個(gè)巨大 Set 的全部成員它會(huì)撐爆網(wǎng)絡(luò)和內(nèi)存。這些替換方案都是現(xiàn)成的SCAN分批遍歷 keySSCAN遍歷 SetLRANGE注意限制數(shù)量。養(yǎng)成“避免 O(N) 命令”的習(xí)慣是所有 Redis 使用者必須過(guò)的一道坎。壓測(cè)工具的選擇上redis-benchmark是官方自帶的最簡(jiǎn)方案但它模擬不了復(fù)雜業(yè)務(wù)流量真要壓真實(shí)場(chǎng)景用redis-cli --latency看延遲分布、用INFO commandstats看每條命令的執(zhí)行次數(shù)和耗時(shí)占比比盲目壓 QPS 更有參考價(jià)值。我見(jiàn)過(guò)很多團(tuán)隊(duì)“壓測(cè) QPS 達(dá)標(biāo)”但線上一緊張就告警觀察下來(lái)基本都是沒(méi)看延遲分布、沒(méi)看命令耗時(shí)導(dǎo)致的性能優(yōu)化要看“哪個(gè)命令最慢”而不是“系統(tǒng)最高支持多少 QPS”。我個(gè)人在實(shí)際操作中的體會(huì)是Redis 不是一個(gè)“學(xué)完就完了”的東西它和業(yè)務(wù)形態(tài)綁定得很深。同一個(gè)命令在緩存場(chǎng)景、計(jì)數(shù)場(chǎng)景、隊(duì)列場(chǎng)景中的語(yǔ)義和坑完全不一樣。你學(xué)它的方式?jīng)Q定了你后來(lái)踩坑的多少——如果只是背命令你會(huì)覺(jué)得它很簡(jiǎn)單如果你動(dòng)手搭過(guò)集群、看過(guò)日志、壓過(guò)延遲、親手修過(guò)緩存穿透你才會(huì)真的理解“高性能中間件”這幾個(gè)字背后藏著多少容易被忽略的工程細(xì)節(jié)。這篇筆記是我自己踩坑過(guò)程的復(fù)盤(pán)寫(xiě)出來(lái)算是給后來(lái)者省點(diǎn)時(shí)間尤其是那些第一次在項(xiàng)目里引入 Redis 的朋友愿你少走幾步彎路。