Redis所有的數(shù)據(jù)都存在內(nèi)存中, 當前內(nèi)存雖然越來越便宜冰肴, 但跟廉價的硬盤相比成本還是比較昂貴屈藐, 因此如何高效利用Redis內(nèi)存變得非常重要榔组。 高效利用Redis內(nèi)存首先需要理解Redis內(nèi)存消耗在哪里, 如何管理內(nèi)存联逻, 最后才能考慮如何優(yōu)化內(nèi)存搓扯。 掌握這些知識后能夠?qū)崿F(xiàn)用更少的內(nèi)存存儲更多的數(shù)據(jù), 從而降低成本包归。
本篇內(nèi)容包括
1. 內(nèi)存消耗分析
2. 管理內(nèi)存的原理與方法
3. 內(nèi)存優(yōu)化技巧
1. 內(nèi)存消耗
理解Redis內(nèi)存锨推, 首先需要掌握Redis內(nèi)存消耗在哪些方面。 有些內(nèi)存消耗是必不可少的箫踩, 而有些可以通過參數(shù)調(diào)整和合理使用來規(guī)避內(nèi)存浪費爱态。 內(nèi)存消耗可以分為進程自身消耗和子進程消耗。
內(nèi)存使用統(tǒng)計
首先需要了解Redis自身使用內(nèi)存的統(tǒng)計數(shù)據(jù)境钟, 可通過執(zhí)行info memory命令獲取內(nèi)存相關指標锦担。 讀懂每個指標有助于分析Redis內(nèi)存使用情況:
屬性名 | 屬性說明 |
---|---|
used_memory | Redis分配器分配的內(nèi)存總量,內(nèi)存存儲的所有數(shù)據(jù)內(nèi)存占用量 |
used_memory_human | 以可讀的格式返回used_memory |
used_memory_rss | 以操作系統(tǒng)的角度顯示Redis進程占用的物理內(nèi)存總量 |
used_memory_peak | 內(nèi)存使用的最大值 |
used_memory_peak_human | 以可讀的格式返回used_memory_peak |
used_memory_lua | Lua引擎消耗的內(nèi)存大小 |
mem_fragmentation_ratio | used_memory_rss/used_memory比值慨削,表示內(nèi)存碎片率 |
mem_allocator | Redis所使用的內(nèi)存分配器洞渔,默認為jemalloc |
需要重點關注的指標有: used_memory_rss和used_memory以及它們的比值mem_fragmentation_ratio。
當mem_fragmentation_ratio>1時缚态, 說明used_memory_rss - used_memory多出的部分內(nèi)存并沒有用于數(shù)據(jù)存儲磁椒, 而是被內(nèi)存碎片所消耗, 如果兩者相差很大玫芦, 說明碎片率嚴重浆熔。
當mem_fragmentation_ratio<1時, 這種情況一般出現(xiàn)在操作系統(tǒng)把Redis內(nèi)存交換(Swap)到硬盤導致桥帆, 出現(xiàn)這種情況時要格外關注医增, 由于硬盤速度遠遠慢于內(nèi)存, Redis性能會變得很差老虫, 甚至僵死叶骨。
內(nèi)存消耗劃分
Redis進程內(nèi)消耗主要包括: 自身內(nèi)存+對象內(nèi)存+緩沖內(nèi)存+內(nèi)存碎片,其中Redis空進程自身內(nèi)存消耗非常少祈匙, 通常used_memory_rss在3MB左右忽刽,used_memory在800KB左右, 一個空的Redis進程消耗內(nèi)存可以忽略不計夺欲。Redis主要內(nèi)存消耗如圖所示跪帝。
對象內(nèi)存
對象內(nèi)存是Redis內(nèi)存占用最大的一塊, 存儲著用戶所有的數(shù)據(jù)些阅。Redis所有的數(shù)據(jù)都采用key-value數(shù)據(jù)類型伞剑, 每次創(chuàng)建鍵值對時, 至少創(chuàng)建兩個類型對象: key對象和value對象扑眉。 對象內(nèi)存消耗可以簡單理解為sizeof(keys)+sizeof(values) 纸泄。 鍵對象都是字符串赖钞, 在使用Redis時很容易忽略鍵對內(nèi)存消耗的影響, 應當避免使用過長的鍵聘裁。 value對象更復雜些雪营, 主要包含5種基本數(shù)據(jù)類型: 字符串、 列表衡便、 哈希献起、 集合、 有序集合镣陕。 其他數(shù)據(jù)類型都是建立在這5種數(shù)據(jù)結構之上實現(xiàn)的谴餐, 如: Bitmaps和HyperLogLog使用字符串實現(xiàn), GEO使用有序集合實現(xiàn)等呆抑。每種value對象類型根據(jù)使用規(guī)模不同岂嗓, 占用內(nèi)存不同。 在使用時一定要合理預估并監(jiān)控value對象占用情況鹊碍, 避免內(nèi)存溢出厌殉。緩沖內(nèi)存
緩沖內(nèi)存主要包括: 客戶端緩沖、 復制積壓緩沖區(qū)侈咕、 AOF緩沖區(qū)公罕。
客戶端緩沖指的是所有接入到Redis服務器TCP連接的輸入輸出緩沖。輸入緩沖無法控制耀销, 最大空間為1G楼眷, 如果超過將斷開連接。 輸出緩沖通過參數(shù)client-output-buffer-limit控制熊尉, 如下所示:
- 普通客戶端
除了復制和訂閱的客戶端之外的所有連接罐柳, Redis的默認配置是: client-output-buffer-limit normal000, Redis并沒有對普通客戶端的輸出緩沖區(qū)做限制帽揪, 一般普通客戶端的內(nèi)存消耗可以忽略不計硝清, 但是當有大量慢連接客戶端接入時這部分內(nèi)存消耗就不能忽略了辅斟, 可以設置maxclients做限制转晰。 特別是當使用大量數(shù)據(jù)輸出的命令且數(shù)據(jù)無法及時推送給客戶端時,如monitor命令士飒, 容易造成Redis服務器內(nèi)存突然飆升查邢。 - 從客戶端
主節(jié)點會為每個從節(jié)點單獨建立一條連接用于命令復制,默認配置是: client-output-buffer-limit slave256mb64mb60酵幕。 當主從節(jié)點之間網(wǎng)絡延遲較高或主節(jié)點掛載大量從節(jié)點時這部分內(nèi)存消耗將占用很大一部分扰藕, 建議主節(jié)點掛載的從節(jié)點不要多于2個, 主從節(jié)點不要部署在較差的網(wǎng)絡環(huán)境下芳撒, 如異地跨機房環(huán)境邓深, 防止復制客戶端連接緩慢造成溢出未桥。 - 訂閱客戶端
當使用發(fā)布訂閱功能時, 連接客戶端使用單獨的輸出緩沖區(qū)芥备, 默認配置為: client-output-buffer-limit pubsub32mb8mb60冬耿, 當訂閱服務的消息生產(chǎn)快于消費速度時, 輸出緩沖區(qū)會產(chǎn)生積壓造成輸出緩沖區(qū)空間溢出萌壳。
輸入輸出緩沖區(qū)在大流量的場景中容易失控亦镶, 造成Redis內(nèi)存的不穩(wěn)定, 需要重點監(jiān)控袱瓮。
復制積壓緩沖區(qū):Redis在2.8版本之后提供了一個可重用的固定大小緩沖區(qū)用于實現(xiàn)部分復制功能缤骨, 根據(jù)repl-backlog-size參數(shù)控制, 默認1MB尺借。對于復制積壓緩沖區(qū)整個主節(jié)點只有一個绊起, 所有的從節(jié)點共享此緩沖區(qū), 因此可以設置較大的緩沖區(qū)空間燎斩, 如100MB勒庄, 這部分內(nèi)存投入是有價值的, 可以有效避免全量復制瘫里。
AOF緩沖區(qū): 這部分空間用于在Redis重寫期間保存最近的寫入命令实蔽,AOF緩沖區(qū)空間消耗用戶無法控制, 消耗的內(nèi)存取決于AOF重寫時間和寫入命令量谨读, 這部分空間占用通常很小局装。
- 內(nèi)存碎片
Redis默認的內(nèi)存分配器采用jemalloc, 可選的分配器還有: glibc劳殖、tcmalloc铐尚。 內(nèi)存分配器為了更好地管理和重復利用內(nèi)存, 分配內(nèi)存策略一般采用固定范圍的內(nèi)存塊進行分配哆姻。 例如jemalloc在64位系統(tǒng)中將內(nèi)存空間劃分為: 小宣增、 大、 巨大三個范圍矛缨。 每個范圍內(nèi)又劃分為多個小的內(nèi)存塊單位爹脾,如下所示:
- 小: [8byte]箕昭, [16byte灵妨, 32byte, 48byte落竹, ...泌霍, 128byte], [192byte述召,256byte朱转, ...蟹地, 512byte], [768byte藤为, 1024byte锈津, ..., 3840byte]
- 大: [4KB凉蜂, 8KB琼梆, 12KB, ...窿吩, 4072KB]
- 巨大: [4MB茎杂, 8MB, 12MB纫雁, ...]
比如當保存5KB對象時jemalloc可能會采用8KB的塊存儲煌往, 而剩下的3KB空間變?yōu)榱藘?nèi)存碎片不能再分配給其他對象存儲。 內(nèi)存碎片問題雖然是所有內(nèi)存服務的通病轧邪, 但是jemalloc針對碎片化問題專門做了優(yōu)化刽脖, 一般不會存在過度碎片化的問題, 正常的碎片率(mem_fragmentation_ratio) 在1.03左右忌愚。 但是當存儲的數(shù)據(jù)長短差異較大時曲管, 以下場景容易出現(xiàn)高內(nèi)存碎片問題:
- 頻繁做更新操作, 例如頻繁對已存在的鍵執(zhí)行append硕糊、 setrange等更新操作院水。
- 大量過期鍵刪除, 鍵對象過期刪除后简十, 釋放的空間無法得到充分利用檬某, 導致碎片率上升。
出現(xiàn)高內(nèi)存碎片問題時常見的解決方式如下:
- 數(shù)據(jù)對齊: 在條件允許的情況下盡量做數(shù)據(jù)對齊螟蝙, 比如數(shù)據(jù)盡量采用數(shù)字類型或者固定長度字符串等恢恼, 但是這要視具體的業(yè)務而定, 有些場景無法做到胰默。
- 安全重啟: 重啟節(jié)點可以做到內(nèi)存碎片重新整理场斑, 因此可以利用高可用架構, 如Sentinel或Cluster初坠, 將碎片率過高的主節(jié)點轉(zhuǎn)換為從節(jié)點和簸, 進行安全重啟彭雾。
子進程內(nèi)存消耗
子進程內(nèi)存消耗主要指執(zhí)行AOF/RDB重寫時Redis創(chuàng)建的子進程內(nèi)存消耗碟刺。 Redis執(zhí)行fork操作產(chǎn)生的子進程內(nèi)存占用量對外表現(xiàn)為與父進程相同,理論上需要一倍的物理內(nèi)存來完成重寫操作薯酝。 但Linux具有寫時復制技術(copy-on-write) 半沽, 父子進程會共享相同的物理內(nèi)存頁爽柒, 當父進程處理寫請求時會對需要修改的頁復制出一份副本完成寫操作, 而子進程依然讀取fork時整個父進程的內(nèi)存快照者填。
Linux Kernel在2.6.38內(nèi)核增加了Transparent Huge Pages(THP) 機制浩村, 而有些Linux發(fā)行版即使內(nèi)核達不到2.6.38也會默認加入并開啟這個功能, 如Redhat Enterprise Linux在6.0以上版本默認會引入THP占哟。 雖然開啟THP可以降低fork子進程的速度心墅, 但之后copy-on-write期間復制內(nèi)存頁的單位從4KB變?yōu)?MB, 如果父進程有大量寫命令榨乎, 會加重內(nèi)存拷貝量怎燥, 從而造成過度內(nèi)存
消耗。 例如蜜暑, 以下兩個執(zhí)行AOF重寫時的內(nèi)存消耗日志:
// 開啟THP:
C * AOF rewrite: 1039 MB of memory used by copy-on-write
// 關閉THP:
C * AOF rewrite: 9 MB of memory used by copy-on-write
這兩個日志出自同一Redis進程铐姚, used_memory總量為1.5GB, 子進程執(zhí)行期間每秒寫命令量都在200左右肛捍。 當分別開啟和關閉THP時隐绵, 子進程內(nèi)存消耗有天壤之別。 如果在高并發(fā)寫的場景下開啟THP拙毫, 子進程內(nèi)存消耗可能是父進程的數(shù)倍依许, 極易造成機器物理內(nèi)存溢出, 從而觸發(fā)SWAP或OOM killer缀蹄。
子進程內(nèi)存消耗總結如下:
- Redis產(chǎn)生的子進程并不需要消耗1倍的父進程內(nèi)存悍手, 實際消耗根據(jù)期間寫入命令量決定, 但是依然要預留出一些內(nèi)存防止溢出袍患。
- 需要設置sysctl vm.overcommit_memory=1允許內(nèi)核可以分配所有的物理內(nèi)存坦康, 防止Redis進程執(zhí)行fork時因系統(tǒng)剩余內(nèi)存不足而失敗。
- 排查當前系統(tǒng)是否支持并開啟THP诡延, 如果開啟建議關閉滞欠, 防止copy-onwrite期間內(nèi)存過度消耗。
2. 內(nèi)存管理
Redis主要通過控制內(nèi)存上限和回收策略實現(xiàn)內(nèi)存管理肆良, 本節(jié)將圍繞這兩個方面來介紹Redis如何管理內(nèi)存筛璧。
設置內(nèi)存上限
Redis使用maxmemory參數(shù)限制最大可用內(nèi)存。 限制內(nèi)存的目的主要有:
- 用于緩存場景惹恃, 當超出內(nèi)存上限maxmemory時使用LRU等刪除策略釋放空間夭谤。
- 防止所用內(nèi)存超過服務器物理內(nèi)存。
需要注意巫糙, maxmemory限制的是Redis實際使用的內(nèi)存量朗儒, 也就是used_memory統(tǒng)計項對應的內(nèi)存。 由于內(nèi)存碎片率的存在管宵, 實際消耗的內(nèi)存可能會比maxmemory設置的更大奕删, 實際使用時要小心這部分內(nèi)存溢出付枫。 通過設置內(nèi)存上限可以非常方便地實現(xiàn)一臺服務器部署多個Redis進程的內(nèi)存控制翩瓜。 比如一臺24GB內(nèi)存的服務器查牌, 為系統(tǒng)預留4GB內(nèi)存交惯, 預留4GB空閑內(nèi)存給其他進程或Redis fork進程罩阵, 留給Redis16GB內(nèi)存杜耙, 這樣可以部署4個maxmemory=4GB的Redis進程烟勋。 得益于Redis單線程架構和內(nèi)存限制機制规求, 即使沒有采用虛擬化, 不同的Redis進程之間也可以很好地實現(xiàn)CPU和內(nèi)存的隔離性卵惦。
動態(tài)調(diào)整內(nèi)存上限
Redis的內(nèi)存上限可以通過config set maxmemory進行動態(tài)修改颓哮, 即修改最大可用內(nèi)存。 例如之前的示例鸵荠, 當發(fā)現(xiàn)Redis-2沒有做好內(nèi)存預估冕茅, 實際只用了不到2GB內(nèi)存, 而Redis-1實例需要擴容到6GB內(nèi)存才夠用蛹找, 這時可以分別執(zhí)行如下命令進行調(diào)整:
Redis-1>config set maxmemory 6GB
Redis-2>config set maxmemory 2GB
如果此時Redis-3和Redis-4實例也需要分別擴容到6GB姨伤, 這時超出系統(tǒng)物理內(nèi)存限制就不能簡單的通過調(diào)整maxmemory來達到擴容的目的, 需要采用在線遷移數(shù)據(jù)或者通過復制切換服務器來達到擴容的目的庸疾。
Redis默認無限使用服務器內(nèi)存乍楚, 為防止極端情況下導致系統(tǒng)內(nèi)存耗盡, 建議所有的Redis進程都要配置maxmemory届慈。
在保證物理內(nèi)存可用的情況下徒溪, 系統(tǒng)中所有Redis實例可以調(diào)整maxmemory參數(shù)來達到自由伸縮內(nèi)存的目的。
內(nèi)存回收策略
Redis的內(nèi)存回收機制主要體現(xiàn)在以下兩個方面:
刪除到達過期時間的鍵對象金顿。
內(nèi)存使用達到maxmemory上限時觸發(fā)內(nèi)存溢出控制策略臊泌。
- 刪除過期鍵對象
Redis所有的鍵都可以設置過期屬性, 內(nèi)部保存在過期字典中揍拆。 由于進程內(nèi)保存大量的鍵渠概, 維護每個鍵精準的過期刪除機制會導致消耗大量的CPU, 對于單線程的Redis來說成本過高嫂拴, 因此Redis采用惰性刪除和定時任務刪除機制實現(xiàn)過期鍵的內(nèi)存回收播揪。
- 惰性刪除
惰性刪除用于當客戶端讀取帶有超時屬性的鍵時, 如果已經(jīng)超過鍵設置的過期時間筒狠, 會執(zhí)行刪除操作并返回空猪狈, 這種策略是出于節(jié)省CPU成本考慮, 不需要單獨維護TTL鏈表來處理過期鍵的刪除辩恼。 但是單獨用這種方式存在內(nèi)存泄露的問題雇庙, 當過期鍵一直沒有訪問將無法得到及時刪除谓形, 從而導致內(nèi)存不能及時釋放。 正因為如此状共, Redis還提供另一種定時任務刪除機制作為惰性刪除的補充套耕。 - 定時任務刪除
Redis內(nèi)部維護一個定時任務谁帕, 默認每秒運行10次(通過配置hz控制) 峡继。 定時任務中刪除過期鍵邏輯采用了自適應算法, 根據(jù)鍵的過期比例匈挖、 使用快慢兩種速率模式回收鍵碾牌。
流程說明:
1)定時任務在每個數(shù)據(jù)庫空間隨機檢查20個鍵, 當發(fā)現(xiàn)過期時刪除對應的鍵儡循。
2)如果超過檢查數(shù)25%的鍵過期舶吗, 循環(huán)執(zhí)行回收邏輯直到不足25%或運行超時為止, 慢模式下超時時間為25毫秒择膝。
3)如果之前回收鍵邏輯超時誓琼, 則在Redis觸發(fā)內(nèi)部事件之前再次以快模式運行回收過期鍵任務, 快模式下超時時間為1毫秒且2秒內(nèi)只能運行1次肴捉。
4)快慢兩種模式內(nèi)部刪除邏輯相同腹侣, 只是執(zhí)行的超時時間不同
- 內(nèi)存溢出控制策略
當Redis所用內(nèi)存達到maxmemory上限時會觸發(fā)相應的溢出控制策略。具體策略受maxmemory-policy參數(shù)控制齿穗, Redis支持6種策略傲隶。
1) noeviction: 默認策略, 不會刪除任何數(shù)據(jù)窃页, 拒絕所有寫入操作并返回客戶端錯誤信息( error) OOM command not allowed when used memory跺株, 此時Redis只響應讀操作。
2) volatile-lru: 根據(jù)LRU算法刪除設置了超時屬性( expire) 的鍵脖卖, 直到騰出足夠空間為止乒省。 如果沒有可刪除的鍵對象, 回退到noeviction策略畦木。
3) allkeys-lru: 根據(jù)LRU算法刪除鍵作儿, 不管數(shù)據(jù)有沒有設置超時屬性,直到騰出足夠空間為止馋劈。
4) allkeys-random: 隨機刪除所有鍵攻锰, 直到騰出足夠空間為止。
5) volatile-random: 隨機刪除過期鍵妓雾, 直到騰出足夠空間為止娶吞。
6) volatile-ttl: 根據(jù)鍵值對象的ttl屬性, 刪除最近將要過期數(shù)據(jù)械姻。 如果沒有妒蛇, 回退到noeviction策略机断。
內(nèi)存溢出控制策略可以采用config set maxmemory-policy{policy}動態(tài)配置。 Redis支持豐富的內(nèi)存溢出應對策略绣夺, 可以根據(jù)實際需求靈活定制吏奸, 比如當設置volatile-lru策略時, 保證具有過期屬性的鍵可以根據(jù)LRU剔除陶耍, 而未設置超時的鍵可以永久保留奋蔚。 還可以采用allkeys-lru策略把Redis變?yōu)榧兙彺娣掌魇褂谩?當Redis因為內(nèi)存溢出刪除鍵時, 可以通過執(zhí)行info stats命令查看evicted_keys指標找出當前Redis服務器已剔除的鍵數(shù)量烈钞。
每次Redis執(zhí)行命令時如果設置了maxmemory參數(shù)泊碑, 都會嘗試執(zhí)行回收內(nèi)存操作。 當Redis一直工作在內(nèi)存溢出(used_memory>maxmemory) 的狀態(tài)下且設置非noeviction策略時毯欣, 會頻繁地觸發(fā)回收內(nèi)存的操作馒过, 影響Redis服務器的性能。 回收內(nèi)存邏輯偽代碼如下:
頻繁執(zhí)行回收內(nèi)存成本很高酗钞, 主要包括查找可回收鍵和刪除鍵的開銷腹忽, 如果當前Redis有從節(jié)點, 回收內(nèi)存操作對應的刪除命令會同步到從節(jié)點砚作, 導致寫放大的問題窘奏。
建議線上Redis內(nèi)存工作在maxmemory>used_memory狀態(tài)下, 避免頻繁內(nèi)存回收開銷偎巢。
對于需要收縮Redis內(nèi)存的場景蔼夜, 可以通過調(diào)小maxmemory來實現(xiàn)快速回收。 比如對一個實際占用6GB內(nèi)存的進程設置maxmemory=4GB压昼, 之后第一次執(zhí)行命令時求冷, 如果使用非noeviction策略, 它會一次性回收到maxmemory指定的內(nèi)存量窍霞, 從而達到快速回收內(nèi)存的目的匠题。 注意, 此操作會導致數(shù)據(jù)丟失和短暫的阻塞問題但金, 一般在緩存場景下使用韭山。
內(nèi)存優(yōu)化
Redis所有的數(shù)據(jù)都在內(nèi)存中, 而內(nèi)存又是非常寶貴的資源冷溃。 如何優(yōu)化內(nèi)存的使用一直是Redis用戶非常關注的問題钱磅。 本節(jié)深入到Redis細節(jié)中, 探索內(nèi)存優(yōu)化的技巧似枕。
-
redisObject對象
Redis存儲的所有值對象在內(nèi)部定義為redisObject結構體盖淡, 內(nèi)部結構如圖
Redis存儲的數(shù)據(jù)都使用redisObject來封裝, 包括string凿歼、 hash褪迟、 list冗恨、set、 zset在內(nèi)的所有數(shù)據(jù)類型味赃。 理解redisObject對內(nèi)存優(yōu)化非常有幫助掀抹, 下面針對每個字段做詳細說明:
type字段: 表示當前對象使用的數(shù)據(jù)類型, Redis主要支持5種數(shù)據(jù)類型: string心俗、 hash傲武、 list、 set另凌、 zset谱轨。 可以使用type{key}命令查看對象所屬類型戒幔, type命令返回的是值對象類型吠谢, 鍵都是string類型。
encoding字段: 表示Redis內(nèi)部編碼類型诗茎, encoding在Redis內(nèi)部使用工坊,代表當前對象內(nèi)部采用哪種數(shù)據(jù)結構實現(xiàn)。 理解Redis內(nèi)部編碼方式對于優(yōu)化內(nèi)存非常重要敢订, 同一個對象采用不同的編碼實現(xiàn)內(nèi)存占用存在明顯差異王污。
lru字段: 記錄對象最后一次被訪問的時間, 當配置了maxmemory和maxmemory-policy=volatile-lru或者allkeys-lru時楚午, 用于輔助LRU算法刪除鍵數(shù)據(jù)昭齐。 可以使用object idletime{key}命令在不更新lru字段情況下查看當前鍵的空閑時間》瘢可以使用scan+object idletime命令批量查詢哪些鍵長時間未被訪問阱驾, 找出長時間不訪問的鍵進行清理, 可降低內(nèi)存占用怪蔑。
refcount字段: 記錄當前對象被引用的次數(shù)里覆, 用于通過引用次數(shù)回收內(nèi)存, 當refcount=0時缆瓣, 可以安全回收當前對象空間喧枷。 使用object refcount{key}獲取當前對象引用。 當對象為整數(shù)且范圍在[0-9999]時弓坞, Redis可以使用共享對象的方式來節(jié)省內(nèi)存隧甚。
*ptr字段: 與對象的數(shù)據(jù)內(nèi)容相關, 如果是整數(shù)渡冻, 直接存儲數(shù)據(jù)戚扳; 否則表示指向數(shù)據(jù)的指針。 Redis在3.0之后對值對象是字符串且長度<=39字節(jié)的數(shù)據(jù)菩帝, 內(nèi)部編碼為embstr類型咖城, 字符串sds和redisObject一起分配茬腿, 從而只要一次內(nèi)存操作即可。高并發(fā)寫入場景中宜雀, 在條件允許的情況下切平, 建議字符串長度控制在39字節(jié)以內(nèi), 減少創(chuàng)建redisObject內(nèi)存分配次數(shù)辐董, 從而提高性能悴品。
- 縮減鍵值對象
降低Redis內(nèi)存使用最直接的方式就是縮減鍵(key) 和值(value) 的長度。
- key長度: 如在設計鍵時简烘, 在完整描述業(yè)務情況下苔严, 鍵值越短越好。 如user: {uid}: friends: notify: {fid}可以簡化為u: {uid}: fs: nt: {fid}孤澎。
- value長度: 值對象縮減比較復雜届氢, 常見需求是把業(yè)務對象序列化成二進制數(shù)組放入Redis。 首先應該在業(yè)務上精簡業(yè)務對象覆旭, 去掉不必要的屬性避免存儲無效數(shù)據(jù)退子。 其次在序列化工具選擇上, 應該選擇更高效的序列化工具來降低字節(jié)數(shù)組大小型将。 以Java為例寂祥, 內(nèi)置的序列化方式無論從速度還是壓縮比都不盡如人意, 這時可以選擇更高效的序列化工具七兜, 如: protostuff丸凭、
kryo等,以下是Java常見序列化工具空間壓縮對比腕铸。
其中java-built-in-serializer表示Java內(nèi)置序列化方式惜犀, 更多數(shù)據(jù)見jvm-serializers項目: https://github.com/eishay/jvm-serializers/wiki, 其他語言也有各自對應的高效序列化工具恬惯。值對象除了存儲二進制數(shù)據(jù)之外向拆, 通常還會使用通用格式存儲數(shù)據(jù)比如: json、 xml等作為字符串存儲在Redis中酪耳。 這種方式優(yōu)點是方便調(diào)試和跨語言浓恳, 但是同樣的數(shù)據(jù)相比字節(jié)數(shù)組所需的空間更大, 在內(nèi)存緊張的情況下碗暗, 可以使用通用壓縮算法壓縮json颈将、 xml后再存入Redis, 從而降低內(nèi)存占用言疗, 例如使用GZIP壓縮后的json可降低約60%的空間晴圾。
當頻繁壓縮解壓json等文本數(shù)據(jù)時, 開發(fā)人員需要考慮壓縮速度和計算開銷成本噪奄, 這里推薦使用Google的Snappy壓縮工具死姚, 在特定的壓縮率情況下效率遠遠高于GZIP等傳統(tǒng)壓縮工具人乓, 且支持所有主流語言環(huán)境。
- 共享對象池
共享對象池是指Redis內(nèi)部維護[0-9999]的整數(shù)對象池都毒。 創(chuàng)建大量的整數(shù)類型redisObject存在內(nèi)存開銷色罚, 每個redisObject內(nèi)部結構至少占16字節(jié), 甚至超過了整數(shù)自身空間消耗账劲。 所以Redis內(nèi)存維護一個[0-9999]的整數(shù)對象池戳护, 用于節(jié)約內(nèi)存。 除了整數(shù)值對象瀑焦, 其他類型如list腌且、 hash、 set榛瓮、 zset內(nèi)部元素也可以使用整數(shù)對象池铺董。 因此開發(fā)中在滿足需求的前提下, 盡量使用整數(shù)對象以節(jié)省內(nèi)存榆芦。
整數(shù)對象池在Redis中通過變量REDIS_SHARED_INTEGERS定義柄粹, 不能通過配置修改喘鸟。 可以通過object refcount命令查看對象引用數(shù)驗證是否啟用整數(shù)對象池技術匆绣, 如下:
redis> set foo 100
OK
redis> object refcount foo
(integer) 2
redis> set bar 100
OK
redis> object refcount bar
(integer) 3
設置鍵foo等于100時, 直接使用共享池內(nèi)整數(shù)對象什黑, 因此引用數(shù)是2崎淳,再設置鍵bar等于100時, 引用數(shù)又變?yōu)?愕把。
使用整數(shù)對象池究竟能降低多少內(nèi)存拣凹? 讓我們通過測試來對比對象池的內(nèi)存優(yōu)化效果
使用共享對象池后, 相同的數(shù)據(jù)內(nèi)存使用降低30%以上恨豁。 可見當數(shù)據(jù)大量使用[0-9999]的整數(shù)時嚣镜, 共享對象池可以節(jié)約大量內(nèi)存。 需要注意的是對象池并不是只要存儲[0-9999]的整數(shù)就可以工作橘蜜。 當設置maxmemory并啟用LRU相關淘汰策略如: volatile-lru菊匿, allkeys-lru時, Redis禁止使用共享對象池计福, 測試命令如下:
redis> set key:1 99
OK // 設置key:1=99
redis> object refcount key:1
(integer) 2 // 使用了對象共享,引用數(shù)為2
redis> config set maxmemory-policy volatile-lru
OK // 開啟LRU淘汰策略
redis> set key:2 99
OK // 設置key:2=99
redis> object refcount key:2
(integer) 3 // 使用了對象共享,引用數(shù)變?yōu)?
redis> config set maxmemory 1GB
OK // 設置最大可用內(nèi)存
redis> set key:3 99
OK // 設置key:3=99
redis> object refcount key:3
(integer) 1 // 未使用對象共享,引用數(shù)為1
redis> config set maxmemory-policy volatile-ttl
OK // 設置非LRU淘汰策略
redis> set key:4 99
OK // 設置key:4=99
redis> object refcount key:4
(integer) 4 // 又可以使用對象共享,引用數(shù)變?yōu)?
為什么開啟maxmemory和LRU淘汰策略后對象池無效跌捆?
LRU算法需要獲取對象最后被訪問時間, 以便淘汰最長未訪問數(shù)據(jù)象颖, 每個對象最后訪問時間存儲在redisObject對象的lru字段佩厚。 對象共享意味著多個引用共享同一個redisObject, 這時lru字段也會被共享说订, 導致無法獲取每個對象的最后訪問時間抄瓦。 如果沒有設置maxmemory潮瓶, 直到內(nèi)存被用盡Redis也不會觸發(fā)內(nèi)存回收, 所以共享對象池可以正常工作钙姊。
綜上所述筋讨, 共享對象池與maxmemory+LRU策略沖突, 使用時需要注意摸恍。 對于ziplist編碼的值對象悉罕, 即使內(nèi)部數(shù)據(jù)為整數(shù)也無法使用共享對象池, 因為ziplist使用壓縮且內(nèi)存連續(xù)的結構立镶, 對象共享判斷成本過高壁袄, ziplist編碼細節(jié)后面內(nèi)容詳細說明。
為什么只有整數(shù)對象池媚媒?
首先整數(shù)對象池復用的幾率最大嗜逻, 其次對象共享的一個關鍵操作就是判斷相等性, Redis之所以只有整數(shù)對象池缭召, 是因為整數(shù)比較算法時間復雜度為O(1) , 只保留一萬個整數(shù)為了防止對象池浪費萄凤。 如果是字符串判斷相等性搪哪, 時間復雜度變?yōu)镺(n) 靡努, 特別是長字符串更消耗性能(浮點數(shù)在Redis內(nèi)部使用字符串存儲) 晓折。 對于更復雜的數(shù)據(jù)結構如hash漾月、 list等, 相等性判斷需要O(n2) 堂鲜。 對于單線程的Redis來說, 這樣的開銷顯然不合理厌秒, 因此Redis只保留整數(shù)共享對象池蚌讼。
- 字符串優(yōu)化
字符串對象是Redis內(nèi)部最常用的數(shù)據(jù)類型西采。 所有的鍵都是字符串類型胖眷, 值對象數(shù)據(jù)除了整數(shù)之外都使用字符串存儲食棕。 比如執(zhí)行命令: lpush cache: type "redis" "memcache" "tair" "levelDB"千埃, Redis首先創(chuàng)建"cache: type"鍵字符串, 然后創(chuàng)建鏈表對象拾氓, 鏈表對象內(nèi)再包含四個字符串對象趾徽, 排除Redis內(nèi)部用到的字符串對象之外至少創(chuàng)建5個字符串對象蜡峰。 可見字符串對象在Redis內(nèi)部使用非常廣泛肖爵, 因此深刻理解Redis字符串對于內(nèi)存優(yōu)化非常有幫助秒啦。
1)字符串結構
Redis沒有采用原生C語言的字符串類型而是自己實現(xiàn)了字符串結構驻呐, 內(nèi)部簡單動態(tài)字符串(simple dynamic string即舌, SDS)
Redis自身實現(xiàn)的字符串結構有如下特點:
- O(1) 時間復雜度獲取: 字符串長度蜜葱、 已用長度、 未用長度。
- 可用于保存字節(jié)數(shù)組, 支持安全的二進制數(shù)據(jù)存儲。
- 內(nèi)部實現(xiàn)空間預分配機制脆霎, 降低內(nèi)存再分配次數(shù)。
- 惰性刪除機制, 字符串縮減后的空間不釋放, 作為預分配空間保留。
2)預分配機制
因為字符串(SDS) 存在預分配機制, 日常開發(fā)中要小心預分配帶來的內(nèi)存浪費
從測試數(shù)據(jù)可以看出俊抵, 同樣的數(shù)據(jù)追加后內(nèi)存消耗非常嚴重谎替, 下面我們結合圖來分析這一現(xiàn)象秩命。 階段1每個字符串對象空間占用如圖
階段1插入新的字符串后霹菊, free字段保留空間為0鸠按, 總占用空間=實際占用空間+1字節(jié)卑雁, 最后1字節(jié)保存‘\0’標示結尾扣甲, 這里忽略int類型len和free字段消耗的8字節(jié)示辈。 在階段1原有字符串上追加60字節(jié)數(shù)據(jù)空間占用如圖
追加操作后字符串對象預分配了一倍容量作為預留空間险耀, 而且大量追加操作需要內(nèi)存重新分配, 造成內(nèi)存碎片率(mem_fragmentation_ratio) 上升赠群。
直接插入與階段2相同數(shù)據(jù)的空間占用匀油, 如圖
階段3直接插入同等數(shù)據(jù)后弛车, 相比階段2節(jié)省了每個字符串對象預分配的空間邀杏, 同時降低了碎片率脖律。
字符串之所以采用預分配的方式是防止修改操作需要不斷重分配內(nèi)存和字節(jié)數(shù)據(jù)拷貝勒叠。 但同樣也會造成內(nèi)存的浪費膏孟。 字符串預分配每次并不都是翻倍擴容柒桑, 空間預分配規(guī)則如下:
1) 第一次創(chuàng)建len屬性等于數(shù)據(jù)實際大小魁淳, free等于0界逛, 不做預分配。
2) 修改后如果已有free空間不夠且數(shù)據(jù)小于1M溉潭, 每次預分配一倍容量喳瓣。 如原有l(wèi)en=60byte仁讨, free=0曙咽, 再追加60byte羔挡, 預分配120byte商佛, 總占用空間: 60byte+60byte+120byte+1byte。
3) 修改后如果已有free空間不夠且數(shù)據(jù)大于1MB叮贩, 每次預分配1MB數(shù)據(jù)酷誓。 如原有l(wèi)en=30MB, free=0掰邢, 當再追加100byte, 預分配1MB, 總占用空間: 1MB+100byte+1MB+1byte。
盡量減少字符串頻繁修改操作如append黄鳍、 setrange输玷, 改為直接使用set修改字符串袄膏, 降低預分配帶來的內(nèi)存浪費和內(nèi)存碎片化。
- 字符串重構
字符串重構: 指不一定把每份數(shù)據(jù)作為字符串整體存儲, 像json這樣的數(shù)據(jù)可以使用hash結構斩熊, 使用二級結構存儲也能幫我們節(jié)省內(nèi)存似忧。 同時可以使用hmget、 hmset命令支持字段的部分讀取修改, 而不用每次整體存取。 例如下面的json數(shù)據(jù):
{
"vid": "413368768",
"title": "搜狐屌絲男士",
"videoAlbumPic":"http://photocdn.sohu.com/60160518/vrsa_ver8400079_ae433_pic26.jpg",
"pid": "6494271",
"type": "1024",
"playlist": "6494271",
"playTime": "468"
}
分別使用字符串和hash結構測試內(nèi)存表現(xiàn)辜羊,
根據(jù)測試結構八秃, 第一次默認配置下使用hash類型昔驱, 內(nèi)存消耗不但沒有降低反而比字符串存儲多出2倍骤肛, 而調(diào)整hash-max-ziplist-value=66之后內(nèi)存降低為535.60M腋颠。 因為json的videoAlbumPic屬性長度是65淑玫, 而hash-max-ziplistvalue默認值是64絮蒿, Redis采用hashtable編碼方式, 反而消耗了大量內(nèi)存毁嗦。 調(diào)整配置后hash類型內(nèi)部編碼方式變?yōu)閦iplist狗准, 相比字符串更省內(nèi)存且支持屬性的部分操作腔长。 下一節(jié)將具體介紹ziplist編碼優(yōu)化細節(jié)捞附。
編碼優(yōu)化
- 了解編碼
Redis對外提供了string鸟召、 list晴氨、 hash跟继、 set舔糖、 zet等類型, 但是Redis內(nèi)部針對不同類型存在編碼的概念十兢, 所謂編碼就是具體使用哪種底層數(shù)據(jù)結構來實現(xiàn)纪挎。 編碼不同將直接影響數(shù)據(jù)的內(nèi)存占用和讀寫效率。 使用object encoding{key}命令獲取編碼類型玛臂。 如下所示:
redis> set str:1 hello
OK
redis> object encoding str:1
"embstr" // embstr編碼字符串
redis> lpush list:1 1 2 3
(integer) 3
redis> object encoding list:1
"ziplist" // ziplist編碼列表
Redis針對每種數(shù)據(jù)類型(type) 可以采用至少兩種編碼方式來實現(xiàn)迹冤,如下所示:
了解編碼和類型對應關系之后, 我們不禁疑惑Redis為什么對一種數(shù)據(jù)結構實現(xiàn)多種編碼方式膜蠢?
主要原因是Redis作者想通過不同編碼實現(xiàn)效率和空間的平衡挑围。 比如當我們的存儲只有10個元素的列表杉辙, 當使用雙向鏈表數(shù)據(jù)結構時, 必然需要維護大量的內(nèi)部字段如每個元素需要: 前置指針综看, 后置指針红碑, 數(shù)據(jù)指針等句喷, 造成空間浪費唾琼, 如果采用連續(xù)內(nèi)存結構的壓縮列表(ziplist) 锡溯, 將會節(jié)省大量內(nèi)存祭饭, 而由于數(shù)據(jù)長度較小倡蝙, 存取操作時間復雜度即使為O(n2) 性能也可滿足需求寺鸥。
- 控制編碼類型
編碼類型轉(zhuǎn)換在Redis寫入數(shù)據(jù)時自動完成胆建, 這個轉(zhuǎn)換過程是不可逆的笆载, 轉(zhuǎn)換規(guī)則只能從小內(nèi)存編碼向大內(nèi)存編碼轉(zhuǎn)換凉驻。 例如:
redis> lpush list:1 a b c d
(integer) 4 // 存儲4個元素
redis> object encoding list:1
"ziplist" // 采用ziplist壓縮列表編碼
redis> config set list-max-ziplist-entries 4
OK // 設置列表類型ziplist編碼最大允許4個元素
redis> lpush list:1 e
(integer) 5 // 寫入第5個元素e
redis> object encoding list:1
"linkedlist" // 編碼類型轉(zhuǎn)換為鏈表
redis> rpop list:1
"a" // 彈出元素a
redis> llen list:1
(integer) 4 // 列表此時有4個元素
redis> object encoding list:1
"linkedlist" // 編碼類型依然為鏈表沿侈, 未做編碼回退
以上命令體現(xiàn)了list類型編碼的轉(zhuǎn)換過程缀拭, 其中Redis之所以不支持編碼回退蛛淋, 主要是數(shù)據(jù)增刪頻繁時褐荷, 數(shù)據(jù)向壓縮編碼轉(zhuǎn)換非常消耗CPU叛甫, 得不償失其监。 以上示例用到了list-max-ziplist-entries參數(shù)抖苦, 這個參數(shù)用來決定列表長度在多少范圍內(nèi)使用ziplist編碼锌历。 當然還有其他參數(shù)控制各種數(shù)據(jù)類型的編碼:
續(xù)
掌握編碼轉(zhuǎn)換機制窗慎, 對我們通過編碼來優(yōu)化內(nèi)存使用非常有幫助捉邢。 下面以hash類型為例, 介紹編碼轉(zhuǎn)換的運行流程
理解編碼轉(zhuǎn)換流程和相關配置之后晕拆, 可以使用config set命令設置編碼相關參數(shù)來滿足使用壓縮編碼的條件实幕。 對于已經(jīng)采用非壓縮編碼類型的數(shù)據(jù)如hashtable昆庇、 linkedlist等整吆, 設置參數(shù)后即使數(shù)據(jù)滿足壓縮編碼條件表蝙, Redis也不會做轉(zhuǎn)換府蛇, 需要重啟Redis重新加載數(shù)據(jù)才能完成轉(zhuǎn)換汇跨。
-
ziplist編碼
ziplist編碼主要目的是為了節(jié)約內(nèi)存穷遂, 因此所有數(shù)據(jù)都是采用線性連續(xù)的內(nèi)存結構塞颁。 ziplist編碼是應用范圍最廣的一種酷窥, 可以分別作為hash蓬推、 list沸伏、zset類型的底層數(shù)據(jù)結構實現(xiàn)毅糟。 首先從ziplist編碼結構開始分析姆另, 它的內(nèi)部結構類似這樣: <zlbytes><zltail><zllen><entry-1><entry-2><....><entry-n><zlend>迹辐。 一個ziplist可以包含多個entry(元素) 明吩, 每個entry保存具體的數(shù)據(jù)(整數(shù)或者字節(jié)數(shù)組) 印荔, 內(nèi)部結構如圖
ziplist結構字段含義:
1) zlbytes: 記錄整個壓縮列表所占字節(jié)長度氮采, 方便重新調(diào)整ziplist空間鹊漠。 類型是int-32躯概, 長度為4字節(jié)娶靡。
2) zltail: 記錄距離尾節(jié)點的偏移量姿锭, 方便尾節(jié)點彈出操作呻此。 類型是int-32焚鲜, 長度為4字節(jié)放前。
3) zllen: 記錄壓縮鏈表節(jié)點數(shù)量忿磅, 類型是int-16, 長度為2字節(jié)凭语。
4) entry: 記錄具體的節(jié)點葱她, 長度根據(jù)實際存儲的數(shù)據(jù)而定。
? ? ? ?a) prev_entry_bytes_length: 記錄前一個節(jié)點所占空間似扔, 用于快速定位上一個節(jié)點览效, 可實現(xiàn)列表反向迭代虫几。
? ? ? ?b) encoding: 標示當前節(jié)點編碼和長度, 前兩位表示編碼類型: 字符串/整數(shù)挽拔, 其余位表示數(shù)據(jù)長度辆脸。
? ? ? ?c) contents: 保存節(jié)點的值, 針對實際數(shù)據(jù)長度做內(nèi)存占用優(yōu)化螃诅。
5) zlend: 記錄列表結尾啡氢, 占用一個字節(jié)状囱。
根據(jù)以上對ziplist字段說明, 可以分析出該數(shù)據(jù)結構特點如下:
- 內(nèi)部表現(xiàn)為數(shù)據(jù)緊湊排列的一塊連續(xù)內(nèi)存數(shù)組倘是。
- 可以模擬雙向鏈表結構亭枷, 以O( 1) 時間復雜度入隊和出隊。
- 新增刪除操作涉及內(nèi)存重新分配或釋放搀崭, 加大了操作的復雜性叨粘。
- 讀寫操作涉及復雜的指針移動, 最壞時間復雜度為O( n2) 瘤睹。
- 適合存儲小對象和長度有限的數(shù)據(jù)升敲。
下面通過測試展示ziplist編碼在不同類型中內(nèi)存和速度的表現(xiàn)
測試數(shù)據(jù)采用100W個36字節(jié)數(shù)據(jù), 劃分為1000個鍵轰传, 每個類型長度統(tǒng)一為1000驴党。 從測試結果可以看出:
1) 使用ziplist可以分別作為hash、 list获茬、 zset數(shù)據(jù)類型實現(xiàn)港庄。
2) 使用ziplist編碼類型可以大幅降低內(nèi)存占用。
3) ziplist實現(xiàn)的數(shù)據(jù)類型相比原生結構恕曲, 命令操作更加耗時鹏氧, 不同類型耗時排序: list<hash<zset。
ziplist壓縮編碼的性能表現(xiàn)跟值長度和元素個數(shù)密切相關码俩, 正因為如此Redis提供了{type}-max-ziplist-value和{type}-max-ziplist-entries相關參數(shù)來做控制ziplist編碼轉(zhuǎn)換度帮。 最后再次強調(diào)使用ziplist壓縮編碼的原則: 追求空間和時間的平衡。
針對性能要求較高的場景使用ziplist稿存, 建議長度不要超過1000笨篷, 每個元素大小控制在512字節(jié)以內(nèi)。
命令平均耗時使用info Commandstats命令獲取瓣履, 包含每個命令調(diào)用次數(shù)率翅、 總耗時、 平均耗時袖迎, 單位為微秒
- intset編碼
intset編碼是集合(set) 類型編碼的一種冕臭, 內(nèi)部表現(xiàn)為存儲有序、 不重復的整數(shù)集燕锥。 當集合只包含整數(shù)且長度不超過set-max-intset-entries配置時被啟用辜贵。 執(zhí)行以下命令查看intset表現(xiàn):
redis> sadd set:test 3 4 2 6 8 9 2
(integer) 6 // 亂序?qū)懭?個整數(shù)
Redis> object encoding set:test
"intset" // 使用intset編碼
Redis> smembers set:test
"2" "3" "4" "6" "8" "9" // 排序輸出整數(shù)結合
redis> config set set-max-intset-entries 6
OK // 設置intset最大允許整數(shù)長度
redis> sadd set:test 5
(integer) 1 // 寫入第7個整數(shù) 5
redis> object encoding set:test
"hashtable" // 編碼變?yōu)閔ashtable
redis> smembers set:test
"8" "3" "5" "9" "4" "2" "6" // 亂序輸出
以上命令可以看出intset對寫入整數(shù)進行排序,通過O(log(n))時間復雜度實現(xiàn)查找和去重操作归形, intset編碼結構如圖
intset的字段結構含義:
1) encoding: 整數(shù)表示類型托慨, 根據(jù)集合內(nèi)最長整數(shù)值確定類型, 整數(shù)類型劃分為三種: int-16暇榴、 int-32厚棵、 int-64蕉世。
2) length: 表示集合元素個數(shù)。
3) contents: 整數(shù)數(shù)組婆硬, 按從小到大順序保存狠轻。
intset保存的整數(shù)類型根據(jù)長度劃分, 當保存的整數(shù)超出當前類型時彬犯,將會觸發(fā)自動升級操作且升級后不再做回退向楼。 升級操作將會導致重新申請內(nèi)存空間, 把原有數(shù)據(jù)按轉(zhuǎn)換類型后拷貝到新數(shù)組躏嚎。
使用intset編碼的集合時蜜自, 盡量保持整數(shù)范圍一致, 如都在int-16范圍內(nèi)卢佣。 防止個別大整數(shù)觸發(fā)集合升級操作重荠, 產(chǎn)生內(nèi)存浪費。
下面通過測試查看ziplist編碼的集合內(nèi)存和速度表現(xiàn)
根據(jù)以上測試結果發(fā)現(xiàn)intset表現(xiàn)非常好虚茶, 同樣的數(shù)據(jù)內(nèi)存占用只有不到hashtable編碼的十分之一戈鲁。 intset數(shù)據(jù)結構插入命令復雜度為O(n) , 查詢命令為O(log(n) ) 嘹叫, 由于整數(shù)占用空間非常小婆殿, 所以在集合長度可控的基礎上, 寫入命令執(zhí)行速度也會非痴稚龋快婆芦, 因此當使用整數(shù)集合時盡量使用intset編碼。 測試第三行把ziplist-hash類型也放入其中喂饥, 主要因為intset編碼必須存儲整數(shù)消约, 當集合內(nèi)保存非整數(shù)數(shù)據(jù)時, 無法使用intset實現(xiàn)內(nèi)存優(yōu)化员帮。 這時可以使用ziplist-hash類型對象模擬集合類型或粮, hash的field當作集合中的元素, value設置為1字節(jié)占位符即可捞高。 使用ziplist編碼的hash類型依然比使用hashtable編碼的集合節(jié)省大量內(nèi)存氯材。
- 控制鍵的數(shù)量
當使用Redis存儲大量數(shù)據(jù)時, 通常會存在大量鍵硝岗, 過多的鍵同樣會消耗大量內(nèi)存氢哮。 Redis本質(zhì)是一個數(shù)據(jù)結構服務器, 它為我們提供多種數(shù)據(jù)結構型檀, 如hash冗尤、 list、 set、 zset等生闲。 使用Redis時不要進入一個誤區(qū), 大量使用get/set這樣的API月幌, 把Redis當成Memcached使用碍讯。 對于存儲相同的數(shù)據(jù)內(nèi)容利用Redis的數(shù)據(jù)結構降低外層鍵的數(shù)量, 也可以節(jié)省大量內(nèi)存扯躺。 如圖所示捉兴, 通過在客戶端預估鍵規(guī)模, 把大量鍵分組映射到多個hash結構中降低鍵的數(shù)量录语。
hash結構降低鍵數(shù)量分析:
- 根據(jù)鍵規(guī)模在客戶端通過分組映射到一組hash對象中倍啥, 如存在100萬個鍵, 可以映射到1000個hash中澎埠, 每個hash保存1000個元素虽缕。
- hash的field可用于記錄原始key字符串, 方便哈希查找蒲稳。
- hash的value保存原始值對象氮趋, 確保不要超過hash-max-ziplist-value限制。
通過這個測試數(shù)據(jù)江耀, 可以說明:
- 同樣的數(shù)據(jù)使用ziplist編碼的hash類型存儲比string類型節(jié)約內(nèi)存剩胁。
- 節(jié)省內(nèi)存量隨著value空間的減少越來越明顯。
- hash-ziplist類型比string類型寫入耗時祥国, 但隨著value空間的減少昵观, 耗時逐漸降低。