Redis鍵值設(shè)計的具體實(shí)現(xiàn)
1 優(yōu)雅的key結(jié)構(gòu)
Redis的Key雖然可以自定義,但最好遵循下面的幾個最佳實(shí)踐約定:
- 遵循基本格式:[業(yè)務(wù)名稱]:[數(shù)據(jù)名]:[id]
- 長度不超過44字節(jié)
- 不包含特殊字符
例如:我們的登錄業(yè)務(wù),保存用戶信息,其key可以設(shè)計成如下格式:
這樣設(shè)計的好處:
- 可讀性強(qiáng)
- 避免key沖突
- 方便管理
- 更節(jié)省內(nèi)存: key是string類型,底層編碼包含int、embstr和raw三種。embstr在小于44字節(jié)使用,采用連續(xù)內(nèi)存空間,內(nèi)存占用更小。當(dāng)字節(jié)數(shù)大于44字節(jié)時,會轉(zhuǎn)為raw模式存儲,在raw模式下,內(nèi)存空間不是連續(xù)的,而是采用一個指針指向了另外一段內(nèi)存空間,在這段空間里存儲SDS內(nèi)容,這樣空間不連續(xù),訪問的時候性能也就會收到影響,還有可能產(chǎn)生內(nèi)存碎片
2 拒絕BigKey
BigKey通常以Key的大小和Key中成員的數(shù)量來綜合判定,例如:
- Key本身的數(shù)據(jù)量過大:一個String類型的Key,它的值為5 MB
- Key中的成員數(shù)過多:一個ZSET類型的Key,它的成員數(shù)量為10,000個
- Key中成員的數(shù)據(jù)量過大:一個Hash類型的Key,它的成員數(shù)量雖然只有1,000個但這些成員的Value(值)總大小為100 MB
那么如何判斷元素的大小呢?redis也給我們提供了命令
推薦值:
- 單個key的value小于10KB
- 對于集合類型的key,建議元素數(shù)量小于1000
2.1 BigKey的危害
網(wǎng)絡(luò)阻塞
- 對BigKey執(zhí)行讀請求時,少量的QPS就可能導(dǎo)致帶寬使用率被占滿,導(dǎo)致Redis實(shí)例,乃至所在物理機(jī)變慢
數(shù)據(jù)傾斜
- BigKey所在的Redis實(shí)例內(nèi)存使用率遠(yuǎn)超其他實(shí)例,無法使數(shù)據(jù)分片的內(nèi)存資源達(dá)到均衡
Redis阻塞
- 對元素較多的hash、list、zset等做運(yùn)算會耗時較舊,使主線程被阻塞
CPU壓力
- 對BigKey的數(shù)據(jù)序列化和反序列化會導(dǎo)致CPU的使用率飆升,影響Redis實(shí)例和本機(jī)其它應(yīng)用
2.2 如何發(fā)現(xiàn)BigKey
①redis-cli --bigkeys
利用redis-cli提供的–bigkeys參數(shù),可以遍歷分析所有key,并返回Key的整體統(tǒng)計信息與每個數(shù)據(jù)的Top1的big key
命令:redis-cli -a 密碼 --bigkeys
②scan掃描
自己編程,利用scan掃描Redis中的所有key,利用strlen、hlen等命令判斷key的長度(此處不建議使用MEMORY USAGE)
scan 命令調(diào)用完后每次會返回2個元素,第一個是下一次迭代的光標(biāo),第一次光標(biāo)會設(shè)置為0,當(dāng)最后一次scan 返回的光標(biāo)等于0時,表示整個scan遍歷結(jié)束了,第二個返回的是List,一個匹配的key的數(shù)組
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.選擇庫 jedis.select(0); } final static int STR_MAX_LEN = 10 * 1024; 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(); } } }
③第三方工具
- 利用第三方工具,如 Redis-Rdb-Tools 分析RDB快照文件,全面分析內(nèi)存使用情況
- https://github.com/sripathikrishnan/redis-rdb-tools
④網(wǎng)絡(luò)監(jiān)控
- 自定義工具,監(jiān)控進(jìn)出Redis的網(wǎng)絡(luò)數(shù)據(jù),超出預(yù)警值時主動告警
- 一般阿里云搭建的云服務(wù)器就有相關(guān)監(jiān)控頁面
2.3 如何刪除BigKey
BigKey內(nèi)存占用較多,即便時刪除這樣的key也需要耗費(fèi)很長時間,導(dǎo)致Redis主線程阻塞,引發(fā)一系列問題。
redis 3.0 及以下版本
- 如果是集合類型,則遍歷BigKey的元素,先逐個刪除子元素,最后刪除BigKey
Redis 4.0以后
- Redis在4.0后提供了異步刪除的命令:unlink
3 恰當(dāng)?shù)臄?shù)據(jù)類型
例1:比如存儲一個User對象,我們有三種存儲方式:
①方式一:json字符串
user:1 | {“name”: “Jack”, “age”: 21} |
---|
優(yōu)點(diǎn):實(shí)現(xiàn)簡單粗暴
缺點(diǎn):數(shù)據(jù)耦合,不夠靈活
②方式二:字段打散
user:1:name | Jack |
---|---|
user:1:age | 21 |
優(yōu)點(diǎn):可以靈活訪問對象任意字段
缺點(diǎn):占用空間大、沒辦法做統(tǒng)一控制
③方式三:hash(推薦)
user:1 | name | jack |
age | 21 |
優(yōu)點(diǎn):底層使用ziplist(壓縮列表),空間占用小,可以靈活訪問對象的任意字段
缺點(diǎn):代碼相對復(fù)雜(有工具類可以方便實(shí)現(xiàn))
例2:假如有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)存占用較多
可以通過hash-max-ziplist-entries配置entry上限。但是如果entry過多就會導(dǎo)致BigKey問題
方案一
拆分為string類型
key | value |
id:0 | value0 |
..... | ..... |
id:999999 | value999999 |
存在的問題:
string結(jié)構(gòu)底層沒有太多內(nèi)存優(yōu)化,內(nèi)存占用較多
想要批量獲取這些數(shù)據(jù)比較麻煩
方案二
拆分為小的hash,將 id / 100 作為key, 將id % 100 作為field,這樣每100個元素為一個Hash
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 |
package com.heima.test; 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.Pipeline; 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.選擇庫 jedis.select(0); } @Test void testSetBigKey() { Map<String, String> map = new HashMap<>(); for (int i = 1; i <= 650; i++) { map.put("hello_" + i, "world!"); } jedis.hmset("m2", map); } @Test void testBigHash() { Map<String, String> map = new HashMap<>(); for (int i = 1; i <= 100000; i++) { map.put("key_" + i, "value_" + i); } jedis.hmset("test:big:hash", map); } @Test void testBigString() { for (int i = 1; i <= 100000; i++) { jedis.set("test:str:key_" + i, "value_" + i); } } @Test void testSmallHash() { int hashSize = 100; Map<String, String> map = new HashMap<>(hashSize); for (int i = 1; i <= 100000; i++) { int k = (i - 1) / hashSize; int v = i % hashSize; map.put("key_" + v, "value_" + v); if (v == 0) { jedis.hmset("test:small:hash_" + k, map); } } } @AfterEach void tearDown() { if (jedis != null) { jedis.close(); } } }
4 總結(jié)
Key的最佳實(shí)踐
固定格式:[業(yè)務(wù)名]:[數(shù)據(jù)名]:[id]
足夠簡短:不超過44字節(jié)
不包含特殊字符
Value的最佳實(shí)踐:
- 合理的拆分?jǐn)?shù)據(jù),拒絕BigKey
- 選擇合適數(shù)據(jù)結(jié)構(gòu)
- Hash結(jié)構(gòu)的entry數(shù)量不要超過1000
- 設(shè)置合理的超時時間
到此這篇關(guān)于Redis鍵值設(shè)計的具體實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Redis鍵值設(shè)計內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis消息隊(duì)列實(shí)現(xiàn)異步秒殺功能
在高并發(fā)場景下,為了提高秒殺業(yè)務(wù)的性能,可將部分工作交給 Redis 處理,并通過異步方式執(zhí)行,Redis 提供了多種數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)消息隊(duì)列,總結(jié)三種,本文詳細(xì)介紹Redis消息隊(duì)列實(shí)現(xiàn)異步秒殺功能,感興趣的朋友一起看看吧2025-04-04利用redis實(shí)現(xiàn)分布式鎖,快速解決高并發(fā)時的線程安全問題
這篇文章主要介紹了利用redis實(shí)現(xiàn)分布式鎖,快速解決高并發(fā)時的線程安全問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-01-01無法連接redis服務(wù)器問題的解決辦法(非常詳細(xì)!)
這篇文章主要介紹了如何解決Spring?Boot項(xiàng)目連接Redis失敗的問題,通過修改Redis配置文件、添加防火墻白名單或關(guān)閉防火墻,并使用RESP工具進(jìn)行測試,需要的朋友可以參考下2025-02-02