自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

線上 Redis 頻繁崩潰?這套大 key 治理方案請(qǐng)收好

數(shù)據(jù)庫 Redis
現(xiàn)在稍微大點(diǎn)的項(xiàng)目都用 Redis 集群,假設(shè)你用的是分片集群(比如 Codis、Redis Cluster),一個(gè)大 key 會(huì)被固定分配到某個(gè)分片上。想象一下,其他分片內(nèi)存使用率才50%,就這個(gè)分片像吹氣球一樣漲到90%,整個(gè)集群的負(fù)載均衡瞬間失效。

兄弟們,凌晨兩點(diǎn),手機(jī)突然像地震一樣狂震,我迷迷糊糊摸到床頭一看,運(yùn)維群里炸了鍋:"Redis節(jié)點(diǎn)又掛了!內(nèi)存使用率飆到99%,CPU直接打滿!" 頂著黑眼圈爬起來連服務(wù)器,剛登錄就看到熟悉的報(bào)錯(cuò):OOM killer 又把 Redis 進(jìn)程干掉了。

那一刻我真想把寫代碼時(shí)隨手往 Redis 里塞大集合的同事拎過來——咱就是說,存數(shù)據(jù)能不能別跟往麻袋里裝磚頭似的,可勁兒造??!

一、先搞明白:啥是 Redis 大 key?它憑啥能搞崩服務(wù)器?

很多新手可能還不清楚,所謂"大 key"其實(shí)分兩種情況:一種是單個(gè) key 的值特別大(比如一個(gè)字符串類型的值超過1MB),另一種是集合類數(shù)據(jù)結(jié)構(gòu)(像 hash、list、set、zset)里的元素?cái)?shù)量超多(比如一個(gè) zset 存了10萬+成員)。別小看這些大塊頭,它們就像藏在 Redis 里的定時(shí)炸彈,主要靠這三招搞破壞:

1. 內(nèi)存分布不均勻,分片集群秒變"單腿跳"

現(xiàn)在稍微大點(diǎn)的項(xiàng)目都用 Redis 集群,假設(shè)你用的是分片集群(比如 Codis、Redis Cluster),一個(gè)大 key 會(huì)被固定分配到某個(gè)分片上。想象一下,其他分片內(nèi)存使用率才50%,就這個(gè)分片像吹氣球一樣漲到90%,整個(gè)集群的負(fù)載均衡瞬間失效。更要命的是,當(dāng)你要?jiǎng)h除這個(gè)大 key 時(shí),分片節(jié)點(diǎn)會(huì)經(jīng)歷一段漫長的"卡頓期",因?yàn)閯h除操作需要釋放大量連續(xù)內(nèi)存,堪比在市中心拆除一棟摩天大樓,周圍的交通都得跟著堵。

2. 網(wǎng)絡(luò)IO成瓶頸,批量操作直接"卡脖子"

舉個(gè)真實(shí)的例子:之前有個(gè)兄弟在項(xiàng)目里用 list 存用戶的歷史操作記錄,一個(gè) key 存了50萬條數(shù)據(jù)。某天運(yùn)營要導(dǎo)出用戶數(shù)據(jù),直接用 LRANGE key 0 -1 撈數(shù)據(jù),結(jié)果 Redis 所在服務(wù)器的網(wǎng)卡流量直接飆到峰值,應(yīng)用服務(wù)器這邊等了10秒都沒拿到響應(yīng)。為啥?因?yàn)?Redis 是單線程模型,處理這種大集合操作時(shí),會(huì)把所有元素序列化后通過網(wǎng)絡(luò)傳輸,就像用一根水管同時(shí)給100戶人家供水,水壓自然上不去。

3. 內(nèi)存碎片瘋狂增長,好好的內(nèi)存變成"碎紙片"

Redis 采用jemalloc分配內(nèi)存,當(dāng)大 key 被頻繁刪除和寫入時(shí),會(huì)產(chǎn)生大量無法利用的小碎片。比如你先存了一個(gè)10MB的大字符串,然后刪除,再存一堆1KB的小字符串,jemalloc 沒辦法把這些小碎片合并成大的連續(xù)內(nèi)存,導(dǎo)致實(shí)際內(nèi)存使用率比 INFO memory 里看到的 used_memory 高很多。曾經(jīng)見過一個(gè)線上節(jié)點(diǎn),used_memory 顯示8GB,但物理內(nèi)存已經(jīng)用了12GB,就是被碎片坑的。

