1. 程式人生 > 程式設計 >Redis能用來做什麼

Redis能用來做什麼

快取資料庫目前最常用的兩種就是 Redis 和 Memcached,與 Memcached 相比 Redis 其一大特點是支援豐富的資料型別(Memcached 只能用 string 型別)。Redis 因為其豐富的資料結構因此應用範圍不侷限於快取,有很多場景用 Redis 來實現可以大大減少工作量。這篇文章我想總結一下 Redis 不同資料結構可以應用的場景。當然 Redis 的使用場景遠不止文章所列的這些,不過瞭解了一些業界的用法,可以開闊自己的思路。

如果要靈活應用 Redis,首先要熟悉 Redis 各種資料結構所支援的各種命令。不過本文不打算對命令最介紹,因為已經有了許多關於這方面的資料。關於命令的如何使用,可以參考下面兩篇文章:

中文版本可以看:redisdoc.com/index.html

英文版本可以看官網:redis.io/commands

String

簡介

String 資料型別是最常用、最簡單的 key-value 型別,普通的 key-value 儲存都可以歸為此類。value 不僅可以是字串,也可以是數字。string 是二進位制安全的,所以你完全可以把一個圖片檔案的內容作為 string 來儲存。Redis 的 string 可以完全實現目前 Memcached 的功能。除了提供與 Memcached 一樣的get、set、incr、decr 等操作外,Redis還額外提供了下面一些操作:

  • 獲取字串長度
  • 往字串末尾append內容
  • 設定和擷取字串的某一段內容
  • 位操作,redis最大可以支援2^23 - 1位的位操作
  • 批量設定一系列字串的內容

應用場景

快取

快取是使用最多的場景了,對於字串和數字可以直接存取。不過更多時候面臨的是需要將一個結構體或者物件裡的資料快取起來。儲存時可以將結構化資料先序列化,再set到redis中,查詢時,先get到後再反序列化到物件中。

使用快取時,儘量要設定過期時間,不然快取資料過多會很快將redis撐滿。

計數統計

Redis是處理命令是單執行緒處理的,因此Redis的INCR、INCRBY、DECR、DECRBY等指令可以實現原子計數的效果。對於業務上一些簡單的統計和計數需求可以通過Redis的這些命令來實現。

GetSet設定新值,返回舊值。比如實現一個計數器,可以用GetSet獲取計數並重置為0。

分散式id生成器

分散式id生成器應用最廣泛的是Twitter開源的SnowFlake演演算法。如果併發請求量不是很大的情況下,也可用Redis的INCR和INCRBY命令實現idmaker,即生成全域性唯一的id,而且還是嚴格自增的。

定時過期資料

Redis可以通過EXPIRE命令給任意key設定過期時間,因此對於需要定期過期的資料可以Redis來儲存,可以很方便的實現過期功能。比如實現一個分散式session系統。建立session會話時,將session_key儲存到Redis中,並設定過期時間。驗證session_key時先根據uid路由到對應的redis,如取不到session_key,則表示session_key已過期,需要重新登入;如取到session_key且校驗通過則重新更新此session_key的過期時間即可。

分散式選主

Set nx或SetNx命令僅當key不存在時才Set成功。可以用來選舉Master或實現分散式鎖:所有Client不斷嘗試使用SetNx master myName搶注Master,成功的那位不斷使用Expire重新整理它的過期時間。如果Master掛掉了key就會失效,剩下的節點又會發生新一輪搶奪。

分散式鎖

加鎖時通過set nx ex|px命令獲取鎖,如果set成功,說明加鎖成功。加鎖成功時,同時會設定一個超時自動釋放的時間,避免發生死鎖。釋放鎖時通過lua指令碼先get到鎖資訊,確認為加鎖者則del刪除這個key,完成鎖的釋放。關於Redis分散式鎖,後面會單獨用一篇文章來說明。

加鎖命令:

 SET resource-name anystring NX EX max-lock-time
複製程式碼

釋放鎖:

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end
複製程式碼

位操作進行資料統計

Redis的GetBit,SetBit,BitOp,BitCount命令用來進行位操作。BitMap的玩法,比如統計一個使用者的簽到次數,使用者籤一次到就將相應的offset的位置1,然後通過bitcount就可以統計指定範圍內1的個數,也就是該使用者的簽到次數。可以分別統計,一週內的簽到次數,一個月內的簽到次數等。

字串操作

Redis的Append,SetRange,GetRange,StrLen命令,分別實現對文字進行擴充套件、替換、擷取和求長度,對特定資料格式非常有用。

頻率限制

