Redis面試必備之緩存設(shè)計(jì)規(guī)范與性能優(yōu)化詳解
一、Redis Key-Value設(shè)計(jì)規(guī)范&性能優(yōu)化
1. key名設(shè)計(jì)規(guī)范
(1)【建議】: 可讀性和可管理性
以業(yè)務(wù)名(或數(shù)據(jù)庫(kù)名)為前綴(防止key沖突),用冒號(hào)分隔,比如業(yè)務(wù)名:表名:id
(2)【建議】:簡(jiǎn)潔性
保證語(yǔ)義的前提下,控制key的長(zhǎng)度,當(dāng)key較多時(shí),內(nèi)存占用也不容忽視,例如:
(3)【強(qiáng)制】:不要包含特殊字符
反例:包含空格、換行、單雙引號(hào)以及其他轉(zhuǎn)義字符
2. Value設(shè)計(jì)規(guī)范
(1)【強(qiáng)制】:拒絕bigkey(防止網(wǎng)卡流量、慢查詢(xún))
在Redis中,一個(gè)字符串最大512MB,一個(gè)二級(jí)數(shù)據(jù)結(jié)構(gòu)(例如hash、list、set、zset)可以存儲(chǔ)大約40億個(gè)(2^32-1)個(gè)元素,但實(shí)際中如果下面兩種情況,我就會(huì)認(rèn)為它是bigkey。
- 字符串類(lèi)型:它的big體現(xiàn)在單個(gè)value值很大,一般認(rèn)為超過(guò)10KB就是bigkey。
- 非字符串類(lèi)型:哈希、列表、集合、有序集合,它們的big體現(xiàn)在元素個(gè)數(shù)太多。
一般來(lái)說(shuō),string類(lèi)型控制在10KB以?xún)?nèi);
hash、list、set、zset元素個(gè)數(shù)不要超過(guò)5000。
反例:一個(gè)包含200萬(wàn)個(gè)元素的list。
3. bigkey性能優(yōu)化
bigkey的危害:
1.導(dǎo)致redis阻塞
2.網(wǎng)絡(luò)擁塞
bigkey也就意味著每次獲取要產(chǎn)生的網(wǎng)絡(luò)流量較大;
假設(shè)一個(gè)bigkey為1MB,客戶(hù)端每秒訪問(wèn)量為1000,那么每秒產(chǎn)生1000MB的流量,對(duì)于普通的千兆網(wǎng)卡(按照字節(jié)算是128MB/s)的服務(wù)器來(lái)說(shuō)簡(jiǎn)直是滅頂之災(zāi),而且一般服務(wù)器會(huì)采用單機(jī)多實(shí)例的方式來(lái)部署,也就是說(shuō)一個(gè)bigkey可能會(huì)對(duì)其他實(shí)例也造成影響,其后果不堪設(shè)想。
3.過(guò)期刪除
有個(gè)bigkey,它安分守己(只執(zhí)行簡(jiǎn)單的命令,例如hget、lpop、zscore等),但它設(shè)置了過(guò)期時(shí)間,當(dāng)它過(guò)期后,會(huì)被刪除,如果沒(méi)有使用Redis 4.0的過(guò)期異步刪除(lazyfree-lazy- expire yes),就會(huì)存在阻塞Redis的可能性。
bigkey的產(chǎn)生:
一般來(lái)說(shuō),bigkey的產(chǎn)生都是由于程序設(shè)計(jì)不當(dāng),或者對(duì)于數(shù)據(jù)規(guī)模預(yù)料不清楚造成的,來(lái)看幾個(gè)例子:
- 社交類(lèi): 粉絲列表,如果某些明星或者大v不精心設(shè)計(jì)下,必是bigkey。
- 統(tǒng)計(jì)類(lèi): 例如按天存儲(chǔ)某項(xiàng)功能或者網(wǎng)站的用戶(hù)集合,除非沒(méi)幾個(gè)人用,否則必是bigkey。
- 緩存類(lèi): 將數(shù)據(jù)從數(shù)據(jù)庫(kù)load出來(lái)序列化放到Redis里,這個(gè)方式非常常用,但有兩個(gè)地方需要注意,第一,是不是有必要把所有字段都緩存;第二,有沒(méi)有相關(guān)關(guān)聯(lián)的數(shù)據(jù),有的同學(xué)為了圖方便把相關(guān)數(shù)據(jù)都存一個(gè)key下,產(chǎn)生bigkey。
如何優(yōu)化bigkey
1、拆
如果是大List(big list),那么就可以拆成多個(gè)List:
比如拆成:list1、list2、...listN
如果是一個(gè)大的哈希表(big hash),可以將數(shù)據(jù)分段存儲(chǔ):
比如一個(gè)大的key,假設(shè)存了1百萬(wàn)的用戶(hù)數(shù)據(jù),可以拆分成 200個(gè)key,每個(gè)key下面存放5000個(gè)用戶(hù)數(shù)據(jù)
如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出來(lái)(例如有時(shí)候僅僅需要 hmget,而不是hgetall),刪除也是一樣,盡量使用優(yōu)雅的方式來(lái)處理。
2、選擇合適的數(shù)據(jù)類(lèi)型【推薦】
最好的優(yōu)化方案其實(shí)是在設(shè)計(jì)階段,所以我們?cè)谑褂肦edis時(shí),在設(shè)計(jì)階段就應(yīng)該盡量避免bigkey,所以選擇合適的數(shù)據(jù)類(lèi)型尤為重要。
例如:實(shí)體類(lèi)型(要合理控制和使用數(shù)據(jù)結(jié)構(gòu),但也要注意節(jié)省內(nèi)存和性能質(zhì)檢的平衡)
錯(cuò)誤的做法:
set user:1:name tom set user:1:age 19 set user:1:favor football
正確的做法:
hmset user:1 name tom age 19 favor football
3、控制key的生命周期,redis不是垃圾桶,當(dāng)不需要使用的數(shù)據(jù),及時(shí)過(guò)期清理【推薦】
建議使用Expire設(shè)置過(guò)期時(shí)間
條件允許可以打散過(guò)期時(shí)間,防止幾種過(guò)期
比如:設(shè)置key的過(guò)期時(shí)間時(shí),采用固定過(guò)期時(shí)間+一定范圍內(nèi)的隨機(jī)數(shù)
二、Redis命令的使用規(guī)范&性能優(yōu)化
1、使用O(N)類(lèi)型的命令要注意關(guān)注N的數(shù)量【推薦】
比如hgetall、lrange、smembers、zrange、sinter等并非不能使用。
但是在使用的時(shí)候一定要明確N的值,不然就可能由于查詢(xún)數(shù)據(jù)太大導(dǎo)致redis阻塞。
建議:有遍歷的需求時(shí)可以使用hscan、sscan、zscan代替
2、生產(chǎn)環(huán)境禁用部分高危命令【推薦】
禁止線上使用keys、flushall、flushdb等,通過(guò)redis的rename機(jī)制禁掉命令。
當(dāng)有需要掃描的需要時(shí),建議使用scan方式漸進(jìn)式處理
3、合理使用select【推薦】
redis的多數(shù)據(jù)庫(kù)較弱,使用數(shù)字進(jìn)行區(qū)分,很多客戶(hù)端支持較差,同時(shí)多業(yè)務(wù)用多數(shù)據(jù)庫(kù)實(shí)際還是單線程處理,會(huì)有干擾
所以建議redis使用數(shù)據(jù)庫(kù)只用序號(hào)0的數(shù)據(jù)庫(kù)即可,在0數(shù)據(jù)庫(kù)里采用key前綴區(qū)分業(yè)務(wù)即可
4、使用批量操作提高效率【推薦】
當(dāng)我們要插入多個(gè)key時(shí),可以采用一些批量命令代替單個(gè)命令,提高查詢(xún)效率,例如:
- 原生命令:例如mget、mset。
- 非原生命令:可以使用pipeline提高效率。
但要注意控制一次批量操作的元素個(gè)數(shù)(例如500以?xún)?nèi),實(shí)際也和元素字節(jié)數(shù)有關(guān))。
注意兩者不同:
- 原生命令是原子操作,pipeline是非原子操作。
- pipeline可以打包不同的命令,原生命令做不到
- pipeline需要客戶(hù)端和服務(wù)端同時(shí)支持。
5、redis事務(wù)功能較弱,不建議過(guò)多使用redis的事務(wù)命令
如果業(yè)務(wù)上有需要,可以使用lua替代【建議】
三、客戶(hù)端使用規(guī)范&性能優(yōu)化
1、避免多個(gè)應(yīng)用使用同一個(gè)Redis實(shí)例【推薦】
錯(cuò)誤的做法:
多個(gè)業(yè)務(wù)線公用同一個(gè)redis實(shí)例,比如訂單、庫(kù)存、權(quán)限都用同一個(gè)redis實(shí)例,只要有一塊業(yè)務(wù)有阻塞,所有業(yè)務(wù)都會(huì)受影響。
正確的做法:
不相干的業(yè)務(wù)拆分為獨(dú)立的redis實(shí)例,比如訂單、庫(kù)存、權(quán)限拆分為3個(gè)redis實(shí)例。
2、客戶(hù)端連接使用帶有連接池的連接,可以有效控制連接,同時(shí)提高效率:
Jedis使用連接池方式:
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(5); jedisPoolConfig.setMaxIdle(2); jedisPoolConfig.setTestOnBorrow(true); JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.0.60", 6379, 3000, null); Jedis jedis = null;
使用連接池執(zhí)行命令:
try { jedis = jedisPool.getResource(); //執(zhí)行具體的命令 jedis.executeCommand() } catch (Exception e) { logger.error("op key {} error: " + e.getMessage(), key, e); } finally { //注意這里不是關(guān)閉連接,在JedisPool模式下,Jedis會(huì)被歸還給資源池。 if (jedis != null) jedis.close(); }
3、連接池配置參數(shù)優(yōu)化建議
1、maxTotal優(yōu)化:最大連接數(shù)(早期版本叫maxActive)【建議】
實(shí)際上最大連接數(shù)該如何優(yōu)化,是一個(gè)很難回答的問(wèn)題,考慮的因素有很多:
比如:
- 業(yè)務(wù)希望的Redis并發(fā)量
- 客戶(hù)端執(zhí)行命令時(shí)間
- Redis資源:例如nodes(實(shí)例應(yīng)用個(gè)數(shù))* maxTotal是不能超過(guò)Redis的最大連接數(shù)maxClients
- 資源開(kāi)銷(xiāo):例如雖然希望控制空閑連接(連接池此刻可馬上使用的連接),但是又不希望因?yàn)檫B接池的頻繁釋放、創(chuàng)建連接造成不必要的開(kāi)銷(xiāo)。
以一個(gè)例子說(shuō)明,假設(shè):
一次命令時(shí)間(borrow|return resource + Jedis執(zhí)行命令(含網(wǎng)絡(luò)) )的平均耗時(shí)約為1ms,一個(gè)連接的QPS大約是1000
業(yè)務(wù)期望的QPS是50000
那么理論上需要的資源池大小是50000 / 1000 = 50個(gè)。但事實(shí)上這是個(gè)理論值,還要考慮到要比理論值預(yù)留一些資源,通常來(lái)講maxTotal可以比理論值大一些。
但這個(gè)值不是越大越好,一方面連接太多占用客戶(hù)端和服務(wù)端資源,另一方面對(duì)于Redis這種高QPS的服務(wù)器,一個(gè)大命令的阻塞即使設(shè)置再大資源池仍然會(huì)無(wú)濟(jì)于事。
2、maxldle和minldle優(yōu)化:(資源池允許最大空閑連接數(shù)和資源池確保最少空閑連接數(shù))【建議】
maxldle(最大空閑連接數(shù)):
maxIdle實(shí)際上才是業(yè)務(wù)需要的最大連接數(shù),maxTotal是為了給出余量,所以maxIdle不要設(shè)置過(guò)小,否則會(huì)有new Jedis(新連接)開(kāi)銷(xiāo)。
連接池的最佳性能是maxTotal = maxIdle,這樣就避免連接池伸縮帶來(lái)的性能干擾。但是如果并發(fā)量不大或者maxTotal設(shè)置過(guò)高,會(huì)導(dǎo)致不必要的連接資源浪費(fèi)。一般推薦maxIdle可以設(shè)置為按上面的業(yè)務(wù)期望QPS計(jì)算出來(lái)的理論連接,maxTotal可以再放大一倍。
minIdle(最小空閑連接數(shù)):
minIdle與其說(shuō)是最小空閑連接數(shù),不如說(shuō)是"至少需要保持的空閑連接數(shù)",在使用連接的過(guò)程中,如果連接數(shù)超過(guò)了minIdle,那么繼續(xù)建立連接,如果超過(guò)了maxIdle,當(dāng)超過(guò)的連接執(zhí)行完業(yè)務(wù)后會(huì)慢慢被移出連接池釋放掉
所以最小空閑連接數(shù)需要根據(jù)自己的業(yè)務(wù)規(guī)模和客戶(hù)端規(guī)模自行評(píng)估配置
【建議】:
如果你的系統(tǒng)QPS很高,系統(tǒng)啟動(dòng)完馬上就會(huì)有很多的請(qǐng)求過(guò)來(lái),那么可以給redis連接池做預(yù)熱,比如快速的創(chuàng)建一些redis連接,執(zhí)行簡(jiǎn)單命令,類(lèi)似ping(),快速的將連接池里的空閑連接提升到minIdle的數(shù)量。
連接池預(yù)熱示例代碼:
List<Jedis> minIdleJedisList = new ArrayList<Jedis>(jedisPoolConfig.getMinIdle()); for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) { Jedis jedis = null; try { jedis = pool.getResource(); minIdleJedisList.add(jedis); jedis.ping(); } catch (Exception e) { logger.error(e.getMessage(), e); } finally { //注意,這里不能馬上close將連接還回連接池,否則最后連接池里只會(huì)建立1個(gè)連接。。 //jedis.close(); } } //統(tǒng)一將預(yù)熱的連接還回連接池 for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) { Jedis jedis = null; try { jedis = minIdleJedisList.get(i); //將連接歸還回連接池 jedis.close(); } catch (Exception e) { logger.error(e.getMessage(), e); } finally { } }
總之,要根據(jù)實(shí)際系統(tǒng)的QPS和調(diào)用redis客戶(hù)端的規(guī)模整體評(píng)估每個(gè)節(jié)點(diǎn)所使用的連接池大小
3、【建議】高并發(fā)下,建議客戶(hù)端添加熔斷功能
(例如接入sentinel、hystrix)
4、【推薦】設(shè)置合理的密碼
有必要可以使用SSL加密訪問(wèn)
5、【建議】設(shè)置合適的緩存淘汰策略
LRU 算法(Least Recently Used,最近最少使用)
淘汰很久沒(méi)被訪問(wèn)過(guò)的數(shù)據(jù),以最近一次訪問(wèn)時(shí)間作為參考。
LFU 算法(Least Frequently Used,最不經(jīng)常使用)
淘汰最近一段時(shí)間被訪問(wèn)次數(shù)最少的數(shù)據(jù),以次數(shù)作為參考。
當(dāng)存在熱點(diǎn)數(shù)據(jù)時(shí),LRU的效率很好,但偶發(fā)性的、周期性的批量操作會(huì)導(dǎo)致LRU命中率急劇下降,緩存污染情況比較嚴(yán)重。這時(shí)使用LFU可能更好點(diǎn)。
根據(jù)自身業(yè)務(wù)類(lèi)型,配置好maxmemory-policy(默認(rèn)是noeviction),推薦使用volatile-lru。如果不設(shè)置最大內(nèi)存,當(dāng) Redis 內(nèi)存超出物理內(nèi)存限制時(shí),內(nèi)存的數(shù)據(jù)會(huì)開(kāi)始和磁盤(pán)產(chǎn)生頻繁的交換 (swap),會(huì)讓 Redis 的性能急劇下降。
當(dāng)Redis運(yùn)行在主從模式時(shí),只有主結(jié)點(diǎn)才會(huì)執(zhí)行過(guò)期刪除策略,然后把刪除操作”del key”同
步到從結(jié)點(diǎn)刪除數(shù)據(jù)。
四、系統(tǒng)內(nèi)核參數(shù)優(yōu)化
1、vm.swapiness配置,根據(jù)linux版本選擇配置(默認(rèn)0)
swap對(duì)于操作系統(tǒng)來(lái)說(shuō)比較重要,當(dāng)物理內(nèi)存不足時(shí),可以將一部分內(nèi)存頁(yè)進(jìn)行swap到硬盤(pán)上,以解燃眉之急。
但世界上沒(méi)有免費(fèi)午餐,swap空間由硬盤(pán)提供,對(duì)于需要高并發(fā)、高吞吐的應(yīng)用來(lái)說(shuō),磁盤(pán)IO通常會(huì)成為系統(tǒng)瓶頸。
在Linux中,并不是要等到所有物理內(nèi)存都使用完才會(huì)使用到swap,系統(tǒng)參數(shù)swppiness會(huì)決定操作系統(tǒng)使用swap的傾向程度。swappiness的取值范圍是0~100,swappiness的值越大,說(shuō)明操作系統(tǒng)可能使用swap的概率越高,swappiness值越低,表示操作系統(tǒng)更加傾向于使用物理內(nèi)存。
swappiness的取值越大,說(shuō)明操作系統(tǒng)可能使用swap的概率越高,越低則越傾向于使用物理內(nèi)存。
如果linux內(nèi)核版本<3.5,那么swapiness設(shè)置為0,這樣系統(tǒng)寧愿swap也不會(huì)oom kille(殺掉進(jìn)程)
如果linux內(nèi)核版本>=3.5,那么swapiness設(shè)置為1,這樣系統(tǒng)寧愿swap也不會(huì)oom killer
一般需要保證redis不會(huì)被kill掉:
cat /proc/version #查看linux內(nèi)核版本 echo 1 > /proc/sys/vm/swappiness echo vm.swapiness=1 >> /etc/sysctl.conf
PS:OOM killer 機(jī)制是指Linux操作系統(tǒng)發(fā)現(xiàn)可用內(nèi)存不足時(shí),強(qiáng)制殺死一些用戶(hù)進(jìn)程(非內(nèi)核進(jìn)程),來(lái)保證系統(tǒng)有足夠的可用內(nèi)存進(jìn)行分配。
2、vm.overcommit_memory配置改為1(默認(rèn)0)
0:表示內(nèi)核將檢查是否有足夠的可用物理內(nèi)存(實(shí)際不一定用滿(mǎn))供應(yīng)用進(jìn)程使用;
- 如果有足夠的可用物理內(nèi)存,內(nèi)存申請(qǐng)?jiān)试S;
- 否則,內(nèi)存申請(qǐng)失敗,并把錯(cuò)誤返回給應(yīng)用進(jìn)程
1:表示內(nèi)核允許分配所有的物理內(nèi)存,而不管當(dāng)前的內(nèi)存狀態(tài)如何;
如果是0的話(huà),可能導(dǎo)致類(lèi)似fork等操作執(zhí)行失敗,申請(qǐng)不到足夠的內(nèi)存空間
Redis建議把這個(gè)值設(shè)置為1,就是為了讓fork操作能夠在低內(nèi)存下也執(zhí)行成功。
cat /proc/sys/vm/overcommit_memory echo "vm.overcommit_memory=1" >> /etc/sysctl.conf sysctl vm.overcommit_memory=1
3、合理設(shè)置文件句柄數(shù)
操作系統(tǒng)進(jìn)程試圖打開(kāi)一個(gè)文件(或者叫句柄),但是現(xiàn)在進(jìn)程打開(kāi)的句柄數(shù)已經(jīng)達(dá)到了上限,繼續(xù)打開(kāi)會(huì)報(bào)錯(cuò):“Too many open files”
ulimit ‐a #查看系統(tǒng)文件句柄數(shù),看open files那項(xiàng) ulimit ‐n 65535 #設(shè)置系統(tǒng)文件句柄數(shù)
總結(jié)
本文梳理了在使用Redis過(guò)程需要遵循的一些最佳實(shí)踐,包括針對(duì)架構(gòu)維度的一些深入性能優(yōu)化的知識(shí),如果面試官問(wèn)你:"說(shuō)下在使用Redis的過(guò)程中,需要注意哪些規(guī)范?",如果你按照本文的思路回答,肯定能讓面試官眼前一亮,offer自然就到手了。
以上就是Redis面試必備之緩存設(shè)計(jì)規(guī)范與性能優(yōu)化詳解的詳細(xì)內(nèi)容,更多關(guān)于Redis緩存設(shè)計(jì)規(guī)范與性能優(yōu)化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Redis在項(xiàng)目中常見(jiàn)的12種使用場(chǎng)景示例和說(shuō)明
Redis是一個(gè)開(kāi)源的高性能鍵值對(duì)數(shù)據(jù)庫(kù),它以其內(nèi)存中數(shù)據(jù)存儲(chǔ)、鍵過(guò)期策略、持久化、事務(wù)、豐富的數(shù)據(jù)類(lèi)型支持以及原子操作等特性,在許多項(xiàng)目中扮演著關(guān)鍵角色,以下是整理的12個(gè)Redis在項(xiàng)目中常見(jiàn)的使用場(chǎng)景舉例說(shuō)明和解釋2024-06-06nestjs使用redis實(shí)現(xiàn)ip限流的步驟詳解
如果使用nestjs開(kāi)發(fā)接口并部署之后,我們通常需要考慮到接口是否會(huì)被惡意盜刷消耗過(guò)多的資源,一個(gè)簡(jiǎn)單的方式就是限制在單位時(shí)間內(nèi)的訪問(wèn)次數(shù),所以本文給大家介紹了nestjs使用redis實(shí)現(xiàn)ip限流的步驟,需要的朋友可以參考下2025-01-01springboot使用Redis作緩存使用入門(mén)教程
這篇文章主要介紹了springboot使用Redis作緩存使用入門(mén)教程,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-07-07Redis 實(shí)現(xiàn)好友關(guān)注和關(guān)注推送的示例代碼
本文介紹了使用Redis實(shí)現(xiàn)好友關(guān)注和關(guān)注推送功能,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-03-03如何自定義redis工具jar包供其他SpringBoot項(xiàng)目直接使用
這篇文章主要介紹了如何自定義redis工具jar包供其他SpringBoot項(xiàng)目直接使用,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03Redis遠(yuǎn)程連接Redis客戶(hù)端的實(shí)現(xiàn)步驟
本文主要介紹了Redis遠(yuǎn)程連接Redis客戶(hù)端的實(shí)現(xiàn)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06分布式使用Redis實(shí)現(xiàn)數(shù)據(jù)庫(kù)對(duì)象自增主鍵ID
本文介紹在分布式項(xiàng)目中使用Redis生成對(duì)象的自增主鍵ID,通過(guò)Redis的INCR等命令實(shí)現(xiàn)計(jì)數(shù)器功能,具有一定的參考價(jià)值,感興趣的可以了解一下2024-12-12