二、檢測(cè)大 key:別等崩潰了才后悔,提前掃描是王道

1. 最簡單的命令:redis-cli --bigkeys

這個(gè)命令是 Redis 自帶的大 key 掃描工具,原理是對(duì)每個(gè)數(shù)據(jù)庫的不同數(shù)據(jù)類型做抽樣檢查。比如檢查 string 類型時(shí),會(huì)隨機(jī)選一些 key 用 STRLEN 查看長度;檢查集合類型時(shí),用 HLEN、LLEN、SCARD、ZCOUNT 統(tǒng)計(jì)元素?cái)?shù)量。注意要加 -i 0.1 參數(shù),這表示每次掃描間隔0.1秒,避免阻塞主線程。不過它有個(gè)缺點(diǎn):只能告訴你每個(gè)數(shù)據(jù)類型的最大 key 是誰,沒辦法掃描所有大 key,適合做初步排查。

# 掃描所有數(shù)據(jù)庫,每隔0.1秒掃描一次
redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.1

2. 更精準(zhǔn)的方案:自己寫掃描工具(附Python代碼)

如果需要全量掃描,就得用 SCAN 命令代替 KEYS *,因?yàn)?nbsp;KEYS 會(huì)阻塞主線程,在生產(chǎn)環(huán)境用就是"自殺行為"。下面這段 Python 代碼可以掃描指定前綴的大 key,支持設(shè)置字符串長度閾值和集合元素?cái)?shù)量閾值:

import redis
def scan_big_keys(redis_client, prefix, str_threshold=1024*1024, collection_threshold=10000):
   big_keys = []
   cursor = '0'
   while cursor != 0:
       cursor, keys = redis_client.scan(cursor=cursor, match=prefix + '*')
       for key in keys:
           type_ = redis_client.type(key)
           if type_ == 'string':
               length = redis_client.strlen(key)
               if length > str_threshold:
                   big_keys.append((key, 'string', length))
           elif type_ in ['hash', 'list', 'set', 'zset']:
               count = 0
               if type_ == 'hash':
                   count = redis_client.hlen(key)
               elif type_ == 'list':
                   count = redis_client.llen(key)
               elif type_ == 'set':
                   count = redis_client.scard(key)
               elif type_ == 'zset':
                   count = redis_client.zcard(key)
               if count > collection_threshold:
                   big_keys.append((key, type_, count))
   return big_keys
# 使用示例
redis_client = redis.Redis(host='localhost', port=6379, db=0)
big_keys = scan_big_keys(redis_client, 'user:')
for key, type_, size in big_keys:
   print(f"大key: {key}, 類型: {type_}, 大小: {size}")

3. 可視化工具輔助:讓大 key 一目了然

如果覺得命令行太麻煩,可以用 RedisInsight(官方可視化工具)或者開源的 RedisDesktopManager,這些工具都有大 key 掃描功能,能生成直觀的圖表。比如 RedisInsight 的"Memory Analysis"模塊,能按數(shù)據(jù)類型展示內(nèi)存占用分布,點(diǎn)擊某個(gè)類型就能看到具體的大 key 列表,適合團(tuán)隊(duì)協(xié)作時(shí)給非技術(shù)同學(xué)演示。

三、治理大 key:分場景出招,不同類型有不同解法

(一)字符串類型大 key:能壓縮就壓縮,能拆分就拆分

案例:用戶詳情存成大 JSON

某電商項(xiàng)目把用戶詳情(包括收貨地址、訂單歷史、會(huì)員信息)存成一個(gè)大 JSON,單個(gè) key 大小超過2MB。解決方案分兩步:

  • 數(shù)據(jù)壓縮:先用 gzip 壓縮 JSON 字符串,壓縮后大小能降到500KB左右。Redis 提供了 COMPRESS 和 DECOMPRESS 命令(需要開啟 redis-module-recompress 模塊),不過更推薦在應(yīng)用層處理,比如 Java 里用 GZIPOutputStream 和 GZIPInputStream。
