Redis 實(shí)戰(zhàn) —— 12. 降低內(nèi)存占用

簡介

降低 Redis 的內(nèi)存占用有助于減少創(chuàng)建快照和加載快照所需的時(shí)間、提升載入 AOF 文件和重寫 AOF 文件時(shí)的效率、縮短從服務(wù)器進(jìn)行同步所需的時(shí)間(快照、 AOF 文件重寫在 持久化選項(xiàng) 中進(jìn)行了介紹,從服務(wù)器同步在 復(fù)制、處理故障、事務(wù)及性能優(yōu)化 中進(jìn)行了介紹),并且能讓 Redis 存儲更多的數(shù)據(jù)而無需添加額外的硬件。 P208

短結(jié)構(gòu) (short structure) P208

Redis 為列表、集合、散列和有序集合提供了一組配置選項(xiàng),這些選項(xiàng)可以讓 Redis 以更節(jié)約空間的方式存儲長度較短的結(jié)構(gòu)(后面簡稱“短結(jié)構(gòu)”)。 P208

在列表、散列和有序集合的長度較短或者體積較小的時(shí)候, Redis 可以選擇使用一種名為壓縮列表 (ziplist) 的緊湊存儲方式來存儲這些機(jī)構(gòu)。壓縮列表是列表、散列和有序集合這 3 種不同類型的對象的一種非結(jié)構(gòu)化 (unstructured) 表示:與 Redis 在通常情況下使用雙向鏈表表示列表、使用散列表表示散列、使用散列表加上跳表 (skiplist) 表示有序集合的做法不同,壓縮列表會以序列化的方式存儲數(shù)據(jù),這些序列化數(shù)據(jù)每次被讀取的時(shí)候都要進(jìn)行解碼,每次被寫入的時(shí)候也要進(jìn)行局部的重新編碼,并且可能需要對內(nèi)存里面的數(shù)據(jù)進(jìn)行移動。 P209

壓縮列表表示 P209

本節(jié)以最簡單的列表進(jìn)行觀察對比。

雙向鏈表 P209

列表不進(jìn)行壓縮時(shí)使用雙向鏈表 (doubly linked list) 進(jìn)行存儲,鏈表的每個(gè)結(jié)點(diǎn)都有三個(gè)指針: P209

  • 指向前一個(gè)結(jié)點(diǎn)的指針
  • 指向后一個(gè)結(jié)點(diǎn)的指針
  • 指向結(jié)點(diǎn)包含的字符串值的指針

其中字符串值又分為三個(gè)部分: P209

  • 字符串的長度
  • 字符串剩余可用的字節(jié)數(shù)
  • 以空字符結(jié)尾的字符串本身

可以發(fā)現(xiàn)未壓縮前,每存儲一個(gè)字符串,最少需要 21 字節(jié)的額外開銷 (overhead) 。(三個(gè)指針每個(gè)占 4 個(gè)字節(jié),兩個(gè)整數(shù)每個(gè)占 4 個(gè)字節(jié),字符串結(jié)尾的空字符占 1 個(gè)字節(jié)) P209

壓縮列表 P209

壓縮列表是由結(jié)點(diǎn)(非真實(shí)結(jié)點(diǎn))組成的序列 (sequence) ,每個(gè)結(jié)點(diǎn)都由兩個(gè)長度值和一個(gè)字符串組成。 P209

  • 第一個(gè)長度值:前一個(gè)結(jié)點(diǎn)的長度,用于從后向前的遍歷(一般以一個(gè)字節(jié)存儲)
  • 第二個(gè)長度值:當(dāng)前結(jié)點(diǎn)的長度(一般以一個(gè)字節(jié)存儲)
  • 字符串:長度等于字節(jié)數(shù),沒有空字符

可以發(fā)現(xiàn)壓縮后,每存儲一個(gè)字符串,最少需要 2 字節(jié)的額外開銷。 P210

使用壓縮列表編碼 P210

不同結(jié)構(gòu)關(guān)于使用壓縮列表的配置選項(xiàng) P210

# 列表使用壓縮列表表示的限制條件
list-max-ziplist-entries 512
list-max-ziplist-value 64

# 散列使用壓縮列表表示的限制條件
hash-max-ziplist-entries 512
hash-max-ziplist-value 64

# 有序集合使用壓縮列表表示的限制條件
zset-max-ziplist-entries 512
zset-max-ziplist-value 64

其中, ...-entries 選項(xiàng)說明列表、散列和有序集合在被編碼為壓縮列表的情況下,允許包含的最大元素?cái)?shù)量; ...-value 選項(xiàng)說明了壓縮列表每個(gè)結(jié)點(diǎn)的最大體積是多少字節(jié)。當(dāng)這些選項(xiàng)設(shè)置的限制條件中的任意一個(gè)被突破時(shí), Redis 就會將對應(yīng)的列表、散列和有序集合從壓縮列表編碼轉(zhuǎn)換為其他結(jié)構(gòu),而內(nèi)存占用也會因此增加,并且即使其將來重新滿足限制條件,也不會再轉(zhuǎn)換回壓縮列表。 P210

