Redis鍵值設(shè)計(jì)的實(shí)踐
在Redis中,良好的鍵值設(shè)計(jì)可以達(dá)成事半功倍的效果,而不好的鍵值設(shè)計(jì)可能會(huì)帶來(lái)Redis服務(wù)停滯,網(wǎng)絡(luò)阻塞,CPU使用率飆升等一系列問(wèn)題,今天就教大家如何設(shè)計(jì)一個(gè)良好的key-value
1 優(yōu)雅的key結(jié)構(gòu)
Redis的Key雖然可以自定義,但最好遵循下面的幾個(gè)最佳實(shí)踐約定:
遵循基本格式:[業(yè)務(wù)名稱]:[數(shù)據(jù)名]:[id],例如我們的登錄業(yè)務(wù),需要保存用戶信息,其key可以設(shè)計(jì)成如下格式

這種設(shè)計(jì)的好處不僅在于可讀性強(qiáng),還在于可以避免key的沖突問(wèn)題,而且方便管理
Key的長(zhǎng)度不超過(guò)44字節(jié)
無(wú)論是哪種數(shù)據(jù)類型, key都是string類型,string類型的底層編碼包含int、embstr和raw三種。如果key中全是數(shù)字,那么就會(huì)直接以int類型去存儲(chǔ),而int占用的空間也是最小的,當(dāng)然出于業(yè)務(wù)需求,我們不可能將key設(shè)計(jì)為一個(gè)全數(shù)字的,而如果不是純數(shù)字,底層存儲(chǔ)的就是SDS內(nèi)容,如果小于44字節(jié),就會(huì)使用embstr類型,embstr在內(nèi)存中是一段連續(xù)的存儲(chǔ)空間,內(nèi)存占用相對(duì)raw來(lái)說(shuō)較小,而當(dāng)字節(jié)數(shù)大于44字節(jié)時(shí),會(huì)轉(zhuǎn)為raw模式存儲(chǔ),在raw模式下,內(nèi)存空間不是連續(xù)的,而是采用一個(gè)指針指向了另外一段內(nèi)存空間,在這段空間里存儲(chǔ)SDS內(nèi)容,這樣空間不連續(xù),訪問(wèn)的時(shí)候性能也就會(huì)收到影響,還有可能產(chǎn)生內(nèi)存碎片
需要注意的是,如果你的redis版本低于4.0,那么界限是39字節(jié)而非44字節(jié)
Key中不包含一些特殊字符
2 拒絕BigKey
2.1 判斷BigKey
BigKey顧名思義就是一個(gè)很大的Key,這里的大并不是指Key本身很大,而是指包括這個(gè)Key的Value在內(nèi)的一整個(gè)鍵值對(duì)很大
BigKey通常以Key-Value的大小或者Key中成員的數(shù)量來(lái)綜合判定,例如:
- Key的Value過(guò)大:例如一個(gè)String類型的Key,它的Value為5MB
- Key中的成員數(shù)過(guò)多:例如一個(gè)ZSET類型的Key,它的成員數(shù)量為10000個(gè)
- Key中成員的Value過(guò)大:例如一個(gè)Hash類型的Key,它的成員數(shù)量雖然只有1000個(gè),但這些成員的Value總大小為100 MB
那么如何判斷元素的大小呢?redis中為我們提供了相應(yīng)的命令,語(yǔ)法如下:
memory usage 鍵名
這條命令會(huì)返回一條數(shù)據(jù)占用內(nèi)存的總大小,這個(gè)大小不僅包括Key和Value的大小,還包括數(shù)據(jù)存儲(chǔ)時(shí)的一些元信息,因此可能你的Key與Value只占用了幾十個(gè)字節(jié),但最終的返回結(jié)果是幾百個(gè)字節(jié)
但是我們一般不推薦使用memory指令,因?yàn)檫@個(gè)指令對(duì)CPU的占用率是很高的,實(shí)際開(kāi)發(fā)中我們一般只需要衡量Value的大小或者Key中的成員數(shù)即可
例如如果我們使用的數(shù)據(jù)類型是String,就可以使用以下命令,返回的結(jié)果是Value的長(zhǎng)度
strlen 鍵名
如果我們使用的數(shù)據(jù)類型是List,就可以使用以下命令,返回的結(jié)果是List中成員的個(gè)數(shù)
llen 鍵名
一般我們推薦,單個(gè)key的value小于10KB,集合類型的key元素?cái)?shù)量小于1000
2.2 BigKey的危害
網(wǎng)絡(luò)阻塞
當(dāng)我們對(duì)一個(gè)BigKey發(fā)起讀請(qǐng)求時(shí),只需少量的QPS就可能導(dǎo)致帶寬使用率被占滿,導(dǎo)致Redis實(shí)例乃至所在物理機(jī)變慢,例如一個(gè)bigkey占用5M內(nèi)存,只需要QPS達(dá)到20,那么1秒鐘就會(huì)占100M的帶寬
數(shù)據(jù)傾斜
集群環(huán)境下,由于所有插槽一開(kāi)始都是均衡分配的,因此BigKey所在的Redis實(shí)例內(nèi)存使用率會(huì)遠(yuǎn)超其他實(shí)例,從而無(wú)法使數(shù)據(jù)分片的內(nèi)存資源達(dá)到均衡,最后不得不手動(dòng)重新分配插槽,增加運(yùn)維人員的負(fù)擔(dān)
Redis阻塞
對(duì)元素較多的hash、list、zset等做運(yùn)算會(huì)耗時(shí)較久,而且由于Redis是單線程的,在運(yùn)算過(guò)程中會(huì)導(dǎo)致服務(wù)阻塞,無(wú)法接收其他用戶請(qǐng)求
CPU壓力
對(duì)BigKey的數(shù)據(jù)進(jìn)行序列化或反序列化都會(huì)導(dǎo)致CPU的使用率飆升,影響Redis實(shí)例和本機(jī)其它應(yīng)用
2.3 如何發(fā)現(xiàn)BigKey
既然我們知道了什么叫BigKey以及BigKey的危害,那么如何去快速發(fā)現(xiàn)Redis中所有的BigKey呢?這里為大家提供以下幾種方案:
1)利用Redis本身提供的命令
利用以下命令,可以遍歷分析所有key,并返回Key的整體統(tǒng)計(jì)信息與每種數(shù)據(jù)類型中Top1的BigKey
redis-cli -a 密碼 --bigkeys
演示如下(這里我的redis沒(méi)有設(shè)置密碼,如果你的redis設(shè)置了密碼,則需要使用 -a 密碼 進(jìn)行連接)