// 壓縮數(shù)據(jù)
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);
gzipOutputStream.write(userJson.getBytes());
gzipOutputStream.close();
byte[] compressedData = byteArrayOutputStream.toByteArray();
redisTemplate.opsForValue().set("user:123", compressedData);

// 解壓縮數(shù)據(jù)
byte[] data = redisTemplate.opsForValue().get("user:123");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream);
BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream));
StringBuilder decompressedJson = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
    decompressedJson.append(line);
}
  • 按需拆分:把常用字段(比如用戶名、頭像)和不常用字段(比如三年前的訂單)分開存儲(chǔ)。比如用 user:123:base 存基礎(chǔ)信息,user:123:order:2023 存2023年的訂單,查詢時(shí)用 MGET 批量獲取,雖然多了幾個(gè) key,但每次獲取的數(shù)據(jù)量小了,網(wǎng)絡(luò)傳輸速度快了很多。

避坑指南:別用 APPEND 命令往大字符串里追加數(shù)據(jù)

曾經(jīng)有個(gè)項(xiàng)目用 APPEND 記錄用戶操作日志,每天往一個(gè) key 里追加幾MB數(shù)據(jù),一個(gè)月后這個(gè) key 變成了50MB。APPEND 操作在字符串底層實(shí)現(xiàn)是動(dòng)態(tài)擴(kuò)展數(shù)組,當(dāng)數(shù)組需要擴(kuò)容時(shí),會(huì)申請(qǐng)一塊更大的內(nèi)存,把舊數(shù)據(jù)復(fù)制過去,再追加新數(shù)據(jù)。50MB的字符串每次擴(kuò)容都要復(fù)制大量數(shù)據(jù),CPU使用率直接飆升,后來改成按天拆分key,問題立刻解決。

(二)集合類型大 key:分桶存儲(chǔ),別把雞蛋放一個(gè)籃子里

案例:千萬級(jí)用戶的標(biāo)簽集合

某社交APP用 set 存儲(chǔ)每個(gè)用戶的興趣標(biāo)簽,個(gè)別活躍用戶的標(biāo)簽數(shù)量超過20萬。直接遍歷這個(gè) set 時(shí),Redis 主線程被阻塞了好幾秒。解決方案是"分桶+哈希取模":

  • 確定桶的數(shù)量:根據(jù)最大元素?cái)?shù)量決定,比如每個(gè)桶最多存1萬條數(shù)據(jù),20萬條就分20個(gè)桶。
  • 計(jì)算桶編號(hào):用 CRC32 算法對(duì)用戶ID取模,保證同一個(gè)用戶的標(biāo)簽分布在同一個(gè)桶里(如果需要保證順序,用 hash_mod 時(shí)要考慮一致性)。
  • 修改數(shù)據(jù)結(jié)構(gòu):把 set user:123:tags 改成 set user:123:tags:0 到 set user:123:tags:19,每個(gè)桶最多1萬條數(shù)據(jù)。
// 計(jì)算桶編號(hào)
long userId = 123;
int bucketCount = 20;
int bucketId = (int) (userId % bucketCount);
String bucketKey = "user:" + userId + ":tags:" + bucketId;
// 添加標(biāo)簽
redisTemplate.opsForSet().add(bucketKey, "tag1", "tag2");
// 遍歷所有桶
for (int i = 0; i < bucketCount; i++) {
   String key = "user:" + userId + ":tags:" + i;
   Set<String> tags = redisTemplate.opsForSet().members(key);
   // 處理每個(gè)桶的數(shù)據(jù)
}

進(jìn)階操作:用分片集群的路由規(guī)則優(yōu)化

如果用的是 Redis Cluster,大 key 會(huì)被分配到固定分片上,分桶后可以讓不同的桶分布在不同分片,比如每個(gè)桶的 key 加上分片標(biāo)識(shí)(user:123:tags:0:shard1),不過這種方法需要和集群架構(gòu)深度結(jié)合,建議在架構(gòu)設(shè)計(jì)階段就考慮大 key 問題。