調(diào)試 P210

OBJECT 命令允許從內(nèi)部查看給定 key 的 Redis 對象, 它通常用在調(diào)試(debugging) 或者了解為了節(jié)省空間而對 key 使用特殊編碼的情況。當(dāng)將Redis 用于進(jìn)行緩存時(shí),也可以通過 OBJECT 命令中的信息,決定 key 的驅(qū)逐策略 (eviction policies) 。

  • OBJECT REFCOUNT <key>: 返回給定 key 引用所儲存的值的次數(shù)。主要用于調(diào)試
  • OBJECT ENCODING <key>: 返回給定 key 所儲存的值所使用的內(nèi)部表示(representation)
  • OBJECT IDLETIME <key>: 返回給定 key 自儲存以來的空閑時(shí)間(idle, 沒有被讀取也沒有被寫入),以秒為單位
集合的整數(shù)集合編碼 P211

如果集合的所有成員都可以被解釋為十進(jìn)制整數(shù)(在平臺的有符號整數(shù)范圍內(nèi)),并且集合成員的數(shù)量足夠少,那么 Redis 就會以有序整數(shù)數(shù)組的方式存儲集合,這種存儲方式又被稱為整數(shù)集合 (intset) 。整數(shù)集合不僅可以降低內(nèi)存消耗,還可以提升所有標(biāo)準(zhǔn)集合操作的執(zhí)行速度。 P211

整數(shù)集合的配置選項(xiàng) P211

# 集合使用整數(shù)集合表示的限制條件
set-max-intset-entries 512

當(dāng)整數(shù)集合包含當(dāng)元素?cái)?shù)量超過配置選項(xiàng)設(shè)定的限制時(shí),整數(shù)集合將被轉(zhuǎn)換為散列表表示。 P212

長壓縮列表和大整數(shù)集合帶來的性能問題 P212
壓縮列表結(jié)點(diǎn)數(shù) 性能
< 1000 差別不大
5000 ~ 10000 開始下降
50000 下降明顯
> 100000 低到無法使用

推薦將壓縮列表的長度限制在 1024 個(gè)元素內(nèi),并且每個(gè)元素的體積不能超過 64 字節(jié),對于大多數(shù)散列應(yīng)用來說,這種配置可以同時(shí)兼顧低內(nèi)存占用和高性能這兩方面優(yōu)點(diǎn)。 P214

Redis 在 3.2 版本后的列表底層默認(rèn)使用 quicklist ,這種數(shù)據(jù)結(jié)構(gòu)兼顧了雙向鏈表和壓縮列表的優(yōu)點(diǎn),因此列表目前來說已使用最優(yōu)配置。

我們在設(shè)計(jì) Redis 時(shí)同時(shí)也要保持鍵名簡短(包括數(shù)據(jù)鍵、散列的域、集合和有序集合的成員以及所有列表的結(jié)點(diǎn)),當(dāng)存儲結(jié)點(diǎn)的數(shù)據(jù)量達(dá)到上百萬個(gè)或者數(shù)十億個(gè)時(shí),將能節(jié)省 MB 升至 GB 級的空間。 P214

分片結(jié)構(gòu) P214

分片 (sharding) 本質(zhì)上就是基于某些簡單的規(guī)則將數(shù)據(jù)劃分為更小的部分,然后根據(jù)數(shù)據(jù)所屬的部分來決定將數(shù)據(jù)發(fā)送到哪個(gè)位置上面。這種技術(shù)可以擴(kuò)展存儲空間并提高所能處理的負(fù)載量。 P214

接下來將把分片的概念應(yīng)用到散列、集合和有序集合上,并在講解實(shí)現(xiàn)這些數(shù)據(jù)結(jié)構(gòu)的其中一部分標(biāo)準(zhǔn)功能的方法。這種情況下,程序不再是將值 X 存儲到鍵 Y 里面,而是將值 X 存儲到鍵 Y:<shardid> 里面。 P214

對列表進(jìn)行分片 P214

在不使用 Lua 腳本的情況下對列表進(jìn)行分配非常困難,因此將在后面介紹使用 Lua 腳本構(gòu)建一個(gè)分片式的列表,并支持以阻塞和非阻塞兩種方式從列表的兩端進(jìn)行推入和彈出操作。

對有序集合進(jìn)行分片 P215

因?yàn)?ZRANGE, ZRANGEBYSCORE, ZRANK, ZCOUNT, ZREMRANGE, ZREMRANGEBYSCORE 這類命令的分片版本需要對有序集合的所有分片進(jìn)行操作才能計(jì)算出命令的最終結(jié)果,所以這些操作無法運(yùn)行得像普通的有序集合操作那么快,因此對有序集合進(jìn)行分片的作用不大。