對於HTTP請求,為了防止介面惡意被刷或者限制使用者的操作頻率,常常會使用頻率限制元件,限制使用者在一個時間週期(比如1秒)內,只能請求介面2次。而Redis通過lua指令碼和INCR命令可以很方便的實現週期性的頻率限制。示例程式碼如下:

   # KEYS和ARGV是redis命令傳入lua指令碼的引數,KEYS[1]是頻率限制的KEY,ARGV[1]是增加的次數,ARGV[2]是KEY過期時間
   # 將計數加ARGV[1],如果KEY不存在,INCRBY命令會建立KEY,並初始化該KEY的值為ARGV[1]
   "local current = redis.call('INCRBY',KEYS[1],ARGV[1]);"
   # 如果增加後的值與傳入的值相同,說明是新建立的KEY,表示是該週期內第一次更新,則給該key設定過期時間
   "if tonumber(current) == tonumber(ARGV[1]) "
   "then "
   "redis.call('EXPIRE',ARGV[2]) "
   "end "
   # 返回當前計數值
   "return current";
複製程式碼

實現方式

String 在 redis 內部儲存就是一個字串,不過該字串是 Redis 自己實現的動態字串物件,動態字元可以自動進行擴容。對於整數也是按字串進行儲存,不過執行 INCR 這類對數值型別才能進行的命令時,Redis 會將字串先轉換成數值型別進行運算,然後將運算結果再轉成字串寫進記憶體中。

Hash

簡介

Hash存的是字串和字串值之間的對映。Hash將物件的各個屬性存入Map裡,可以只讀取/更新物件的某些屬性。

應用場景

快取結構化資料

對於使用者資訊比如使用者的暱稱、年齡、性別、積分等,如果使用字串型別進行快取,需要將使用者資料先序列化後再儲存,這時,若只需修改其中某一項屬性值,需要將所有值反序列化出來,然後修改該項資料的值,再序列化後儲存回去。這樣序列化與反序列化的開銷比較大。

string和hash都可以儲存結構化資料,那麼這兩種資料結構該如何選擇呢?

[1] 如果大多數時候要訪問結構化資料中的大多數字段,則使用string,反之則使用hash;

[2] 如果大多數時候只修改結構化資料中某一個欄位的值,則使用hash,反之則使用string;

沒有固定的選擇方法和模式,需要根據需要權衡考量。

不過這裡需要注意,Redis提供了介面(hgetall)可以直接取到全部的屬性資料,但是如果內部hash的成員很多,那麼涉及到遍歷整個內部Map的操作,由於Redis單執行緒模型的緣故,這個遍歷操作可能會比較耗時,而對其它客戶端的請求完全不響應,這點需要格外注意。

建索引

比如User物件,除了id有時還要按name來查詢,可以單獨額外建一個key為name_id的Hash物件儲存從name到id的對映關係。在插入User物件時(set user:101 {"id":101,"name":"calvin"}), 順便往這個hash插入一條(hset name_id calvin 101),這時calvin作為hash裡的一個key,值為101。按name查詢的時候,用hget user:name:id calvin 就能從名為calvin的key裡取出id。假如需要使用多種索引來查詢某條資料時可以使用,一個hash key搞定,避免使用多個string key存放索引值。

計數等功能

hash結構的欄位也可以支援與string相同的HINCRBY等操作,因此同樣可用於實現idmaker,計數等。相比與string型別的優勢是hash可以只用一個key實現多個不同的計數。如果一個

實現方式

Redis Hash對應Value內部實際就是一個HashMap,這裡會有2種不同實現,這個Hash的成員比較少時Redis為了節省記憶體會採用類似一維陣列的方式來緊湊儲存,而不會採用真正的HashMap結構,對應的value redisObject的encoding為zipmap,當成員數量增大時會自動轉成真正的HashMap,此時encoding為ht。

List

簡介

List 是一個雙向連結串列,支援雙向的 pop/push。因為採用的是連結串列來實現,因此即使 list 裡有百萬個元素,也可以在常數時間複雜度內完成 push 操作。不過連結串列也使得按 index 訪問元素的時間複雜度變成了 O(N)。從左 push 還是從右 push,江湖規矩一般從左端 push,右端 pop,即lpush/rpop,而且還有 blocking 的版本 blpop/brpop,客戶端可以阻塞在那直到有訊息到來。還有 rpoplpush/ brpoplpush,彈出來返回給 client的同時,把自己又推入另一個 list,llen 獲取列表的長度。還有按值進行的操作:lrem(按值刪除元素)、linster(插在某個值的元素的前後),複雜度是 O(N),N 是 list 長度,因為 list 的值不唯一,所以要遍歷全部元素,而 set 查詢只要 O(log(N)) 的時間複雜度。