(三)業(yè)務(wù)層面優(yōu)化:從源頭減少大 key 的產(chǎn)生

  • 分頁處理:比如用戶的消息列表,別把所有歷史消息都存到一個(gè) list 里,改成按頁存儲(chǔ),用 list:user:123:page:1、list:user:123:page:2,每次只取當(dāng)前頁的數(shù)據(jù)。
  • 時(shí)效性控制:給大 key 設(shè)置合理的過期時(shí)間,比如臨時(shí)緩存的大集合,用完就自動(dòng)刪除,別讓它一直占著內(nèi)存。
  • 數(shù)據(jù)歸檔:像電商的歷史訂單,超過半年的可以歸檔到數(shù)據(jù)庫或文件存儲(chǔ),Redis 里只存最近三個(gè)月的常用數(shù)據(jù)。

四、實(shí)戰(zhàn)案例:從崩潰到穩(wěn)定,我們是怎么搞定大 key 的

背景:某直播平臺(tái)的禮物排行榜

直播間的禮物排行榜用 zset 存儲(chǔ),每個(gè)直播間一個(gè) key,里面存了所有送禮用戶的分?jǐn)?shù),個(gè)別熱門直播間的 zset 成員超過50萬。每天晚上高峰期,存儲(chǔ)排行榜的 Redis 節(jié)點(diǎn)頻繁觸發(fā) OOM,導(dǎo)致整個(gè)集群不可用。

治理過程:

  1. 第一步:定位罪魁禍?zhǔn)?nbsp;用前面提到的 Python 掃描工具,發(fā)現(xiàn) room:123:gifts 這個(gè) zset 有67萬成員,ZRANGE 操作平均耗時(shí)200ms,遠(yuǎn)超 Redis 單次操作1ms的正常水平。
  2. 第二步:分桶+冷熱分離
  • 按送禮時(shí)間分桶:最近1小時(shí)的實(shí)時(shí)數(shù)據(jù)存在 room:123:gifts:hot,1-24小時(shí)的數(shù)據(jù)存在 room:123:gifts:warm,超過24小時(shí)的歸檔到數(shù)據(jù)庫。
  • 每個(gè)桶限制成員數(shù)量:hot桶最多存1萬條(只保留最新的1萬條實(shí)時(shí)數(shù)據(jù)),warm桶按小時(shí)分桶(room:123:gifts:warm:2025041010 表示2025年4月10日10點(diǎn)的數(shù)據(jù))。
  1. 第三步:優(yōu)化查詢邏輯 原來的業(yè)務(wù)直接查整個(gè) zset 取Top100,現(xiàn)在改成先查 hot 桶和最近24個(gè) warm 桶,合并后再取Top100。雖然多了幾次 ZUNIONSTORE 操作,但每個(gè) zset 的成員數(shù)量都控制在1萬以內(nèi),操作耗時(shí)降到了10ms以下。
  2. 第四步:監(jiān)控與預(yù)警 用 Prometheus + Grafana 監(jiān)控每個(gè) zset 的成員數(shù)量,設(shè)置預(yù)警:當(dāng)單個(gè) zset 成員超過8000時(shí)觸發(fā)報(bào)警,同時(shí)監(jiān)控內(nèi)存碎片率(mem_fragmentation_ratio),當(dāng)超過1.5時(shí)自動(dòng)觸發(fā)大 key 掃描。

治理效果:

  • 內(nèi)存使用率從95%降到60%,OOM 再也沒出現(xiàn)過。
  • CPU 負(fù)載從平均80%降到20%,因?yàn)樘幚硇〖系乃俣瓤炝撕芏唷?/li>
  • 業(yè)務(wù)查詢延遲從200ms降到15ms,用戶刷新禮物榜再也不卡頓了。

五、避坑指南:這些大 key 相關(guān)的坑,千萬別踩!

1. 別迷信"大 key 一定是壞事"

有些場景下,合理的大 key 反而更高效。比如存儲(chǔ)一個(gè)1MB的圖片二進(jìn)制數(shù)據(jù),雖然是大 key,但比拆分成多個(gè)小 key 更節(jié)省內(nèi)存(每個(gè) key 本身有元數(shù)據(jù)開銷,Redis 中每個(gè) key 大約占1KB內(nèi)存)。所以治理大 key 要結(jié)合業(yè)務(wù)場景,不能一刀切。

2. 批量操作時(shí)注意"管道"的使用