如果需要將完整的信息存儲到一個(gè)體積較大的有序集合中,但只會對分值排名前 n 位和后 n 位對元素進(jìn)行操作,那么可以使用下面介紹對散列分片對方法對有序集合進(jìn)行分片,并維護(hù)額外對最高分值對有序集合和最低分值對有序集合,然后通過 ZADD 命令為這兩個(gè)有序集合添加新元素,并通過 ZREMRANGEBYRANK 命令確保元素對數(shù)量不會超過限制。 P215

分片式散列 P215

對散列的鍵進(jìn)行劃分時(shí),可以把散列存儲的鍵作為一個(gè)信息源,并使用散列函數(shù)為鍵計(jì)算出一個(gè)數(shù)字散列值,然后根據(jù)需要存儲的鍵的總數(shù)量以及每個(gè)分片需要存儲的鍵數(shù)量,計(jì)算出所需的分片數(shù),最后使用分片數(shù)和散列只決定應(yīng)把鍵存儲到哪個(gè)分片里面。 P215

所思

其實(shí)我們平時(shí)在考慮分片這種形式的時(shí)候是不太會考慮到鍵的總數(shù)量的這種條件,基本上是根據(jù)現(xiàn)有的數(shù)據(jù)進(jìn)行分析后設(shè)定一個(gè)分片數(shù)量 shard_num ,這樣當(dāng)有一個(gè)鍵 key 需要計(jì)算對應(yīng)的分片時(shí),只需要 cal_hash(key) % shard_num 即可得到對應(yīng)的 shard_id 。但類似 CRC32MD5 這種方式進(jìn)行散列值時(shí)有一個(gè)問題,就是書中提到的當(dāng)分片數(shù)量改變時(shí),會有大量鍵的新舊散列值不同,就需要將數(shù)據(jù)遷移至新散列值對應(yīng)的 shard_id 。為了避免這樣的情況,就需要一致性哈希算法,使得分片數(shù)量改變時(shí)需要遷移的數(shù)據(jù)盡量小一點(diǎn),并保證遷移后的數(shù)據(jù)仍能夠較為均勻的在每個(gè)分片上。

將字符串存儲到散列里面 P217

如果發(fā)現(xiàn)將很多相關(guān)聯(lián)的短字符串或者數(shù)字存儲到了字符串鍵里面,并且持續(xù)地將這些鍵命名為 namespace:id 這樣的形式,那么可以考慮將這些值存儲到分片散列里面,在某些情況下,這種做法可以明顯減少內(nèi)存占用。 P217

分片集合 P218

集合一樣可以通過類似散列的方式處理鍵獲得分片 id ,進(jìn)而改造相應(yīng)的命令支持分片式操作。

如果鍵是整數(shù)且最大值相對較小,那么除了直接使用鍵獲取分片 id ,還可以使用位圖 (bitmap) 記錄每個(gè)鍵是否在“集合”中。 P221

如果鍵是整數(shù),數(shù)量非常多,無法全部存下,但又能容忍一定的誤差,那么可以使用布隆過濾器記錄每個(gè)鍵是否在“集合”中(判斷為不存在時(shí),則必定不存在;判斷為存在時(shí),有極低概率不存在)。

打包存儲二進(jìn)制位和字節(jié) P221

前面提到當(dāng)使用類似 namespace:id 這樣當(dāng)字符串鍵去存儲短字符串或者計(jì)數(shù)器時(shí),使用分片散列可以有效降低存儲這些數(shù)據(jù)所需當(dāng)內(nèi)存。但是,如果被存儲的是一些簡短并且長度固定當(dāng)連續(xù) id ,那么我們還有使用比分片散列更為節(jié)約內(nèi)存當(dāng)數(shù)據(jù)存儲方法可用。 P221

Redis 數(shù)據(jù)結(jié)構(gòu)常用命令簡介 中介紹過可用于高效打包和更新 Redis 字符串的四個(gè)命令: P221

  • GETRANGE: 用于讀取被存儲字符串的其中一部分內(nèi)容
  • SETRANGE: 用于對存儲在字符串里面的其中一部分內(nèi)容進(jìn)行設(shè)置
  • GETBIT: 用于獲取字符串里面某個(gè)二進(jìn)制位的值
  • SETBIT: 用于對字符串里面某個(gè)二進(jìn)制位進(jìn)行設(shè)置

通過這四個(gè)命令,我們可以在不對數(shù)據(jù)進(jìn)行壓縮的情況下,使用 Redis 字符串以盡可能緊湊的格式去存儲計(jì)數(shù)器、定長字符串、布爾值等數(shù)據(jù)。 P221

