標籤: Redis Redis數據結構 Redis內存管理策略 Redis數據類型 Redis類型映射java
做者:王清培(Plen wang) 滬江Java資深架構師node
redis 爲咱們提供了 5 種數據類型,基本上咱們使用頻率最高的就是 string ,而對其餘四種數據類型使用的頻次稍弱於 string 。redis
一方面是因爲 string 使用起來比較簡單,能夠方便存儲複雜大對象,使用場景比較多。還有一個緣由就是因爲 redis expire time 只能設置在 key 上,像 list、hash、set、zset 屬於集合類型,會管理一組 item,咱們沒法在這些集合的 item 上設置過時時間,因此使用 expire time 來處理集合的 cache 失效會變得稍微複雜些。可是 string 使用 expire time 來管理過時策略會比較簡單,由於它包含的項少。這裏說的集合是寬泛的相似集合。算法
致使咱們習慣性的使用 string 而忽視其餘四種數據類型的另外一個深層次緣由,大可能是因爲咱們對另外四種數據類型的使用和原理不是太瞭解。這個時候每每會忽視在特定場景下使用某種數據類型可能會比 string 性能高出不少,好比使用 hash 結構來提升某個實體的某個項的修改等。json
這裏咱們不打算羅列這 5 種數據類型的使用方法,這些資料網上有不少。咱們主要討論這 5 種數據類型的功能特色,這些特色分別適合用於處理哪些現實的業務場景,最重要的是咱們如何組合性的使用這 5 種數據類型來解決複雜的 cache 問題。數組
string 是 redis 提供的字符串類型。能夠針對 string 類型獨立設置 expire time 。一般用來存儲長字符串數據,好比,某個對象的 json 字符串。緩存
string 類型咱們在使用上最巧妙的是能夠動態拼接 key。一般咱們能夠將一組 id 放在 set 裏,而後動態查找 string 仍是否存在,若是不存在說明已通過期或者因爲數據修改主動 delete 了,須要再作一次 cache 數據 load 。服務器
雖然 set 沒法設置 item 的過時時間,可是咱們能夠將 set item 與 string key 關聯來達到相同的效果。session
上圖中的左邊是一個 key 爲 set:order:ids 的 set 集合,它多是一個全量集合,也多是某個查詢條件獲取出來的一個集合。數據結構
有時候複雜點的場景須要多個 set 集合來支撐計算,在 redis 服務器 裏可能會有不少相似這樣的集合。
這些集合咱們能夠稱爲 功能數據,這些數據是用來輔助 cache 計算的,當進行各類集合運算以後會得出當前查詢須要返回的子集,最後咱們纔會去獲取某個訂單真正的數據。
這些 string:order:{orderId} 字符串 key 並不必定是爲了服務一種場景,而是整個系統最底層的數據,各類場景最後都須要獲取這些數據。那些 set 集合能夠認爲是查詢條件數據,用來輔助查詢條件的計算。
redis 爲咱們提供了 TYPE 命令來查看某個 key 的數據類型,如:string 類型:
SET string:order:100 order-100 TYPE string:order:100 string
list 在提升 throughput 的場景中很是適用,由於它特有的 LPUSH、RPUSH、LPOP、RPOP 功能能夠無縫的支持生產者、消費者架構模式。
這很是適合實現相似 Java Concurrency Fork/Join 框架中的 work-stealing 算法 (工做竊取) 。
java fork/join 框架使用並行來提升性能,可是會帶來因爲併發 take task 帶來的 race condition (競態條件) 問題,因此採用 work-stealing 算法 來解決因爲競爭問題帶來的性能損耗。
上圖中模擬了一個典型的支付 callback 峯值場景。在峯值出現的地方通常咱們都會使用加 buffer 的方式來加快請求處理速度,這樣才能提升併發處理能力,提升 throughput 。
支付 gateway 收到 callback 以後不作任何處理直接交給 分發器 。分發器 是一個無狀態的 cluster ,每一個 node 經過向 註冊中心 pull handler queue list ,也就是獲取下游處理器註冊到註冊中內心的消息通道。
每個分發器 node 會維護一個本地 queue list ,而後順序推送消息到這些 queue list 便可。這裏會有點小問題,就是 支付 gateway 調用分發器的時候是如何作 load balance ,若是不是平均負載可能會有某個 queue list 高出其餘 queue list 。
而分發器不須要作 soft load balance ,由於哪怕某個 queue list 比其餘 queue list 多也無所謂,由於下游 message handler 會根據 work-stealing 算法來竊取其餘消費慢的 queue list 。
redis list 的 LPUSH、RPUSH、LPOP、RPOP 特性確實能夠在不少場景下提升這種橫向擴展計算能力。
hash 數據類型很明顯是基於 hash 算法的,對於項的查找時間複雜度是 O(1) 的,在極端狀況下可能出現項 hash 衝突問題,redis 內部是使用鏈表加 key 判斷來解決的。具體 redis 內部的數據結構咱們在後面有介紹,這裏就不展開了。
hash 數據類型的特色一般能夠用來解決帶有映射關係,同時又須要對某些項進行更新或者刪除等操做。若是不是某個項須要維護,那麼通常能夠經過使用 string 來解決。
若是有須要對某個字段進行修改,使用 string 很明顯是會多出不少開銷,須要讀取出來反序列化成對象而後操做,而後再序列化寫回 redis ,這中間可能還有併發問題。
那咱們可使用 redis hash 提供的實體屬性 hash 存儲特性,咱們能夠認爲 hash value 是一個 hash table ,實體的每個屬性都是經過 hash 獲得屬性的最終數據索引。
上圖使用 hash 數據類型來記錄頁面的 a/b metrics ,左邊的是首頁 index 的各個區域的統計,右邊是營銷 marketing 的各個區域統計。
在程序裏咱們能夠很方便的使用 redis 的 atomic 特性對 hash 某個項進行累加操做。
HMSET hash:mall:page:ab:metrics:index topbanner 10 leftbanner 5 rightbanner 8 bottombanner 20 productmore 10 topshopping 8 OK
HGETALL hash:mall:page:ab:metrics:index 1) "topbanner" 2) "10" 3) "leftbanner" 4) "5" 5) "rightbanner" 6) "8" 7) "bottombanner" 8) "20" 9) "productmore" 10) "10" 11) "topshopping" 12) "8"
HINCRBY hash:mall:page:ab:metrics:index topbanner 1 (integer) 11
使用 redis hash increment 進行原子增長操做。HINCRBY 命令能夠原子增長任何給定的整數,也能夠經過 HINCRBYFLOAT 來原子增長浮點類型數據。
set 集合數據類型能夠支持集合運算,不能存儲重複數據。
set 最大的特色就是集合的計算能力,inter 交集、union 並集、diff 差集,這些特色能夠用來作高性能的交叉計算或者剔除數據。
set 集合在使用場景上仍是比較多和自由的。舉個簡單的例子,在應用系統中比較常見的就是商品、活動類場景。用一個 set 緩存有效商品集合,再用一個 set 緩存活動商品集合。若是商品出現上下架操做只須要維護有效商品 set ,每次獲取活動商品的時候須要過濾下是否有下架商品,若是有就須要從活動商品中剔除。
固然,下架的時候能夠直接刪除緩存的活動商品,可是活動是從 marketing 系統中 load 出來的,就算我將 cache 裏的活動商品刪除,當下次再從 marketing 系統中 load 活動商品時候仍是會有下架商品。固然這只是舉例,一個場景有不一樣的實現方法。
上圖中左右兩邊是兩個不一樣的集合,左邊是營銷域中的可用商品ids集合,右邊是營銷域中活動商品ids集合,中間計算出兩個集合的交集。
SADD set:marketing:product:available:ids 1000100 1000120 1000130 1000140 1000150 1000160
SMEMBERS set:marketing:product:available:ids 1) "1000100" 2) "1000120" 3) "1000130" 4) "1000140" 5) "1000150" 6) "1000160"
SADD set:marketing:activity:product:ids 1000100 1000120 1000130 1000140 1000200 1000300
SMEMBERS set:marketing:activity:product:ids 1) "1000100" 2) "1000120" 3) "1000130" 4) "1000140" 5) "1000200" 6) "1000300"
SINTER set:marketing:product:available:ids set:marketing:activity:product:ids 1) "1000100" 2) "1000120" 3) "1000130" 4) "1000140"
在一些複雜的場景中,也可使用 SINTERSTORE 命令將交集計算後的結果存儲在一個目標集合中。 這在使用 pipeline 命令管道中特別有用,將 SINTERSTORE 命令包裹在 pipeline 命令串中能夠重複使用計算出來的結果集。
因爲 redis 是 Signle-Thread 單線程模型 ,基於這個特性咱們就可使用 redis 提供的 pipeline 管道 來提交一連串帶有邏輯的命令集合,這些命令在處理期間不會被其餘客戶端的命令干擾。
zset 排序集合與 set 集合相似,可是 zset 提供了排序的功能。在介紹 set 集合的時候咱們知道 set 集合中的成員是無序的,zset 填補了集合能夠排序的空隙。
zset 最強大的功能就是能夠根據某個 score 比分值 進行排序,這在不少業務場景中很是急需。好比,在促銷活動里根據商品的銷售數量來排序商品,在旅遊景區里根據流入人數來排序熱門景點等。
基本上人們在作任何事情都須要根據某些條件進行排序。
其實 zset 在咱們應用系統中能用到地方處處都是,這裏咱們舉一個簡單的例子,在團購系統中咱們一般須要根據參團人數來排序成團列表,你們都但願參加那些即將成團的團。
上圖是一個根據團購code建立的zset,score 分值 就是參團人數累加和。
ZADD zset:marketing:groupon:group:codes 5 G_PXYJY9QQFA 8 G_4EXMT6NZJQ 20 G_W7BMF5QC2P 10 G_429DHBTGZX 8 G_KHZGH9U4PP
ZREVRANGEBYSCORE zset:marketing:groupon:group:codes 1000 0 1) "G_W7BMF5QC2P" 2) "G_ZMZ69HJUCB" 3) "G_429DHBTGZX" 4) "G_KHZGH9U4PP" 5) "G_4EXMT6NZJQ" 6) "G_PXYJY9QQFA"
ZREVRANGEBYSCORE zset:marketing:groupon:group:codes 1000 0 withscores 1) "G_W7BMF5QC2P" 2) "20" 3) "G_ZMZ69HJUCB" 4) "10" 5) "G_429DHBTGZX" 6) "10" 7) "G_KHZGH9U4PP" 8) "8" 9) "G_4EXMT6NZJQ" 10) "8" 11) "G_PXYJY9QQFA" 12) "5"
zset 自己提供了不少方法用來進行集合的排序,若是須要 score 分值可使用 withscore 字句帶出每一項的分值。
在一些比較特殊的場合可能須要組合排序,可能有多個 zset 分別用來對同一個實體在不一樣維度的排序,按時間排序、按人數排序等。這個時候就能夠組合使用 zset 帶來的便捷性,利用 pipeline 再結合多個 zset 最終得出組合排序集合。
咱們總結了 redis 提供的 5 種數據類型的各自特色和通常的使用場景。可是咱們不只僅能夠分開使用這些數據類型,咱們徹底能夠綜合使用這些數據類型來完成複雜的 cache 場景。
下面咱們分享一個使用多個 zset 、string 來優化 團購系統 前臺接口的例子。因爲篇幅和時間限制,這裏只介紹跟本次案例相關的信息。
hot-top 接口是指熱點、排名接口的意思,表示它的瀏覽量、併發量比較高,通常大促的時候都會有幾個這種性能要求比較高的接口。
咱們先來分析一個查詢接口所包含的常規信息。
首先一個查詢接口確定是有 query condition 查詢條件 ,而後是 sort 排序信息_ 、最後是 page 分頁信息_ 。這是通常接口所承擔的基本職責,固然,特殊場景下還須要支持 master/slave replication 時關於數據 session 一致性 的要求,須要提供跟蹤標記來回 master 查詢數據,這裏就不展開了。
咱們能夠抽象出這幾個維度的信息:
query condition
查詢條件,companyid=100,sellerid=1010101 諸如此類。
sort
排序信息,通常是默認一個列排序,可是在複雜的場景下會有可能讓接口使用者定製排序字段,好比一些租戶信息列。
page
分頁信息,簡單理解就是數據記錄排完序以後的第幾行到第幾行。
因爲這裏咱們純粹用 redis 來提升 cache 能力,不涉及到有關於任何搜索的能力,因此這裏忽略其餘複雜查詢的狀況。其實咱們在複雜的地方使用了 elastcsearch 來提升搜索能力。
上述咱們分析總結出了一個查詢接口的基本信息,這裏還有一個有關於高併發接口的設計原則就是將 hot-top 接口和通常 search 接口分離開,由於只有分而治之才能分別根據特色選用不一樣的技術。若是咱們不分職責將全部的查詢場景封裝在一個接口裏,那麼在後面優化接口性能的時候基本就很麻煩了,有些場景是沒法或者很難用 cache 來解決的,由於接口裏耦合了各類場景邏輯,就算勉強能實現性能也不會高。
前面作這些鋪墊是爲了能在介紹案例的時候達成一個基本的共識。如今咱們來看下這個團購系統的 hot-top 接口的具體邏輯。
在大促的時候須要展示團購列表,這個接口的訪問量是很是大的,團購活動須要根據參團人數倒序排序,而且分頁返回指定數量的團列表。
咱們假設這個接口名爲 getTopGroups(getTopGroupsRequest request)
咱們來仔細分析下,首先不一樣的查詢條件從 DB 裏查詢出來的數據是不同的,也就是說查詢出來的團列表是不同的,可能有 company 公司 、channel 渠道 等過濾條件。因爲一個團購活動下不會有太多團,頂多上百個是極限了,因此一個查詢條件出來的團列表也頂多幾十個,並且根據場景分析熱點查詢條件不會超過十個,因此咱們選擇將 查詢條件 hash 出一個 code 來緩存本次查詢條件的全量團列表集合,可是這些結果集是沒有任何排序的。
再看根據參團人數排序問題,咱們馬上就能夠想到使用 zset 來處理團排序問題,由於只有一個排序維度,因此一個 zset 就夠了。咱們使用一個 __zset__來緩存全部團的參團人數集合,它是一個全量的團排序集合。
那麼咱們如何將用戶的查詢條件出來的團列表根據參團人數排序尼,恰好可使用 zset 的交集運算直接計算出當前這個集合的 zset 子集。
經過對已經排序以後的團列表 zset 使用 zrange 來獲取出分頁集合。
咱們來看下完整的流程,如何處理查詢、排序、分頁的。
上圖從 query condition 計算 hash code ,而後經過 DB 查詢出當前條件全量團列表。
zset:marketing:groupon:hottop:available:group key 表示全量團的參團人數,用一個 zset 來緩存。接着將這兩個 zset 計算交集,就能夠得出當前查詢所須要的帶有參團人數的 zset ,最後在使用 zrevrange 獲取分頁區間。
ZADD zset:marketing:groupon:hottop:condition:2986080 0 G4ZD5732YZQ 0 G5VW3YF42UC 0 GF773FEJ7CC 0 GFW8DUEND8S 0 GKPKKW8XEY9 0 GL324DGWMZM (integer) 6
ZADD zset:marketing:groupon:hottop:available:group 5 GN7KQH36ZWK 10 GS7VB22AWD4 15 GF773FEJ7CC 17 G5VW3YF42UC 18 G4ZD5732YZQ 32 GTYJKCEJBRR 40 GKPKKW8XEY9 45 GL324DGWMZM 50 GFW8DUEND8S 60 GYTKY4ACWLT (integer) 10
ZINTERSTORE zset:marketing:groupon:hottop:condition:interstore 2 zset:marketing:groupon:hottop:condition:2986080 zset:marketing:groupon:hottop:available:group (integer) 6
ZRANGE zset:marketing:groupon:hottop:condition:interstore 0 -1 withscores 1) "GF773FEJ7CC" 2) "15" 3) "G5VW3YF42UC" 4) "17" 5) "G4ZD5732YZQ" 6) "18" 7) "GKPKKW8XEY9" 8) "40" 9) "GL324DGWMZM" 10) "45" 11) "GFW8DUEND8S" 12) "50"
ZREVRANGE zset:marketing:groupon:hottop:condition:interstore 2 4 withscores 1) "GKPKKW8XEY9" 2) "40" 3) "G4ZD5732YZQ" 4) "18" 5) "G5VW3YF42UC" 6) "17"
有了返回的團 code 集合以後就能夠經過 mget 來批量獲取 string 類型的團詳情信息,這裏就不貼出代碼了。
因爲篇幅和時間關係,這裏就不展開太多的業務場景介紹了。這其中還涉及到計算 cache 過時時間的問題,這也跟促銷活動的運營規則有關係,還涉及到有可能 query condition hash 衝突問題等,可是這些已經不與咱們本節主題相關。
咱們已經瞭解了 redis 提供的 5 種數據類型,那麼 redis 內部究竟是如何支持這 5 種數據類型的,也就是說 redis 究竟是使用什麼樣的數據結構來存儲、查找咱們設置在內存中的數據。
雖然咱們使用 5 種數據類型來緩存數據,可是 redis 會根據咱們存儲數據的不一樣而選用不一樣的數據結構和編碼。
咱們平常使用的是 redis 提供的 5 種數據類型,可是這 5 種數據類型在內存中的數據結構和編碼有不少種。隨着咱們存儲的數據類型的不一樣、數據量的大小不一樣都會引發內存數據結構的動態調整。
本節只是作數據結構和編碼的通常性介紹,不作過多細節討論,一方面是關於 redis 源碼分析的資料網上有不少,還有一個緣由就是 redis 每個版本的實現有很大差別,一旦展開細節討論每個點每個數據結構都會很複雜,因此咱們這裏就不展開討論這些,只是起到拋磚引玉做用。
咱們知道使用 type 命令能夠查看某個 key 是不是 5 種數據類型之一,可是當咱們想查看某個 key 底層是使用哪一種數據結構和編碼來存儲的時候可使用 OBJECT encoding 命令。
SET string:orderid:10101010 10101010 OK
OBJECT encoding string:orderid:10101010 "int"
SET string:orderid:10101010 "orderid:10101010" OK
OBJECT encoding string:orderid:10101010 "embstr"
一樣一個 key ,可是因爲咱們設置的值不一樣而 redis 選用了不一樣的內存數據結構和編碼。雖然 redis 提供的 string 數據類型,可是 redis 會自動識別咱們 cache 的數據類型是 int 仍是 string 。
若是咱們設置的是字符串,且這個字符串長度不大於 39 字節那麼將使用 embstr 來編碼,若是大於 39 字節將使用 raw 來編碼。redis 4.0 將這個閥值擴大了 45 個字節。
除了使用 OBJECT encoding 命令外,咱們還可使用 DEBUG OBJECT 命令來查看更多詳細信息。
DEBUG OBJECT string:orderid:10101010 Value at:0x7fd190500210 refcount:1 encoding:int serializedlength:5 lru:6468044 lru_seconds_idle:8
DEBUG OBJECT string:orderid:10101010 Value at:0x7fd19043be60 refcount:1 encoding:embstr serializedlength:17 lru:6465804 lru_seconds_idle:1942
DEBUG OBJECT 能看到這個對象的 refcount 引用計數 、serializedlength 長度 、lru_seconds_idle 時間 ,這些信息決定了這個 key 緩存清除策略。
簡單動態字符串簡稱 SDS ,在 redis 中全部涉及到字符串的地方都是使用 SDS 實現,固然這裏不包括字面量。 SDS 與傳統 C 字符串的區別就是 SDS 是結構化的,它能夠高效的處理分配、回收、長度計算等問題。
struct sdshdr { unsigned int len; unsigned int free; char buf[]; };
這是 redis 3.0 版本的 sds.h 頭文件定義,3.0.0 以後變化比較大。len 表示字符串長度,free 表示空間長度,buf 數組表示字符串。
SDS 有不少優勢,好比,獲取長度的時間複雜度 O(1) ,不須要遍歷全部 char buf[] 組數,直接返回 len 值。
static inline size_t sdslen(const sds s) { struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); return sh->len; }
固然還有空間分配檢查、空間預分配、空間惰性釋放等,這些都是 SDS 結構化字符串帶來的強大的擴展能力。
鏈表數據結構咱們是比較熟悉的,最大的特色就是節點的增、刪很是靈活。redis List 數據類型底層就是基於鏈表來實現。這是 redis 3.0 實現。
typedef struct list { listNode *head; listNode *tail; void *(*dup)(void *ptr); void (*free)(void *ptr); int (*match)(void *ptr, void *key); unsigned long len; } list;
typedef struct listNode { struct listNode *prev; struct listNode *next; void *value; } listNode;
在 redis 3.2.0 版本的時候引入了 quicklist 鏈表結構,結合了 linkedlist 和 ziplist 的優點。
typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; /* total count of all entries in all ziplists */ unsigned int len; /* number of quicklistNodes */ int fill : 16; /* fill factor for individual nodes */ unsigned int compress : 16; /* depth of end nodes not to compress;0=off */ } quicklist;
typedef struct quicklistNode { struct quicklistNode *prev; struct quicklistNode *next; unsigned char *zl; unsigned int sz; /* ziplist size in bytes */ unsigned int count : 16; /* count of items in ziplist */ unsigned int encoding : 2; /* RAW==1 or LZF==2 */ unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */ unsigned int recompress : 1; /* was this node previous compressed? */ unsigned int attempted_compress : 1; /* node can't compress; too small */ unsigned int extra : 10; /* more bits to steal for future usage */ } quicklistNode;
quicklist 提供了靈活性同時也兼顧了 ziplist 的壓縮能力,quicklist->encoding 指定了兩種壓縮算法。 quicklist->compress 表示咱們能夠進行 quicklist node 的深度壓縮能力。redis 提供了兩個有關於壓縮的配置。
list-max-ziplist-size:ziplist長度控制
list-compress-depth:控制鏈表兩端節點的壓縮個數,越是靠近兩端的節點被訪問的機率越大,因此能夠將訪問機率大的節點不壓縮,其餘節點進行壓縮
對比 redis 3.2 的 quicklist 與 redis 3.0 ,很明顯 quicklist 提供了更加豐富的壓縮功能。redis 3.0 的版本是每一個 listnode 直接緩存值,而 quicklistnode 還有強大的有關於壓縮能力。
LPUSH list:products:mall 100 200 300 (integer) 3
OBJECT encoding list:products:mall "quicklist"