用 pipeline 批量處理小 key 沒問題,但處理大集合時(shí)別濫用管道。比如用管道執(zhí)行100次 HGETALL 一個(gè)有10萬字段的 hash,會(huì)導(dǎo)致客戶端內(nèi)存飆升,因?yàn)樗薪Y(jié)果會(huì)一次性返回。正確的做法是分批次處理,每次處理1000個(gè)字段,或者用 HSCAN 漸進(jìn)式掃描。

3. 集群遷移時(shí)的大 key 陷阱

當(dāng)需要給 Redis 集群擴(kuò)容時(shí),大 key 的遷移會(huì)導(dǎo)致源節(jié)點(diǎn)和目標(biāo)節(jié)點(diǎn)之間產(chǎn)生大量網(wǎng)絡(luò)流量。比如一個(gè)10MB的大 key 遷移,需要先在源節(jié)點(diǎn)序列化,通過網(wǎng)絡(luò)傳輸,再在目標(biāo)節(jié)點(diǎn)反序列化,這個(gè)過程可能會(huì)阻塞兩個(gè)節(jié)點(diǎn)的主線程。建議在低峰期遷移,并且對(duì)大 key 單獨(dú)處理(比如先刪除,遷移后再重新生成)。

4. 監(jiān)控要關(guān)注這幾個(gè)關(guān)鍵指標(biāo)

  • used_memory:超過物理內(nèi)存80%就該警惕了。
  • mem_fragmentation_ratio:大于1.5說明內(nèi)存碎片太多,需要清理或重啟(僅單節(jié)點(diǎn)有效,集群節(jié)點(diǎn)重啟要謹(jǐn)慎)。
  • blocked_clients:如果這個(gè)值經(jīng)常大于0,說明有慢操作阻塞主線程,很可能是處理大 key 導(dǎo)致的。

六、總結(jié):防患于未然,比事后救火更重要

回顧這次治理經(jīng)歷,最大的感悟是:大 key 問題就像房間里的大象,剛開始覺得"存幾個(gè)大集合沒關(guān)系",等到出問題時(shí)已經(jīng)積重難返。

最好的辦法是在項(xiàng)目初期就建立規(guī)范:

  1. 開發(fā)階段:設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)時(shí)預(yù)估元素?cái)?shù)量,超過1萬的集合類數(shù)據(jù)強(qiáng)制分桶。
  2. 測(cè)試階段:用壓測(cè)工具模擬大 key 場景,比如用 redis-benchmark 測(cè)試 LRANGE 10萬條數(shù)據(jù)的耗時(shí)。
  3. 上線階段:部署自動(dòng)掃描腳本,每天凌晨掃描大 key,發(fā)現(xiàn)異常及時(shí)報(bào)警。
  4. 迭代階段:每次上線新功能,檢查是否引入了潛在的大 key(比如新增的集合類存儲(chǔ))。 

希望這篇文章能讓你少走彎路,下次再遇到 Redis 崩潰,記得先查大 key——相信我,十有八九是它在搞事情。 

責(zé)任編輯:武曉燕 來源: 石杉的架構(gòu)筆記
相關(guān)推薦

2021-07-08 10:04:36

人工智能AI主管

2022-11-03 08:56:43

RediskeyBitmap

2024-12-02 01:16:53

2020-02-10 16:07:42

工具包

2023-05-03 20:53:48

2018-05-18 09:18:00

數(shù)據(jù)分析報(bào)告數(shù)據(jù)收集

2024-05-23 07:59:42

RedisKey性能

2020-05-26 13:45:46

Python函數(shù)字符串

2021-05-13 23:39:19

勒索軟件攻擊數(shù)據(jù)泄露

2023-12-13 09:08:26

CPU性能分析Linux

2024-05-29 12:47:27

2023-02-10 18:32:21

項(xiàng)目管理實(shí)踐

2020-07-02 09:55:32

運(yùn)維架構(gòu)技術(shù)

2022-01-17 18:21:09

數(shù)據(jù)庫社交引流

2022-06-20 15:19:51

前端監(jiān)控方案

2020-09-21 09:00:41

Docker架構(gòu)容器

2022-07-14 21:58:31

MQ中間件降級(jí)機(jī)制

2020-07-23 14:13:04

運(yùn)維架構(gòu)技術(shù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)