2)自己手動(dòng)編寫程序進(jìn)行掃描
我們可以通過(guò)自己編寫程序,將Redis中所有的數(shù)據(jù)查詢出來(lái)并一一統(tǒng)計(jì)長(zhǎng)度來(lái)找出BigKey,這里不建議使用keys *來(lái)查詢所有數(shù)據(jù),因?yàn)?code>keys * 是一次將所有的數(shù)據(jù)全部查找出來(lái),如果數(shù)據(jù)量很大,key *一次可能要幾十秒甚至幾分鐘,在如此長(zhǎng)的時(shí)間內(nèi),Redis的主線程會(huì)因?yàn)閳?zhí)行該命令而被阻塞。
這里建議使用redis提供的scan命令,語(yǔ)法如下:
scan 起始位置 count 數(shù)量
scan掃描有點(diǎn)類似于分頁(yè)查詢,而被分頁(yè)的對(duì)象是redis中所有的數(shù)據(jù),scan命令調(diào)用一次只會(huì)從指定的起始位置開(kāi)始返回指定數(shù)量的數(shù)據(jù),以及此次掃描結(jié)束時(shí)光標(biāo)所在的位置,下一次掃描時(shí)就需要從這個(gè)光標(biāo)開(kāi)始繼續(xù)往下掃描
這里提供一個(gè)已經(jīng)編寫好的查找BigKey的測(cè)試類,大家可以參考一下
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.設(shè)置密碼
jedis.auth("123321");
// 3.選擇庫(kù)
jedis.select(0);
}
//設(shè)置string類型的長(zhǎng)度上限,超過(guò)這個(gè)上限就判斷為BigKey
final static int STR_MAX_LEN = 10 * 1024;
//設(shè)置集合類型允許的成員數(shù)量上限,超過(guò)這個(gè)上限就判斷為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,它會(huì)針對(duì)Redis的RDB快照文件來(lái)分析內(nèi)存使用情況,由于分析的是快照文件,因此不會(huì)占用Redis服務(wù)的任何性能,但是時(shí)效性相對(duì)較差
Redis-Rdb-Tools的github網(wǎng)址:https://github.com/sripathikrishnan/redis-rdb-tools
4)網(wǎng)絡(luò)監(jiān)控
使用自定義工具,監(jiān)控進(jìn)出Redis的網(wǎng)絡(luò)數(shù)據(jù),超出預(yù)警值時(shí)主動(dòng)告警。一般阿里云搭建的云服務(wù)器就有相關(guān)監(jiān)控頁(yè)面:

