Redis爲咱們提供了5種數據類型,基本上咱們使用頻率最高的就是String,而對其餘四種數據類型使用的頻次稍弱於String。緣由在於:node
這裏咱們不打算羅列這5種數據類型的使用方法,由於這些資料網上有不少。咱們主要討論這5種數據類型的功能特色,弄清楚它們分別適合用於處理哪些現實的業務場景,咱們又該如何組合性使用這5種數據類型,找到解決複雜cache問題的最優方案。算法
1、Redis的數據類型及特色json
咱們來簡要了解一下String、List、Hash、Set及Zset:緩存
1)String服務器
String是Redis提供的字符串類型。能夠針對String類型獨立設置expire time,一般用來存儲長字符串數據,好比某個對象的json字符串。session
在使用上,String類型最巧妙的是能夠動態拼接key。一般咱們能夠將一組id放在Set裏,而後動態查找String仍是否存在,若是不存在說明已通過期或者因爲數據修改主動delete了,須要再作一次cache數據load。數據結構
雖然Set沒法設置item的過時時間,可是咱們能夠將Set Item與String Key關聯來達到相同的效果。多線程
下圖中的左邊是一個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
2)List
List在提升throughput的場景中很是適用,由於它特有的LPUSH、RPUSH、LPOP、RPOP功能能夠無縫的支持生產者、消費者架構模式。
這很是適合實現相似Java Concurrency Fork/Join框架中的work-stealing算法(工做竊取)。
注:Java Fork/Join框架使用並行來提升性能,可是會帶來因爲併發take task帶來的race condition(競態條件)問題,因此採用work-stealing算法來解決因爲競爭問題帶來的性能損耗。
下圖中模擬了一個典型的支付callback峯值場景:
在峯值出現的地方通常咱們都會使用加buffer的方式來加快請求處理速度,這樣才能提升併發處理能力,提升through put。
支付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特性確實能夠在不少場景下提升這種橫向擴展計算能力。
3)Hash
Hash數據類型很明顯是基於Hash算法的,對於項的查找時間複雜度是O(1)的,在極端狀況下可能出現項Hash衝突問題,Redis內部是使用鏈表加key判斷來解決的。具體Redis內部的數據結構咱們在後面有介紹,這裏就不展開了。
Hash數據類型的特色一般能夠用來解決帶有映射關係,同時又須要對某些項進行更新或者刪除等操做。若是不是某個項須要維護,那麼通常能夠經過使用String來解決。
若是有須要對某個字段進行修改,使用String很明顯會多出不少開銷,須要讀取出來反序列化成對象而後操做,而後再序列化寫回Redis,這中間可能還有併發問題。
那咱們可使用Redis Hash提供的實體屬性Hash存儲特性,咱們能夠認爲Hash Value是一個Hash Table,實體的每個屬性都是經過Hash獲得屬性的最終數據索引。
下圖使用Hash數據類型來記錄頁面的a/bmetrics:
左邊的是首頁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來原子增長浮點類型數據。
4)Set
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管道來提交一連串帶有邏輯的命令集合,這些命令在處理期間不會被其餘客戶端的命令干擾。
5)Zset
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最終得出組合排序集合。
2、案例:滬江團購系統大促hot-top接口cache設計
以滬江團購系統大促hot-top接口cache設計爲例,咱們總結了Redis提供的5種數據類型的各自特色和通常的使用場景。可是咱們不只僅能夠分開使用這些數據類型,咱們徹底能夠綜合使用這些數據類型來完成複雜的cache場景。
下面咱們分享一個使用多個Zset、String來優化團購系統前臺接口的例子。因爲篇幅和時間限制,這裏只介紹跟本次案例相關的信息。
注:hot-top接口是指熱點、排名接口的意思,表示它的瀏覽量、併發量比較高,通常大促的時候都會有幾個這種性能要求比較高的接口。
咱們先來分析一個查詢接口所包含的常規信息。
首先一個查詢接口確定是有query condition查詢條件,而後是sort排序信息、最後是page分頁信息。這是通常接口所承擔的基本職責,固然,特殊場景下還須要支持master/slave replication時關於數據session一致性的要求,須要提供跟蹤標記來回master查詢數據,這裏就不展開了。
咱們能夠抽象出這幾個維度的信息:
因爲這裏咱們純粹用Redis來提升cache能力,不涉及到有關於任何搜索的能力,因此這裏忽略其餘複雜查詢的狀況。其實咱們在複雜的地方使用了Elastcsearch來提升搜索能力。
上述咱們分析總結出了一個查詢接口的基本信息,這裏還有一個有關於高併發接口的設計原則,就是將hot-top接口和通常search接口分離開,由於只有分而治之才能分別根據特色選用不一樣的技術。
若是咱們不分職責將全部的查詢場景封裝在一個接口裏,那麼在後面優化接口性能的時候基本就很麻煩了,有些場景是沒法或者很難用cache來解決的,由於接口裏耦合了各類場景邏輯,就算勉強能實現性能也不會高。
前面作這些鋪墊是爲了能在介紹案例的時候達成一個基本的共識。如今咱們來看下這個團購系統的hot-top接口的具體邏輯。
注:在大促的時候須要展示團購列表,這個接口的訪問量是很是大的,團購活動須要根據參團人數倒序排序,而且分頁返回指定數量的團列表。咱們假設這個接口名爲getTopGroups(getTopGroupsRequestrequest)。
1)query condition查詢條件問題
咱們來仔細分析下,首先不一樣的查詢條件從DB裏查詢出來的數據是不同的,也就是說查詢出來的團列表是不同的,可能有company公司、channel渠道等過濾條件。
因爲一個團購活動下不會有太多團,頂多上百個是極限了,因此一個查詢條件出來的團列表也頂多幾十個,並且根據場景分析熱點查詢條件不會超過十個,因此咱們選擇將查詢條件Hash出一個code來緩存本次查詢條件的全量團列表集合,可是這些結果集是沒有任何排序的。
2)sort排序問題
再看根據參團人數排序問題,咱們馬上就能夠想到使用Zset來處理團排序問題,由於只有一個排序維度,因此一個Zset就夠了。咱們使用一個Zset來緩存全部團的參團人數集合,它是一個全量的團排序集合。
那麼咱們如何將用戶的查詢條件出來的團列表根據參團人數排序呢?恰好可使用Zset的交集運算,直接計算出當前這個集合的Zset子集。
3)page分頁問題
經過對已經排序以後的團列表Zset使用Zrange來獲取出分頁集合。咱們來看下完整的流程,如何處理查詢、排序、分頁的。
下圖從query condition計算Hash Code,而後經過DB查詢出當前條件全量團列表:
zset:marketing:groupon:hottop:available:groupkey表示全量團的參團人數,用一個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類型的團詳情信息,這裏就不貼出代碼了。
加q羣:834962734 可獲取一份Java架構進階學習資源(高併發+Spring源碼+JVM原理解析+分佈式架構+微服務架構+多線程併發原理等...這些成爲架構師必備的內容)以及Java進階學習路線圖。
因爲篇幅和時間關係,咱們不展開太多的業務場景介紹了。這其中還涉及到計算cache過時時間的問題,這也跟促銷活動的運營規則有關係,還涉及到有可能query condition hash衝突問題等,可是這些已經不與咱們本節主題相關。