轉(zhuǎn)載鏈接:https://juejin.im/post/5e79702af265da570c75580a
Redis 是基于單線程模型實(shí)現(xiàn)的廉嚼,也就是 Redis 是使用一個(gè)線程來處理所有的客戶端請求的,盡管 Redis 使用了非阻塞式 IO倒戏,并且對各種命令都做了優(yōu)化(大部分命令操作時(shí)間復(fù)雜度都是 O(1))怠噪,但由于 Redis 是單線程執(zhí)行的特點(diǎn),因此它對性能的要求更加苛刻杜跷,本文我們將通過一些優(yōu)化手段傍念,讓 Redis 更加高效的運(yùn)行。
本文我們將使用以下手段葱椭,來提升 Redis 的運(yùn)行速度:
- 縮短鍵值對的存儲長度捂寿;
- 使用 lazy free(延遲刪除)特性;
- 設(shè)置鍵值的過期時(shí)間;
- 禁用長耗時(shí)的查詢命令捉撮;
- 使用 slowlog 優(yōu)化耗時(shí)命令滋早;
- 使用 Pipeline 批量操作數(shù)據(jù);
- 避免大量數(shù)據(jù)同時(shí)失效驳概;
- 客戶端使用優(yōu)化;
- 限制 Redis 內(nèi)存大锌趵怠顺又;
- 使用物理機(jī)而非虛擬機(jī)安裝 Redis 服務(wù);
- 檢查數(shù)據(jù)持久化策略等孵;
- 禁用 THP 特性稚照;
- 使用分布式架構(gòu)來增加讀寫速度。
1.縮短鍵值對的存儲長度
鍵值對的長度是和性能成反比的俯萌,比如我們來做一組寫入數(shù)據(jù)的性能測試果录,執(zhí)行結(jié)果如下:
從以上數(shù)據(jù)可以看出,在 key 不變的情況下咐熙,value 值越大操作效率越慢弱恒,因?yàn)?Redis 對于同一種數(shù)據(jù)類型會使用不同的內(nèi)部編碼進(jìn)行存儲,比如字符串的內(nèi)部編碼就有三種:int(整數(shù)編碼)棋恼、raw(優(yōu)化內(nèi)存分配的字符串編碼)返弹、embstr(動態(tài)字符串編碼),這是因?yàn)?Redis 的作者是想通過不同編碼實(shí)現(xiàn)效率和空間的平衡爪飘,然而數(shù)據(jù)量越大使用的內(nèi)部編碼就越復(fù)雜义起,而越是復(fù)雜的內(nèi)部編碼存儲的性能就越低。
這還只是寫入時(shí)的速度师崎,當(dāng)鍵值對內(nèi)容較大時(shí)默终,還會帶來另外幾個(gè)問題:
- 內(nèi)容越大需要的持久化時(shí)間就越長,需要掛起的時(shí)間越長,Redis 的性能就會越低穷蛹;
- 內(nèi)容越大在網(wǎng)絡(luò)上傳輸?shù)膬?nèi)容就越多土陪,需要的時(shí)間就越長,整體的運(yùn)行速度就越低肴熏;
- 內(nèi)容越大占用的內(nèi)存就越多鬼雀,就會更頻繁的觸發(fā)內(nèi)存淘汰機(jī)制,從而給 Redis 帶來了更多的運(yùn)行負(fù)擔(dān)蛙吏。
因此在保證完整語義的同時(shí)源哩,我們要盡量的縮短鍵值對的存儲長度,必要時(shí)要對數(shù)據(jù)進(jìn)行序列化和壓縮再存儲鸦做,以 Java 為例励烦,序列化我們可以使用 protostuff 或 kryo,壓縮我們可以使用 snappy泼诱。
2.使用 lazy free 特性
lazy free 特性是 Redis 4.0 新增的一個(gè)非常使用的功能坛掠,它可以理解為惰性刪除或延遲刪除。意思是在刪除的時(shí)候提供異步延時(shí)釋放鍵值的功能治筒,把鍵值釋放操作放在 BIO(Background I/O) 單獨(dú)的子線程處理中屉栓,以減少刪除刪除對 Redis 主線程的阻塞,可以有效地避免刪除 big key 時(shí)帶來的性能和可用性問題耸袜。
lazy free 對應(yīng)了 4 種場景友多,默認(rèn)都是關(guān)閉的:
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
slave-lazy-flush no
復(fù)制代碼
它們代表的含義如下:
- lazyfree-lazy-eviction:表示當(dāng) Redis 運(yùn)行內(nèi)存超過 maxmeory 時(shí),是否開啟 lazy free 機(jī)制刪除堤框;
- lazyfree-lazy-expire:表示設(shè)置了過期時(shí)間的鍵值域滥,當(dāng)過期之后是否開啟 lazy free 機(jī)制刪除;
- lazyfree-lazy-server-del:有些指令在處理已存在的鍵時(shí)蜈抓,會帶有一個(gè)隱式的 del 鍵的操作启绰,比如 rename 命令,當(dāng)目標(biāo)鍵已存在资昧,Redis 會先刪除目標(biāo)鍵酬土,如果這些目標(biāo)鍵是一個(gè) big key荆忍,就會造成阻塞刪除的問題格带,此配置表示在這種場景中是否開啟 lazy free 機(jī)制刪除;
- slave-lazy-flush:針對 slave(從節(jié)點(diǎn)) 進(jìn)行全量數(shù)據(jù)同步刹枉,slave 在加載 master 的 RDB 文件前叽唱,會運(yùn)行 flushall 來清理自己的數(shù)據(jù),它表示此時(shí)是否開啟 lazy free 機(jī)制刪除微宝。
建議開啟其中的 lazyfree-lazy-eviction棺亭、lazyfree-lazy-expire、lazyfree-lazy-server-del 等配置蟋软,這樣就可以有效的提高主線程的執(zhí)行效率镶摘。
3.設(shè)置鍵值的過期時(shí)間
我們應(yīng)該根據(jù)實(shí)際的業(yè)務(wù)情況嗽桩,對鍵值設(shè)置合理的過期時(shí)間,這樣 Redis 會幫你自動清除過期的鍵值對凄敢,以節(jié)約對內(nèi)存的占用碌冶,以避免鍵值過多的堆積,頻繁的觸發(fā)內(nèi)存淘汰策略涝缝。
4.禁用長耗時(shí)的查詢命令
Redis 絕大多數(shù)讀寫命令的時(shí)間復(fù)雜度都在 O(1) 到 O(N) 之間扑庞,在官方文檔對每命令都有時(shí)間復(fù)雜度說明,地址:redis.io/commands拒逮,如下…
其中 O(1) 表示可以安全使用的罐氨,而 O(N) 就應(yīng)該當(dāng)心了,N 表示不確定滩援,數(shù)據(jù)越大查詢的速度可能會越慢栅隐。因?yàn)?Redis 只用一個(gè)線程來做數(shù)據(jù)查詢,如果這些指令耗時(shí)很長玩徊,就會阻塞 Redis约啊,造成大量延時(shí)。
要避免 O(N) 命令對 Redis 造成的影響佣赖,可以從以下幾個(gè)方面入手改造:
- 決定禁止使用 keys 命令恰矩;
- 避免一次查詢所有的成員,要使用 scan 命令進(jìn)行分批的憎蛤,游標(biāo)式的遍歷外傅;
- 通過機(jī)制嚴(yán)格控制 Hash、Set俩檬、Sorted Set 等結(jié)構(gòu)的數(shù)據(jù)大形取;
- 將排序棚辽、并集技竟、交集等操作放在客戶端執(zhí)行,以減少 Redis 服務(wù)器運(yùn)行壓力屈藐;
- 刪除 (del) 一個(gè)大數(shù)據(jù)的時(shí)候榔组,可能會需要很長時(shí)間,所以建議用異步刪除的方式 unlink联逻,它會啟動一個(gè)新的線程來刪除目標(biāo)數(shù)據(jù)搓扯,而不阻塞 Redis 的主線程。
5.使用 slowlog 優(yōu)化耗時(shí)命令
我們可以使用 slowlog 功能找出最耗時(shí)的 Redis 命令進(jìn)行相關(guān)的優(yōu)化包归,以提升 Redis 的運(yùn)行速度锨推,慢查詢有兩個(gè)重要的配置項(xiàng):
-
slowlog-log-slower-than
:用于設(shè)置慢查詢的評定時(shí)間,也就是說超過此配置項(xiàng)的命令,將會被當(dāng)成慢操作記錄在慢查詢?nèi)罩局谢豢桑鼒?zhí)行單位是微秒 (1 秒等于 1000000 微秒)椎椰; -
slowlog-max-len
:用來配置慢查詢?nèi)罩镜淖畲笥涗洈?shù)。
我們可以根據(jù)實(shí)際的業(yè)務(wù)情況進(jìn)行相應(yīng)的配置沾鳄,其中慢日志是按照插入的順序倒序存入慢查詢?nèi)罩局屑笫叮覀兛梢允褂?slowlog get n
來獲取相關(guān)的慢查詢?nèi)罩荆僬业竭@些慢查詢對應(yīng)的業(yè)務(wù)進(jìn)行相關(guān)的優(yōu)化洞渔。
6.使用 Pipeline 批量操作數(shù)據(jù)
Pipeline (管道技術(shù)) 是客戶端提供的一種批處理技術(shù)套媚,用于一次處理多個(gè) Redis 命令,從而提高整個(gè)交互的性能磁椒。
我們使用 Java 代碼來測試一下 Pipeline 和普通操作的性能對比堤瘤,Pipeline 的測試代碼如下:
public class PipelineExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 記錄執(zhí)行開始時(shí)間
long beginTime = System.currentTimeMillis();
// 獲取 Pipeline 對象
Pipeline pipe = jedis.pipelined();
// 設(shè)置多個(gè) Redis 命令
for (int i = 0; i < 100; i++) {
pipe.set("key" + i, "val" + i);
pipe.del("key"+i);
}
// 執(zhí)行命令
pipe.sync();
// 記錄執(zhí)行結(jié)束時(shí)間
long endTime = System.currentTimeMillis();
System.out.println("執(zhí)行耗時(shí):" + (endTime - beginTime) + "毫秒");
}
}
復(fù)制代碼
以上程序執(zhí)行結(jié)果為:
執(zhí)行耗時(shí):297毫秒
普通的操作代碼如下:
public class PipelineExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 記錄執(zhí)行開始時(shí)間
long beginTime = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
jedis.set("key" + i, "val" + i);
jedis.del("key"+i);
}
// 記錄執(zhí)行結(jié)束時(shí)間
long endTime = System.currentTimeMillis();
System.out.println("執(zhí)行耗時(shí):" + (endTime - beginTime) + "毫秒");
}
}
復(fù)制代碼
以上程序執(zhí)行結(jié)果為:
執(zhí)行耗時(shí):17276毫秒
從以上的結(jié)果可以看出,管道的執(zhí)行時(shí)間是 297 毫秒浆熔,而普通命令執(zhí)行時(shí)間是 17276 毫秒本辐,管道技術(shù)要比普通的執(zhí)行大約快了 58 倍。
7.避免大量數(shù)據(jù)同時(shí)失效
Redis 過期鍵值刪除使用的是貪心策略医增,它每秒會進(jìn)行 10 次過期掃描慎皱,此配置可在 redis.conf 進(jìn)行配置,默認(rèn)值是 hz 10
叶骨,Redis 會隨機(jī)抽取 20 個(gè)值茫多,刪除這 20 個(gè)鍵中過期的鍵,如果過期 key 的比例超過 25% 忽刽,重復(fù)執(zhí)行此流程天揖,如下圖所示:
如果在大型系統(tǒng)中有大量緩存在同一時(shí)間同時(shí)過期,那么會導(dǎo)致 Redis 循環(huán)多次持續(xù)掃描刪除過期字典跪帝,直到過期字典中過期鍵值被刪除的比較稀疏為止今膊,而在整個(gè)執(zhí)行過程會導(dǎo)致 Redis 的讀寫出現(xiàn)明顯的卡頓,卡頓的另一種原因是內(nèi)存管理器需要頻繁回收內(nèi)存頁伞剑,因此也會消耗一定的 CPU斑唬。
為了避免這種卡頓現(xiàn)象的產(chǎn)生,我們需要預(yù)防大量的緩存在同一時(shí)刻一起過期黎泣,就簡單的解決方案就是在過期時(shí)間的基礎(chǔ)上添加一個(gè)指定范圍的隨機(jī)數(shù)恕刘。
8.客戶端使用優(yōu)化
在客戶端的使用上我們除了要盡量使用 Pipeline 的技術(shù)外,還需要注意要盡量使用 Redis 連接池聘裁,而不是頻繁創(chuàng)建銷毀 Redis 連接雪营,這樣就可以減少網(wǎng)絡(luò)傳輸次數(shù)和減少了非必要調(diào)用指令弓千。
9.限制 Redis 內(nèi)存大小
在 64 位操作系統(tǒng)中 Redis 的內(nèi)存大小是沒有限制的衡便,也就是配置項(xiàng) maxmemory <bytes>
是被注釋掉的,這樣就會導(dǎo)致在物理內(nèi)存不足時(shí),使用 swap 空間既交換空間镣陕,而當(dāng)操心系統(tǒng)將 Redis 所用的內(nèi)存分頁移至 swap 空間時(shí)谴餐,將會阻塞 Redis 進(jìn)程,導(dǎo)致 Redis 出現(xiàn)延遲呆抑,從而影響 Redis 的整體性能岂嗓。因此我們需要限制 Redis 的內(nèi)存大小為一個(gè)固定的值,當(dāng) Redis 的運(yùn)行到達(dá)此值時(shí)會觸發(fā)內(nèi)存淘汰策略鹊碍,內(nèi)存淘汰策略在 Redis 4.0 之后有 8 種:
- noeviction:不淘汰任何數(shù)據(jù)厌殉,當(dāng)內(nèi)存不足時(shí),新增操作會報(bào)錯(cuò)侈咕,Redis 默認(rèn)內(nèi)存淘汰策略公罕;
- allkeys-lru:淘汰整個(gè)鍵值中最久未使用的鍵值;
- allkeys-random:隨機(jī)淘汰任意鍵值;
- volatile-lru:淘汰所有設(shè)置了過期時(shí)間的鍵值中最久未使用的鍵值耀销;
- volatile-random:隨機(jī)淘汰設(shè)置了過期時(shí)間的任意鍵值楼眷;
- volatile-ttl:優(yōu)先淘汰更早過期的鍵值。
在 Redis 4.0 版本中又新增了 2 種淘汰策略:
- volatile-lfu:淘汰所有設(shè)置了過期時(shí)間的鍵值中熊尉,最少使用的鍵值罐柳;
- allkeys-lfu:淘汰整個(gè)鍵值中最少使用的鍵值。
其中 allkeys-xxx 表示從所有的鍵值中淘汰數(shù)據(jù)狰住,而 volatile-xxx 表示從設(shè)置了過期鍵的鍵值中淘汰數(shù)據(jù)张吉。
我們可以根據(jù)實(shí)際的業(yè)務(wù)情況進(jìn)行設(shè)置,默認(rèn)的淘汰策略不淘汰任何數(shù)據(jù)催植,在新增時(shí)會報(bào)錯(cuò)芦拿。
10.使用物理機(jī)而非虛擬機(jī)
在虛擬機(jī)中運(yùn)行 Redis 服務(wù)器,因?yàn)楹臀锢頇C(jī)共享一個(gè)物理網(wǎng)口查邢,并且一臺物理機(jī)可能有多個(gè)虛擬機(jī)在運(yùn)行蔗崎,因此在內(nèi)存占用上和網(wǎng)絡(luò)延遲方面都會有很糟糕的表現(xiàn),我們可以通過 ./redis-cli --intrinsic-latency 100
命令查看延遲時(shí)間扰藕,如果對 Redis 的性能有較高要求的話缓苛,應(yīng)盡可能在物理機(jī)上直接部署 Redis 服務(wù)器。
11.檢查數(shù)據(jù)持久化策略
Redis 的持久化策略是將內(nèi)存數(shù)據(jù)復(fù)制到硬盤上邓深,這樣才可以進(jìn)行容災(zāi)恢復(fù)或者數(shù)據(jù)遷移未桥,但維護(hù)此持久化的功能,需要很大的性能開銷芥备。
在 Redis 4.0 之后冬耿,Redis 有 3 種持久化的方式:
- RDB(Redis DataBase,快照方式)將某一個(gè)時(shí)刻的內(nèi)存數(shù)據(jù)萌壳,以二進(jìn)制的方式寫入磁盤亦镶;
- AOF(Append Only File日月,文件追加方式),記錄所有的操作命令缤骨,并以文本的形式追加到文件中爱咬;
- 混合持久化方式,Redis 4.0 之后新增的方式绊起,混合持久化是結(jié)合了 RDB 和 AOF 的優(yōu)點(diǎn)精拟,在寫入的時(shí)候,先把當(dāng)前的數(shù)據(jù)以 RDB 的形式寫入文件的開頭虱歪,再將后續(xù)的操作命令以 AOF 的格式存入文件蜂绎,這樣既能保證 Redis 重啟時(shí)的速度,又能減低數(shù)據(jù)丟失的風(fēng)險(xiǎn)笋鄙。
RDB 和 AOF 持久化各有利弊荡碾,RDB 可能會導(dǎo)致一定時(shí)間內(nèi)的數(shù)據(jù)丟失,而 AOF 由于文件較大則會影響 Redis 的啟動速度局装,為了能同時(shí)擁有 RDB 和 AOF 的優(yōu)點(diǎn)坛吁,Redis 4.0 之后新增了混合持久化的方式,因此我們在必須要進(jìn)行持久化操作時(shí)铐尚,應(yīng)該選擇混合持久化的方式拨脉。
查詢是否開啟混合持久化可以使用 config get aof-use-rdb-preamble
命令,執(zhí)行結(jié)果如下圖所示:
其中 yes 表示已經(jīng)開啟混合持久化宣增,no 表示關(guān)閉玫膀,Redis 5.0 默認(rèn)值為 yes。 如果是其他版本的 Redis 首先需要檢查一下爹脾,是否已經(jīng)開啟了混合持久化帖旨,如果關(guān)閉的情況下,可以通過以下兩種方式開啟:
- 通過命令行開啟
- 通過修改 Redis 配置文件開啟
① 通過命令行開啟
使用命令 config set aof-use-rdb-preamble yes
執(zhí)行結(jié)果如下圖所示:
命令行設(shè)置配置的缺點(diǎn)是重啟 Redis 服務(wù)之后灵妨,設(shè)置的配置就會失效解阅。
② 通過修改 Redis 配置文件開啟
在 Redis 的根路徑下找到 redis.conf 文件,把配置文件中的 aof-use-rdb-preamble no
改為 aof-use-rdb-preamble yes
如下圖所示:
配置完成之后泌霍,需要重啟 Redis 服務(wù)器货抄,配置才能生效,但修改配置文件的方式朱转,在每次重啟 Redis 服務(wù)之后蟹地,配置信息不會丟失。
需要注意的是藤为,在非必須進(jìn)行持久化的業(yè)務(wù)中怪与,可以關(guān)閉持久化,這樣可以有效的提升 Redis 的運(yùn)行速度缅疟,不會出現(xiàn)間歇性卡頓的困擾分别。
12.禁用 THP 特性
Linux kernel 在 2.6.38 內(nèi)核增加了 Transparent Huge Pages (THP) 特性 遍愿,支持大內(nèi)存頁 2MB 分配,默認(rèn)開啟茎杂。
當(dāng)開啟了 THP 時(shí)错览,fork 的速度會變慢纫雁,fork 之后每個(gè)內(nèi)存頁從原來 4KB 變?yōu)?2MB煌往,會大幅增加重寫期間父進(jìn)程內(nèi)存消耗。同時(shí)每次寫命令引起的復(fù)制內(nèi)存頁單位放大了 512 倍轧邪,會拖慢寫操作的執(zhí)行時(shí)間刽脖,導(dǎo)致大量寫操作慢查詢。例如簡單的 incr 命令也會出現(xiàn)在慢查詢中忌愚,因此 Redis 建議將此特性進(jìn)行禁用曲管,禁用方法如下:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
為了使機(jī)器重啟后 THP 配置依然生效,可以在 /etc/rc.local 中追加 echo never > /sys/kernel/mm/transparent_hugepage/enabled
硕糊。
13.使用分布式架構(gòu)來增加讀寫速度
Redis 分布式架構(gòu)有三個(gè)重要的手段:
- 主從同步
- 哨兵模式
- Redis Cluster 集群
使用主從同步功能我們可以把寫入放到主庫上執(zhí)行院水,把讀功能轉(zhuǎn)移到從服務(wù)上,因此就可以在單位時(shí)間內(nèi)處理更多的請求简十,從而提升的 Redis 整體的運(yùn)行速度檬某。
而哨兵模式是對于主從功能的升級,但當(dāng)主節(jié)點(diǎn)奔潰之后螟蝙,無需人工干預(yù)就能自動恢復(fù) Redis 的正常使用恢恼。
Redis Cluster 是 Redis 3.0 正式推出的,Redis 集群是通過將數(shù)據(jù)庫分散存儲到多個(gè)節(jié)點(diǎn)上來平衡各個(gè)節(jié)點(diǎn)的負(fù)載壓力胰默。
Redis Cluster 采用虛擬哈希槽分區(qū)场斑,所有的鍵根據(jù)哈希函數(shù)映射到 0 ~ 16383 整數(shù)槽內(nèi),計(jì)算公式:slot = CRC16(key) & 16383牵署,每一個(gè)節(jié)點(diǎn)負(fù)責(zé)維護(hù)一部分槽以及槽所映射的鍵值數(shù)據(jù)漏隐。這樣 Redis 就可以把讀寫壓力從一臺服務(wù)器,分散給多臺服務(wù)器了奴迅,因此性能會有很大的提升锁保。
在這三個(gè)功能中,我們只需要使用一個(gè)就行了半沽,毫無疑問 Redis Cluster 應(yīng)該是首選的實(shí)現(xiàn)方案爽柒,它可以把讀寫壓力自動的分擔(dān)給更多的服務(wù)器,并且擁有自動容災(zāi)的能力者填。