2.4 如何刪除BigKey
BigKey內(nèi)存占用較多,因此即便我們使用的是刪除操作,刪除BigKey也需要耗費(fèi)很長(zhǎng)時(shí)間,導(dǎo)致Redis主線程阻塞,引發(fā)一系列問(wèn)題。
如果redis版本在4.0之后,我們可以通過(guò)異步刪除命令unlink來(lái)刪除一個(gè)BigKey,該命令會(huì)先把數(shù)據(jù)標(biāo)記為已刪除,然后再異步執(zhí)行刪除操作。
如果redis版本在4.0之前,針對(duì)集合類型,我們可以先遍歷BigKey中所有的元素,先將子元素逐個(gè)刪除,最后再刪除BigKey。至于如何遍歷,針對(duì)不同的集合類型,可以參考以下不同的命令

3 恰當(dāng)?shù)臄?shù)據(jù)類型
找出BigKey中,我們應(yīng)該如何對(duì)BigKey進(jìn)行優(yōu)化呢?這里我們需要選擇恰當(dāng)?shù)臄?shù)據(jù)類型
3.1 存儲(chǔ)對(duì)象
如果我們要存儲(chǔ)一個(gè)User對(duì)象,有三種存儲(chǔ)方式:
1)JSON字符串
將一整個(gè)對(duì)象轉(zhuǎn)成Json格式進(jìn)行存儲(chǔ)
| user:1 | {“name”: “Jack”, “age”: 21} |
|---|
優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單粗暴
缺點(diǎn):數(shù)據(jù)耦合,不夠靈活,且需要維護(hù)JSON結(jié)構(gòu),占用內(nèi)存相對(duì)較大
2)字段打散
將對(duì)象的不同屬性存儲(chǔ)到不同的key中
| key | value |
|---|---|
| user:1:name | Jack |
| user:1:age | 21 |
優(yōu)點(diǎn):可以靈活訪問(wèn)對(duì)象任意字段
缺點(diǎn):由于每條數(shù)據(jù)都會(huì)有一些元信息需要存儲(chǔ),因此將一個(gè)Key分成多個(gè)Key進(jìn)行存儲(chǔ),占用的內(nèi)存會(huì)變的更大,且由于字段分散,當(dāng)我們需要做統(tǒng)一控制時(shí)會(huì)變得很困難
3)hash(推薦)
使用hash結(jié)構(gòu)來(lái)存儲(chǔ)對(duì)象,對(duì)象的一個(gè)屬性對(duì)應(yīng)集合中的一個(gè)成員
| user:1 | name | jack |
| age | 21 |
優(yōu)點(diǎn):hash結(jié)構(gòu)底層會(huì)使用ziplist壓縮列表,空間占用小,且可以靈活訪問(wèn)對(duì)象的任意字段
缺點(diǎn):代碼編寫時(shí)相對(duì)復(fù)雜
3.2 Hash優(yōu)化
假如有一個(gè)hash類型的key,其中有100萬(wàn)對(duì)field和value,field是自增id,這個(gè)key存在什么問(wèn)題?如何優(yōu)化?
| key | field | value |
| someKey | id:0 | value0 |
| ..... | ..... | |
| id:999999 | value999999 |
當(dāng)hash的entry數(shù)量超過(guò)500時(shí),底層會(huì)使用哈希表存儲(chǔ)而不是ZipList,內(nèi)存占用會(huì)變得比較高,雖然這個(gè)數(shù)量限制我們是可以通過(guò)以下命令進(jìn)行修改的
config set hash-max-ziplist-entries 數(shù)量
但是entry數(shù)量如果實(shí)在太大了還是會(huì)導(dǎo)致BigKey問(wèn)題,這是需要優(yōu)化的,這里提供以下兩種解決思路:
1)拆分為String類型(不推薦)
將Hash中的每個(gè)成員單獨(dú)使用一個(gè)String類型的key進(jìn)行存儲(chǔ)
| key | value |
| id:0 | value0 |
| ..... | ..... |
| id:999999 | value999999 |
這種方案是不推薦的,存在的問(wèn)題如下
- string結(jié)構(gòu)底層沒(méi)有太多內(nèi)存優(yōu)化的,且存儲(chǔ)這些key的同時(shí)也會(huì)存儲(chǔ)大量的元信息,雖然數(shù)據(jù)打散了,但是整體內(nèi)存占用更多了
- 如果我們想要批量獲取這些數(shù)據(jù),會(huì)變得格外麻煩
2)拆分成多個(gè)Hash類型
拆分為小的hash,將 id / 100 作為key, 將id % 100 作為field,這樣每100個(gè)元素為一個(gè)Hash,這種方式相對(duì)上面兩種來(lái)說(shuō)內(nèi)存占用會(huì)少很多,而且解決了Bigkey的問(wèn)題,當(dāng)然多少個(gè)元素作為一個(gè)Hash是自己定義的,這里建議數(shù)量不要超過(guò)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 | |
到此這篇關(guān)于Redis鍵值設(shè)計(jì)的實(shí)踐的文章就介紹到這了,更多相關(guān)Redis鍵值設(shè)計(jì)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
全網(wǎng)最完整的Redis新手入門指導(dǎo)教程
這篇文章主要給大家介紹了Redis新手入門的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
redis中5種數(shù)據(jù)基礎(chǔ)查詢命令
本文主要介紹了redis中5種數(shù)據(jù)基礎(chǔ)查詢命令,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
Redis數(shù)據(jù)類型string和Hash詳解
大家都知道Redis中有五大數(shù)據(jù)類型分別是String、List、Set、Hash和Zset,本文給大家分享Redis數(shù)據(jù)類型string和Hash的相關(guān)操作,感興趣的朋友跟隨小編一起看看吧2022-03-03
RabbitMQ+redis+Redisson分布式鎖+seata實(shí)現(xiàn)訂單服務(wù)的流程分析
訂單服務(wù)涉及許多方面,分布式事務(wù),分布式鎖,例如訂單超時(shí)未支付要取消訂單,訂單如何防止重復(fù)提交,如何防止超賣、這里都會(huì)使用到,這篇文章主要介紹了RabbitMQ+redis+Redisson分布式鎖+seata實(shí)現(xiàn)訂單服務(wù)的流程分析,需要的朋友可以參考下2024-07-07