應用場景

各種列表

比如 twitter 的關注列表、粉絲列表、評論列表等也可以用 redis 的 list 結構來實現。

訊息佇列

可以利用 list 的 push 操作,將任務存在 list 中,然後工作執行緒再用 pop 操作將任務取出執行。如果消費者取到訊息後就宕機了怎麼辦?

解決方法之一是加多一個 sorted set,分發的時候同時發到 list 與 sorted set,以分發時間為 score,使用者把任務做完了之後要用 ZREM 消掉 sorted set 裡的 job,並且定時從 sorted set中取出超時沒有完成的任務,重新放回 list。

另一個做法是為每個 worker 多加一個的 list,彈出任務時改用 rpoplpush,將 訊息同時放到 worker 自己的 list 中,完成時用 lrem 消掉。如果叢集管理(如 zookeeper)發現 worker 已經掛掉,就將 worker 的 list 內容重新放回主 list。

但是這兩種方法對於同樣的資料都要儲存多份,並不高效。更好的辦法是使用 ack 機制來保證訊息的可靠性,使用 rrange 來取訊息,通過單獨的位置偏移量來記錄消費的位置,收到 ack 後更新偏移量,然後刪除已經消費的元素。

分頁查詢

利用 lrange 可以很方便的實現 list 內容分頁的功能。

取最新的 N 條資料

取最新 N 個資料的操作:lpush 用來插入一個內容 ID,作為關鍵字儲存在列表頭部。lrem 用來限制列表中的專案數最多為5000。如果使用者需要的檢索的資料量超越這個快取容量,這時才需要把請求傳送到資料庫。

實現方式

Redis list 的實現是壓縮列表或者雙向連結串列,資料較少時用壓縮列表,資料較多時用連結串列。即可以支援反向查詢和遍歷,更方便操作,不過帶來了部分額外的記憶體開銷,redis 內部的很多實現,包括髮送緩衝佇列等也都是用的這個資料結構。

Set

簡介

是一種無序的集合,集合中的元素沒有先後順序,不重複。將重複的元素放入 set 會自動去重。

應用場景

資料去重

一個沒有排序要求的集合,但是要求元素不能重複,那麼 set 是最合適的選擇。並且 set 提供了判斷某個成員是否在一個 set 集合內的重要介面,這個也是 list 所不能提供的。

交併集實現共同關注

將使用者的關注列表放在一個 set 中,set 可以保證去重,這樣就不會重複關注,介面可以實現冪等。另外 redis 還為集合提供了求交集、並集、差集等操作,可以非常方便的實現如共同關注、共同喜好、二度好友等功能,對上面的所有集合操作,你還可以使用不同的命令選擇將結果返回給客戶端還是存集到一個新的集合中。又比如 QQ 有一個社交功能叫做“好友標籤”,大家可以給你的好友貼標籤,比如“大美女”、“土豪”、“歐巴”等等,這裡也可以把每一個使用者的標籤都儲存在一個集合之中。

實現方式:

set 的內部實現是整數集合或者是 hashmap,因此 set 可以很快的實現查詢使用者是否在 set 中。

Sorted Set

簡介

有序集合,與 set 相比 sorted set 在滿足去重的要求下還實現了排序功能,也使得有序集合的使用場景會更多。有序集合元素放入集合時還要提供該元素的分數,有序集合會根據分數進行排序。

使用場景

按時間順序排列的列表

很多場景需要按照時間順序排列列表,一般需要時間最近的排在最前面,那麼就可以用時間戳做score來實現按時間排序的列表。

排行榜

sorted set可以根據分數進行排序,因此可以將排行榜的指標資料作為分數,使用者資訊作為value儲存在sorted set結構中。 得到前100名高分使用者很簡單:ZREVRANGE leaderboard 0 99。查詢某個使用者的排名也很簡單:ZRANK leaderboard 。

按照某種權重排列的列表

比如掘金推薦的文章會根據時間和熱度等資訊來計算每個文章的權重值,可以用這個權重值做score,value為文章的id,那麼就可以實現熱榜的文章列表了。

延時任務

延時任務使用的場景也非常多,比如電商業務使用者30分鐘內未付款就自動取消訂單等。通過redis的zset可以很方便的實現延時任務,具體可以看我這篇文章:基於REDIS實現延時任務

實現方式

sorted set的使用跳躍表(SkipList)來現實資料有序儲存。另外為了實現較快查詢指定元素的分數,redis 還單獨在 hashmap 裡放了成員到 score 的對映,因此可以在 O(1) 的時間複雜度類查詢指定元素的分數。