Redis鍵值設計的實踐
在Redis中,良好的鍵值設計可以達成事半功倍的效果,而不好的鍵值設計可能會帶來Redis服務停滯,網(wǎng)絡阻塞,CPU使用率飆升等一系列問題,今天就教大家如何設計一個良好的key-value
1 優(yōu)雅的key結(jié)構(gòu)
Redis的Key雖然可以自定義,但最好遵循下面的幾個最佳實踐約定:
遵循基本格式:[業(yè)務名稱]:[數(shù)據(jù)名]:[id]
,例如我們的登錄業(yè)務,需要保存用戶信息,其key可以設計成如下格式
這種設計的好處不僅在于可讀性強,還在于可以避免key的沖突問題,而且方便管理
Key的長度不超過44字節(jié)
無論是哪種數(shù)據(jù)類型, key都是string類型,string類型的底層編碼包含int、embstr和raw三種。如果key中全是數(shù)字,那么就會直接以int類型去存儲,而int占用的空間也是最小的,當然出于業(yè)務需求,我們不可能將key設計為一個全數(shù)字的,而如果不是純數(shù)字,底層存儲的就是SDS內(nèi)容,如果小于44字節(jié),就會使用embstr類型,embstr在內(nèi)存中是一段連續(xù)的存儲空間,內(nèi)存占用相對raw來說較小,而當字節(jié)數(shù)大于44字節(jié)時,會轉(zhuǎn)為raw模式存儲,在raw模式下,內(nèi)存空間不是連續(xù)的,而是采用一個指針指向了另外一段內(nèi)存空間,在這段空間里存儲SDS內(nèi)容,這樣空間不連續(xù),訪問的時候性能也就會收到影響,還有可能產(chǎn)生內(nèi)存碎片
需要注意的是,如果你的redis版本低于4.0,那么界限是39字節(jié)而非44字節(jié)
Key中不包含一些特殊字符
2 拒絕BigKey
2.1 判斷BigKey
BigKey顧名思義就是一個很大的Key,這里的大并不是指Key本身很大,而是指包括這個Key的Value在內(nèi)的一整個鍵值對很大
BigKey通常以Key-Value的大小或者Key中成員的數(shù)量來綜合判定,例如:
- Key的Value過大:例如一個String類型的Key,它的Value為5MB
- Key中的成員數(shù)過多:例如一個ZSET類型的Key,它的成員數(shù)量為10000個
- Key中成員的Value過大:例如一個Hash類型的Key,它的成員數(shù)量雖然只有1000個,但這些成員的Value總大小為100 MB
那么如何判斷元素的大小呢?redis中為我們提供了相應的命令,語法如下:
memory usage 鍵名
這條命令會返回一條數(shù)據(jù)占用內(nèi)存的總大小,這個大小不僅包括Key和Value的大小,還包括數(shù)據(jù)存儲時的一些元信息,因此可能你的Key與Value只占用了幾十個字節(jié),但最終的返回結(jié)果是幾百個字節(jié)
但是我們一般不推薦使用memory指令,因為這個指令對CPU的占用率是很高的,實際開發(fā)中我們一般只需要衡量Value的大小或者Key中的成員數(shù)即可
例如如果我們使用的數(shù)據(jù)類型是String,就可以使用以下命令,返回的結(jié)果是Value的長度
strlen 鍵名
如果我們使用的數(shù)據(jù)類型是List,就可以使用以下命令,返回的結(jié)果是List中成員的個數(shù)
llen 鍵名
一般我們推薦,單個key的value小于10KB,集合類型的key元素數(shù)量小于1000
2.2 BigKey的危害
網(wǎng)絡阻塞
當我們對一個BigKey發(fā)起讀請求時,只需少量的QPS就可能導致帶寬使用率被占滿,導致Redis實例乃至所在物理機變慢,例如一個bigkey占用5M內(nèi)存,只需要QPS達到20,那么1秒鐘就會占100M的帶寬
數(shù)據(jù)傾斜
集群環(huán)境下,由于所有插槽一開始都是均衡分配的,因此BigKey所在的Redis實例內(nèi)存使用率會遠超其他實例,從而無法使數(shù)據(jù)分片的內(nèi)存資源達到均衡,最后不得不手動重新分配插槽,增加運維人員的負擔
Redis阻塞
對元素較多的hash、list、zset等做運算會耗時較久,而且由于Redis是單線程的,在運算過程中會導致服務阻塞,無法接收其他用戶請求
CPU壓力
對BigKey的數(shù)據(jù)進行序列化或反序列化都會導致CPU的使用率飆升,影響Redis實例和本機其它應用
2.3 如何發(fā)現(xiàn)BigKey
既然我們知道了什么叫BigKey以及BigKey的危害,那么如何去快速發(fā)現(xiàn)Redis中所有的BigKey呢?這里為大家提供以下幾種方案:
1)利用Redis本身提供的命令
利用以下命令,可以遍歷分析所有key,并返回Key的整體統(tǒng)計信息與每種數(shù)據(jù)類型中Top1的BigKey
redis-cli -a 密碼 --bigkeys
演示如下(這里我的redis沒有設置密碼,如果你的redis設置了密碼,則需要使用 -a 密碼
進行連接)
2)自己手動編寫程序進行掃描
我們可以通過自己編寫程序,將Redis中所有的數(shù)據(jù)查詢出來并一一統(tǒng)計長度來找出BigKey,這里不建議使用keys *
來查詢所有數(shù)據(jù),因為keys *
是一次將所有的數(shù)據(jù)全部查找出來,如果數(shù)據(jù)量很大,key *
一次可能要幾十秒甚至幾分鐘,在如此長的時間內(nèi),Redis的主線程會因為執(zhí)行該命令而被阻塞。
這里建議使用redis提供的scan命令,語法如下:
scan 起始位置 count 數(shù)量
scan掃描有點類似于分頁查詢,而被分頁的對象是redis中所有的數(shù)據(jù),scan命令調(diào)用一次只會從指定的起始位置開始返回指定數(shù)量的數(shù)據(jù),以及此次掃描結(jié)束時光標所在的位置,下一次掃描時就需要從這個光標開始繼續(xù)往下掃描
這里提供一個已經(jīng)編寫好的查找BigKey的測試類,大家可以參考一下
import com.heima.jedis.util.JedisConnectionFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import redis.clients.jedis.Jedis; import redis.clients.jedis.ScanResult; import java.util.HashMap; import java.util.List; import java.util.Map; public class JedisTest { private Jedis jedis; @BeforeEach void setUp() { // 1.建立連接 // jedis = new Jedis("192.168.150.101", 6379); jedis = JedisConnectionFactory.getJedis(); // 2.設置密碼 jedis.auth("123321"); // 3.選擇庫 jedis.select(0); } //設置string類型的長度上限,超過這個上限就判斷為BigKey final static int STR_MAX_LEN = 10 * 1024; //設置集合類型允許的成員數(shù)量上限,超過這個上限就判斷為BigKey final static int HASH_MAX_LEN = 500; @Test void testScan() { int maxLen = 0; long len = 0; String cursor = "0"; do { // 掃描并獲取一部分key ScanResult<String> result = jedis.scan(cursor); // 記錄cursor cursor = result.getCursor(); List<String> list = result.getResult(); if (list == null || list.isEmpty()) { break; } // 遍歷 for (String key : list) { // 判斷key的類型 String type = jedis.type(key); switch (type) { case "string": len = jedis.strlen(key); maxLen = STR_MAX_LEN; break; case "hash": len = jedis.hlen(key); maxLen = HASH_MAX_LEN; break; case "list": len = jedis.llen(key); maxLen = HASH_MAX_LEN; break; case "set": len = jedis.scard(key); maxLen = HASH_MAX_LEN; break; case "zset": len = jedis.zcard(key); maxLen = HASH_MAX_LEN; break; default: break; } if (len >= maxLen) { System.out.printf("Found big key : %s, type: %s, length or size: %d %n", key, type, len); } } } while (!cursor.equals("0")); } @AfterEach void tearDown() { if (jedis != null) { jedis.close(); } } }
3)第三方工具
利用第三方工具,這里推薦Redis-Rdb-Tools,它會針對Redis的RDB快照文件來分析內(nèi)存使用情況,由于分析的是快照文件,因此不會占用Redis服務的任何性能,但是時效性相對較差
Redis-Rdb-Tools的github網(wǎng)址:https://github.com/sripathikrishnan/redis-rdb-tools
4)網(wǎng)絡監(jiān)控
使用自定義工具,監(jiān)控進出Redis的網(wǎng)絡數(shù)據(jù),超出預警值時主動告警。一般阿里云搭建的云服務器就有相關監(jiān)控頁面:
2.4 如何刪除BigKey
BigKey內(nèi)存占用較多,因此即便我們使用的是刪除操作,刪除BigKey也需要耗費很長時間,導致Redis主線程阻塞,引發(fā)一系列問題。
如果redis版本在4.0之后,我們可以通過異步刪除命令unlink來刪除一個BigKey,該命令會先把數(shù)據(jù)標記為已刪除,然后再異步執(zhí)行刪除操作。
如果redis版本在4.0之前,針對集合類型,我們可以先遍歷BigKey中所有的元素,先將子元素逐個刪除,最后再刪除BigKey。至于如何遍歷,針對不同的集合類型,可以參考以下不同的命令
3 恰當?shù)臄?shù)據(jù)類型
找出BigKey中,我們應該如何對BigKey進行優(yōu)化呢?這里我們需要選擇恰當?shù)臄?shù)據(jù)類型
3.1 存儲對象
如果我們要存儲一個User對象,有三種存儲方式:
1)JSON字符串
將一整個對象轉(zhuǎn)成Json格式進行存儲
user:1 | {“name”: “Jack”, “age”: 21} |
---|
優(yōu)點:實現(xiàn)簡單粗暴
缺點:數(shù)據(jù)耦合,不夠靈活,且需要維護JSON結(jié)構(gòu),占用內(nèi)存相對較大
2)字段打散
將對象的不同屬性存儲到不同的key中
key | value |
---|---|
user:1:name | Jack |
user:1:age | 21 |
優(yōu)點:可以靈活訪問對象任意字段
缺點:由于每條數(shù)據(jù)都會有一些元信息需要存儲,因此將一個Key分成多個Key進行存儲,占用的內(nèi)存會變的更大,且由于字段分散,當我們需要做統(tǒng)一控制時會變得很困難
3)hash(推薦)
使用hash結(jié)構(gòu)來存儲對象,對象的一個屬性對應集合中的一個成員
user:1 | name | jack |
age | 21 |
優(yōu)點:hash結(jié)構(gòu)底層會使用ziplist壓縮列表,空間占用小,且可以靈活訪問對象的任意字段
缺點:代碼編寫時相對復雜
3.2 Hash優(yōu)化
假如有一個hash類型的key,其中有100萬對field和value,field是自增id,這個key存在什么問題?如何優(yōu)化?
key | field | value |
someKey | id:0 | value0 |
..... | ..... | |
id:999999 | value999999 |
當hash的entry數(shù)量超過500時,底層會使用哈希表存儲而不是ZipList,內(nèi)存占用會變得比較高,雖然這個數(shù)量限制我們是可以通過以下命令進行修改的
config set hash-max-ziplist-entries 數(shù)量
但是entry數(shù)量如果實在太大了還是會導致BigKey問題,這是需要優(yōu)化的,這里提供以下兩種解決思路:
1)拆分為String類型(不推薦)
將Hash中的每個成員單獨使用一個String類型的key進行存儲
key | value |
id:0 | value0 |
..... | ..... |
id:999999 | value999999 |
這種方案是不推薦的,存在的問題如下
- string結(jié)構(gòu)底層沒有太多內(nèi)存優(yōu)化的,且存儲這些key的同時也會存儲大量的元信息,雖然數(shù)據(jù)打散了,但是整體內(nèi)存占用更多了
- 如果我們想要批量獲取這些數(shù)據(jù),會變得格外麻煩
2)拆分成多個Hash類型
拆分為小的hash,將 id / 100 作為key, 將id % 100 作為field,這樣每100個元素為一個Hash,這種方式相對上面兩種來說內(nèi)存占用會少很多,而且解決了Bigkey的問題,當然多少個元素作為一個Hash是自己定義的,這里建議數(shù)量不要超過500
key | field | value |
key:0 | id:00 | value0 |
..... | ..... | |
id:99 | value99 | |
key:1 | id:00 | value100 |
..... | ..... | |
id:99 | value199 | |
.... | ||
key:9999 | id:00 | value999900 |
..... | ..... | |
id:99 | value999999 |
到此這篇關于Redis鍵值設計的實踐的文章就介紹到這了,更多相關Redis鍵值設計內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Redis Cluster集群數(shù)據(jù)分片機制原理
這篇文章主要介紹了Redis Cluster集群數(shù)據(jù)分片機制原理,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-04-04Redis+Caffeine實現(xiàn)分布式二級緩存組件實戰(zhàn)教程
這篇文章主要介紹了Redis+Caffeine實現(xiàn)分布式二級緩存組件實戰(zhàn)教程,介紹了分布式二級緩存的優(yōu)勢,使用組件的方法,通過示例代碼給大家介紹的非常詳細,需要的朋友可以參考下2022-08-08Redis教程(四):Hashes數(shù)據(jù)類型
這篇文章主要介紹了Redis教程(四):Hashes數(shù)據(jù)類型,本文講解了Hashes數(shù)據(jù)類型概述、相關命令列表和命令使用示例等內(nèi)容,需要的朋友可以參考下2015-04-04Redisson分布式限流器RRateLimiter的使用及原理小結(jié)
本文主要介紹了Redisson分布式限流器RRateLimiter的使用及原理小結(jié),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2024-06-06Redis中的3種特殊數(shù)據(jù)結(jié)構(gòu)詳解
在本文中,我們對三種特殊的數(shù)據(jù)類型進行了介紹,它們分別是geospatial(地理空間數(shù)據(jù)類型)、HyperLogLogs和Bitmaps(位圖),這些數(shù)據(jù)類型在不同的領域和應用中發(fā)揮著重要作用,并且具有各自獨特的特性和用途,對Redis特殊數(shù)據(jù)結(jié)構(gòu)相關知識感興趣的朋友一起看看吧2024-02-02