決定被存儲位置信息的格式 P221

我們以存儲的信息是用戶的位置信息為例,不同內(nèi)存的使用量決定了不同的位置精度: P221

  • 1 字節(jié):精確到國家
  • 2 字節(jié):精確到國家及所在州/省
  • 3 字節(jié):精確到郵政編碼
  • 4 字節(jié):精確到經(jīng)緯度(2 米)

這里我們用 2 字節(jié)存儲位置信息,首先我們可以使用一個(gè)數(shù)組存儲所有國家(或地區(qū))的 ISO3 國家(或地區(qū))編碼,然后用第一個(gè)字節(jié)存儲所在國家(或地區(qū))在數(shù)組中的下標(biāo)。然后我們可以使用一個(gè) map ,同樣使用數(shù)組存儲每個(gè)國家(或地區(qū))的州/省信息,用第二個(gè)字節(jié)存儲所在州/省在對應(yīng)數(shù)組中的下標(biāo)。 P222

存儲打包后的數(shù)據(jù) P223

獲取到位置信息對應(yīng)到兩個(gè)字節(jié)到數(shù)據(jù)后,就可以使用 SETRANGE 命令將其存儲到字符串鍵里面去了。但是還需要考慮用戶的總量,假如用戶數(shù)量達(dá)到 7.5 億,那么需要 1.5 GB 內(nèi)存存儲所有用戶的數(shù)據(jù),但 Redis 的字符串鍵最大只能存儲 512 MB 數(shù)據(jù),并且 Redis 在對現(xiàn)有的字符串進(jìn)行設(shè)置的時(shí)候,如果被設(shè)置的部分超過了現(xiàn)有字符串的末尾,那么 Redis 可能需要分配更多的內(nèi)存以存儲新數(shù)據(jù),因此對一個(gè)長字符串的末尾進(jìn)行設(shè)置,耗時(shí)要比執(zhí)行一個(gè)簡單的 SETBIT 調(diào)用多得多。為了解決上述問題,我們需要將數(shù)據(jù)分片到多個(gè)字符串鍵里面。 P223

我們可以在每個(gè)字符串里面存儲 2^20 個(gè)用戶的位置信息,這相當(dāng)于在字符串里面構(gòu)建 100 多萬個(gè)節(jié)點(diǎn),而這樣的字符串需要占 2 MB 的內(nèi)存。 P223

對分片字符串進(jìn)行聚合計(jì)算 P224

對所有用戶的位置信息進(jìn)行聚合計(jì)算 P224

找到提前存儲的最大的用戶 id ,然后計(jì)算最大分片 id ,遍歷每個(gè)字符串分片中的每個(gè)用戶的數(shù)據(jù)(使用 GETRANGE 分塊獲取數(shù)據(jù)),根據(jù)兩個(gè)字節(jié)對應(yīng)的下標(biāo)找到對應(yīng)的國家(或地區(qū))及州/省信息,然后統(tǒng)計(jì)即可。

對指定用戶的位置信息進(jìn)行聚合計(jì)算 P226

遍歷每個(gè)指定的用戶 id ,計(jì)算其對應(yīng)的分片 id 和分片中的偏移量,使用 GETRANGE 獲取對應(yīng)的兩個(gè)字節(jié),根據(jù)兩個(gè)字節(jié)對應(yīng)的下標(biāo)找到對應(yīng)的國家(或地區(qū))及州/省信息,然后統(tǒng)計(jì)即可。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.Redis降低內(nèi)存占用方案 短結(jié)構(gòu) 分片結(jié)構(gòu) 2.短結(jié)構(gòu) 2.1 壓縮列表 以列表為例,列表是由雙向鏈表實(shí)現(xiàn),...
    孤塵F閱讀 1,318評論 0 0
  • 1.原理 短結(jié)構(gòu) Redis為列表、集合、散列和有序集合提供了一組配置選項(xiàng),讓Redis以跟節(jié)約內(nèi)存的方式存儲這些...
    志華_C閱讀 1,113評論 0 0
  • Redis實(shí)戰(zhàn)篇 1 Redis 客戶端 1.1 客戶端通信 原理 客戶端和服務(wù)器通過 TCP 連接來進(jìn)行數(shù)據(jù)交...
    Java小窩閱讀 290評論 0 0
  • Redis 客戶端 客戶端通信原理 客戶端和服務(wù)器通過TCP 連接來進(jìn)行數(shù)據(jù)交互, 服務(wù)器默認(rèn)的端口號為6379 ...
    WEIJAVA閱讀 858評論 0 9
  • redis 和 memcached 的區(qū)別 1. redis支持更豐富的數(shù)據(jù)類型(支持更復(fù)雜的應(yīng)用場景):Redi...
    陳晨_軟件五千言閱讀 402評論 0 1

友情鏈接更多精彩內(nèi)容