Redis?很屌,不懂使用規(guī)范就糟蹋了
時間:2021-09-29 13:47:49
手機看文章
掃描二維碼
隨時隨地手機看文章
[導讀]?這可能是最中肯的Redis使用規(guī)范了碼哥,昨天我被公司Leader批評了。我在單身紅娘婚戀類型互聯(lián)網(wǎng)公司工作,在雙十一推出下單就送女朋友的活動。誰曾想,凌晨12點之后,用戶量暴增,出現(xiàn)了一個技術(shù)故障,用戶無法下單,當時老大火冒三丈!經(jīng)過查找發(fā)現(xiàn)Redis報Couldnotgetaresourcefromthepool。獲取不到連接資源,并且集群中的單臺Redis連接量很高。于是各種更改最大連接數(shù)、連接等待數(shù),雖然報錯信息頻率有所緩解,但還是持續(xù)報錯。后來經(jīng)過線下測試,發(fā)現(xiàn)存放Redis中的字符數(shù)據(jù)很大,平均1s返回數(shù)據(jù)。?碼哥,可以分享下使用Redis的規(guī)范么?我想做一個唯快不破的真男人!R...
?這可能是最中肯的 Redis 使用規(guī)范了碼哥,昨天我被公司 Leader 批評了。我在單身紅娘婚戀類型互聯(lián)網(wǎng)公司工作,在雙十一推出下單就送女朋友的活動。誰曾想,凌晨 12 點之后,用戶量暴增,出現(xiàn)了一個技術(shù)故障,用戶無法下單,當時老大火冒三丈!經(jīng)過查找發(fā)現(xiàn) Redis 報
Could not get a resource from the pool
。獲取不到連接資源,并且集群中的單臺 Redis 連接量很高。于是各種更改最大連接數(shù)、連接等待數(shù),雖然報錯信息頻率有所緩解,但還是持續(xù)報錯。后來經(jīng)過線下測試,發(fā)現(xiàn)存放 Redis 中的字符數(shù)據(jù)很大,平均 1s 返回數(shù)據(jù)。?碼哥,可以分享下使用 Redis 的規(guī)范么?我想做一個唯快不破的真男人!
Redis 使用規(guī)范圍繞如下幾個緯度展開:
- 鍵值對使用規(guī)范;
- 命令使用規(guī)范;
- 數(shù)據(jù)保存規(guī)范;
- 運維規(guī)范。
鍵值對使用規(guī)范
有兩點需要注意:- 好的
key
命名,才能提供可讀性強、可維護性高的 key,便于定位問題和尋找數(shù)據(jù)。 value
要避免出現(xiàn)bigkey
、選擇高效的序列化和壓縮、使用對象共享池、選擇高效恰當?shù)臄?shù)據(jù)類型(可參考《Redis 實戰(zhàn)篇:巧用數(shù)據(jù)類型實現(xiàn)億級數(shù)據(jù)統(tǒng)計》)。
key 命名規(guī)范
規(guī)范的key
命名,在遇到問題的時候能夠方便定位。Redis 屬于 沒有 Scheme
的 NoSQL
數(shù)據(jù)庫。所以要靠規(guī)范來建立其 Scheme
語意,就好比根據(jù)不同的場景我們建立不同的數(shù)據(jù)庫。敲黑板把「業(yè)務模塊名」作為前綴(好比數(shù)據(jù)庫 Scheme
),通過「冒號」分隔,再加上「具體業(yè)務名」。這樣我們就可以通過 key
前綴來區(qū)分不同的業(yè)務數(shù)據(jù),清晰明了。總結(jié)起來就是:「業(yè)務名:表名:id」比如我們要統(tǒng)計公眾號屬于技術(shù)類型的博主「碼哥字節(jié)」的粉絲數(shù)。set?公眾號:技術(shù)類:碼哥字節(jié)?100000
?碼哥,key 太長的話有什么問題么?key 是字符串,底層的數(shù)據(jù)結(jié)構(gòu)是
SDS
,SDS 結(jié)構(gòu)中會包含字符串長度、分配空間大小等元數(shù)據(jù)信息。字符串長度增加,SDS 的元數(shù)據(jù)也會占用更多的內(nèi)存空間。所以當字符串太長的時候,我們可以采用適當縮寫的形式。不要使用 bigkey
?碼哥,我就中招了,導致報錯獲取不到連接。因為 Redis 是單線程執(zhí)行讀寫指令,如果出現(xiàn)
bigkey
的讀寫操作就會阻塞線程,降低 Redis 的處理效率。bigkey
包含兩種情況:- 鍵值對的
value
很大,比如value
保存了2MB
的String
數(shù)據(jù); - 鍵值對的
value
是集合類型,元素很多,比如保存了 5 萬個元素的List
集合。
key
和string
類型 value
限制均為512MB
。防止網(wǎng)卡流量、慢查詢,string
類型控制在10KB
以內(nèi),hash、list、set、zset
元素個數(shù)不要超過 5000。?碼哥,如果業(yè)務數(shù)據(jù)就是這么大咋辦?比如保存的是《金瓶梅》這個大作。我們還可以通過
gzip
數(shù)據(jù)壓縮來減小數(shù)據(jù)大小:/**
?*?使用gzip壓縮字符串
?*/
public?static?String?compress(String?str)?{
????if?(str?==?null?||?str.length()?==?0)?{
????????return?str;
????}
????try?(ByteArrayOutputStream?out?=?new?ByteArrayOutputStream();
????GZIPOutputStream?gzip?=?new?GZIPOutputStream(out))?{
????????gzip.write(str.getBytes());
????}?catch?(IOException?e)?{
????????e.printStackTrace();
????}
????return?new?sun.misc.BASE64Encoder().encode(out.toByteArray());
}
/**
?*?使用gzip解壓縮
?*/
public?static?String?uncompress(String?compressedStr)?{
????if?(compressedStr?==?null?||?compressedStr.length()?==?0)?{
????????return?compressedStr;
????}
????byte[]?compressed?=?new?sun.misc.BASE64Decoder().decodeBuffer(compressedStr);;
????String?decompressed?=?null;
????try?(ByteArrayOutputStream?out?=?new?ByteArrayOutputStream();
????ByteArrayInputStream?in?=?new?ByteArrayInputStream(compressed);
????GZIPInputStream?ginzip?=?new?GZIPInputStream(in);)?{
????????byte[]?buffer?=?new?byte[1024];
????????int?offset?=?-1;
????????while?((offset?=?ginzip.read(buffer))?!=?-1)?{
????????????out.write(buffer,?0,?offset);
????????}
????????decompressed?=?out.toString();
????}?catch?(IOException?e)?{
????????e.printStackTrace();
????}
????return?decompressed;
}
集合類型如果集合類型的元素的確很多,我們可以將一個大集合拆分成多個小集合來保存。使用高效序列化和壓縮方法
為了節(jié)省內(nèi)存,我們可以使用高效的序列化方法和壓縮方法去減少value
的大小。protostuff
和 kryo
這兩種序列化方法,就要比 Java
內(nèi)置的序列化方法效率更高。上述的兩種序列化方式雖然省內(nèi)存,但是序列化后都是二進制數(shù)據(jù),可讀性太差。通常我們會序列化成 JSON
或者 XML
,為了避免數(shù)據(jù)占用空間大,我們可以使用壓縮工具(snappy、 gzip)將數(shù)據(jù)壓縮再存到 Redis 中。使用整數(shù)對象共享池
Redis 內(nèi)部維護了 0 到 9999 這 1 萬個整數(shù)對象,并把這些整數(shù)作為一個共享池使用。即使大量鍵值對保存了 0 到 9999 范圍內(nèi)的整數(shù),在 Redis 實例中,其實只保存了一份整數(shù)對象,可以節(jié)省內(nèi)存空間。需要注意的是,有兩種情況是不生效的:- Redis 中設置了
maxmemory
,而且啟用了LRU
策略(allkeys-lru 或 volatile-lru 策略
),那么,整數(shù)對象共享池就無法使用了。?這是因為 LRU 需要統(tǒng)計每個鍵值對的使用時間,如果不同的鍵值對都復用一個整數(shù)對象就無法統(tǒng)計了。
- 如果集合類型數(shù)據(jù)采用 ziplist 編碼,而集合元素是整數(shù),這個時候,也不能使用共享池。
?因為 ziplist 使用了緊湊型內(nèi)存結(jié)構(gòu),判斷整數(shù)對象的共享情況效率低。
命令使用規(guī)范
有的命令的執(zhí)行會造成很大的性能問題,我們需要格外注意。生產(chǎn)禁用的指令
Redis 是單線程處理請求操作,如果我們執(zhí)行一些涉及大量操作、耗時長的命令,就會嚴重阻塞主線程,導致其它請求無法得到正常處理。- KEYS:該命令需要對 Redis 的全局哈希表進行全表掃描,嚴重阻塞 Redis 主線程;
?應該使用 SCAN 來代替,分批返回符合條件的鍵值對,避免主線程阻塞。
- FLUSHALL:刪除 Redis 實例上的所有數(shù)據(jù),如果數(shù)據(jù)量很大,會嚴重阻塞 Redis 主線程;
- FLUSHDB,刪除當前數(shù)據(jù)庫中的數(shù)據(jù),如果數(shù)據(jù)量很大,同樣會阻塞 Redis 主線程。
?加上 ASYNC 選項,讓 FLUSHALL,F(xiàn)LUSHDB 異步執(zhí)行。
rename-command
命令在配置文件中對這些命令進行重命名,讓客戶端無法使用這些命令。慎用 MONITOR 命令
MONITOR 命令會把監(jiān)控到的內(nèi)容持續(xù)寫入輸出緩沖區(qū)。如果線上命令的操作很多,輸出緩沖區(qū)很快就會溢出了,這就會對 Redis 性能造成影響,甚至引起服務崩潰。所以,除非十分需要監(jiān)測某些命令的執(zhí)行(例如,Redis 性能突然變慢,我們想查看下客戶端執(zhí)行了哪些命令)我們才使用。慎用全量操作命令
比如獲取集合中的所有元素(HASH 類型的 hgetall、List 類型的 lrange、Set 類型的 smembers、zrange 等命令)。這些操作會對整個底層數(shù)據(jù)結(jié)構(gòu)進行全量掃描 ,導致阻塞 Redis 主線程。?碼哥,如果業(yè)務場景就是需要獲取全量數(shù)據(jù)咋辦?有兩個方式可以解決:
- 使用
SSCAN、HSCAN
等命令分批返回集合數(shù)據(jù); - 把大集合拆成小集合,比如按照時間、區(qū)域等劃分。
數(shù)據(jù)保存規(guī)范
冷熱數(shù)據(jù)分離
雖然 Redis 支持使用 RDB 快照和 AOF 日志持久化保存數(shù)據(jù),但是,這兩個機制都是用來提供數(shù)據(jù)可靠性保證的,并不是用來擴充數(shù)據(jù)容量的。不要什么數(shù)據(jù)都存在 Redis,應該作為緩存保存熱數(shù)據(jù),這樣既可以充分利用 Redis 的高性能特性,還可以把寶貴的內(nèi)存資源用在服務熱數(shù)據(jù)上。業(yè)務數(shù)據(jù)隔離
不要將不相關(guān)的數(shù)據(jù)業(yè)務都放到一個 Redis 中。一方面避免業(yè)務相互影響,另一方面避免單實例膨脹,并能在故障時降低影響面,快速恢復。設置過期時間
在數(shù)據(jù)保存時,我建議你根據(jù)業(yè)務使用數(shù)據(jù)的時長,設置數(shù)據(jù)的過期時間。寫入 Redis 的數(shù)據(jù)會一直占用內(nèi)存,如果數(shù)據(jù)持續(xù)增多,就可能達到機器的內(nèi)存上限,造成內(nèi)存溢出,導致服務崩潰。控制單實例的內(nèi)存容量
建議設置在 2~6 GB 。這樣一來,無論是 RDB 快照,還是主從集群進行數(shù)據(jù)同步,都能很快完成,不會阻塞正常請求的處理。防止緩存雪崩
避免集中過期 key 導致緩存雪崩。?碼哥,什么是緩存雪崩?當某一個時刻出現(xiàn)大規(guī)模的緩存失效的情況,那么就會導致大量的請求直接打在數(shù)據(jù)庫上面,導致數(shù)據(jù)庫壓力巨大,如果在高并發(fā)的情況下,可能瞬間就會導致數(shù)據(jù)庫宕機。
運維規(guī)范
- 使用 Cluster 集群或者哨兵集群,做到高可用;
- 實例設置最大連接數(shù),防止過多客戶端連接導致實例負載過高,影響性能。
- 不開啟 AOF 或開啟 AOF 配置為每秒刷盤,避免磁盤 IO 拖慢 Redis 性能。
- 設置合理的 repl-backlog,降低主從全量同步的概率
- 設置合理的 slave client-output-buffer-limit,避免主從復制中斷情況發(fā)生。
- 根據(jù)實際場景設置合適的內(nèi)存淘汰策略。
- 使用連接池操作 Redis。