2015/2/16

如何使用 redis

以下根據 redis date types 來查看 redis 基本的資料操作指令,並提供幾個可以利用 redis 實做的使用情境。

redis 指令

redis 指令共有 12 類,從指令的分類,我們可以再區分為

  1. key/value data
    Keys, Strings, Hashes, Lists, Sets, Sorted Sets, HyperLogLog
  2. 連線相關
    Pub/Sub, Transactions
  3. server 系統相關
    Scripting, Connection, Server

redis data types

redis 支援的資料型別

  1. Binary-safe strings
    中文也沒問題的 string
  2. Lists
    string collection,依照insert的順序排序,換句話說就是 linked lists
  3. Sets
    唯一的、沒有順序的 string 集合
  4. Sorted sets
    類似 Sets,但每一個string element關聯到一個稱為 score 的 floating number。所有 elements 都是由 score 排序,可以取得某一個範圍的 elements(例如前10個或最後10個)
  5. Hashes
    key 跟 value 都是 strings 的 map
  6. Bit arrays (or simply bitmaps):
    可透過指令操作 string value 的內容,將 string 視為 an array of bits,例如設定或清除某個 bit,計算設定為 1 的 bits 數量
  7. HyperLogLogs
    這是 probabilistic data structure,用來計算一個 set 的基數 (cardinality of a set)

    在集合論裡面的基數就是用來計算數量並比較數量多寡的方法,有兩個集合 X 與 Y,如果 X 裡面所有元素都可以一對一映射到 Y,那麼就表示 X 與 Y 裡面的元素數量是一樣的。基數並不一定就是一個整數的數字,因為在無限集合(例如所有雙數的集合)中,我們無法直接說出這個集合的基數是某一個數字。

keys

redis keys 是 binary safe,我們可以用一般的字串,或是用 JPEG file 作為 key,空字串也是一個正確的 key。key 的規則如下

  1. 避免使用太長的 key
    例如使用 1024 bytes 的 key 是很糟糕的,除了浪費記憶體與頻寬的問題之外,key lookup 也需要消耗掉一些資源。如果真的有這種情境要使用,建議先將 key 進行一次 SHA1 hash。
  2. 避免使用太短的 key
    在 key 上面增加一些描述用途的字串,可增加可讀性,例如 "user:1000:followers"
  3. try to stick with a schema
    可以在 key 使用"object-type:id"(例如 "user:1000")這樣固定的模式,模式固定有助於 key 的管理,也可以使用 dot 或 dash(例如 "comment:1234:reply.to", "comment:1234:reply-to")

  4. key 最大允許的長度為 512MB

strings

這是最基本的最常使用的資料型別

存取 string key, value,set 將會把既有的 value 取代掉

127.0.0.1:6379> set mykey somevalue
OK
127.0.0.1:6379> get mykey
"somevalue"

set 可加上參數 nx (如果 key 不存在,才能設定成功) 或 xx (如果 key 已經存在,才能設定成功),因為剛剛已經設定了 mykey,所以第一行 nx 的指令是失敗的。

127.0.0.1:6379> set mykey newval nx
(nil)
127.0.0.1:6379> set mykey newval xx
OK

set 可設定 expire time,ex seconds 或是 px miliseconds

127.0.0.1:6379> set mykey test ex 5
OK
127.0.0.1:6379> get mykey
"test"
127.0.0.1:6379> get mykey
"test"
127.0.0.1:6379> get mykey
(nil)
127.0.0.1:6379> get mykey
(nil)

如果 string value 是存放整數,則可以直接進行運算,類似的指令有 incr, incrby, decr, decrby

127.0.0.1:6379> set counter 100
OK
127.0.0.1:6379> incr counter
(integer) 101
127.0.0.1:6379> incr counter
(integer) 102
127.0.0.1:6379> incrby counter 50
(integer) 152
127.0.0.1:6379> incrby counter 50xx
(error) ERR value is not an integer or out of range

如果有兩個 client 同時進行 incr,也不會造成 race condition,因為 redis 可保證 read-increment-set 這個 atomic 操作不會被中斷。

getset 可取得舊值,同時設定為新值

127.0.0.1:6379> getset mykey val2
"newval"
127.0.0.1:6379> getset mykey val3
"val2"

mset, mget 可一次處理多個 key

127.0.0.1:6379> mset a 10 b 20 c 30
OK
127.0.0.1:6379> mget a b c
1) "10"
2) "20"
3) "30"

del 回傳 1 代表既存的 key 已經刪除了,回傳 0 代表不存在該 key
exists 可判斷是否存在某個 key

127.0.0.1:6379> set mykey hello
OK
127.0.0.1:6379> exists mykey
(integer) 1
127.0.0.1:6379> del mykey
(integer) 1
127.0.0.1:6379> del mykey
(integer) 0
127.0.0.1:6379> exists mykey
(integer) 0

用 type 查詢該 key 儲存的 value 的資料型別,none 表示不存在該 key

127.0.0.1:6379> set mykey hello
OK
127.0.0.1:6379> type mykey
string
127.0.0.1:6379> del mykey
(integer) 1
127.0.0.1:6379> type mykey
none

用 expire 設定某個 key 的 expire time,ttl 可查詢該 key 存活的剩餘時間,回傳 -2 表示該 key 不存在,回傳 -1 表示該 key 沒有設定 expire time

127.0.0.1:6379> set mykey hello
OK
127.0.0.1:6379> expire mykey 5
(integer) 1
127.0.0.1:6379> get mykey
"hello"
127.0.0.1:6379> ttl mykey
(integer) 2
127.0.0.1:6379> get mykey
(nil)
127.0.0.1:6379> ttl mykey
(integer) -2

lists

redis 是以 linked list 實做 list,用 lpush 指令加入 1個 elements 跟 加入 10 million 個 elements 所需要的時間是一樣的。缺點是計算 list 裡面的 index 會比較慢,如果程式需要快速取得 value 中某個範圍的 elements,建議改使用 sorted sets。

rpush 將新元素放在 list 最右邊
lpush 將新元素放在 list 最左邊
lrange 由 list 取出一部份的資料

lrange key 第一個index 最後的index
index 為 0 是 list 第一個元素,index 為 -1 是 list 最後一個元素,-2 是 list 倒數第二個元素。

127.0.0.1:6379> rpush mylist A
(integer) 1
127.0.0.1:6379> rpush mylist B
(integer) 2
127.0.0.1:6379> lpush mylist first
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
127.0.0.1:6379> lrange mylist 0 -2
1) "first"
2) "A"

可以一次放入多個 elements

127.0.0.1:6379> rpush mylist 1 2 3 4 5 "foo bar"
(integer) 9
127.0.0.1:6379> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
4) "1"
5) "2"
6) "3"
7) "4"
8) "5"
9) "foo bar"

以 rpop 或 lpop 取出 list 裡面的元素

127.0.0.1:6379> rpush newlist a b c
(integer) 3
127.0.0.1:6379> rpop newlist
"c"
127.0.0.1:6379> lpop newlist
"a"
127.0.0.1:6379> lpop newlist
"b"
127.0.0.1:6379> lpop newlist
(nil)

lrem key count value,移除某個 value
count > 0: 從頭往尾尋找,移除幾個 value
count < 0: 從尾往頭尋找,移除幾個 value
count = 0: 移除所有等於 value 的 elements

# 從尾往頭移除 2 個等於 a 的 value
127.0.0.1:6379> lrem new list -2 a

lists 常用的使用情境

  1. 在 social network 中,記錄使用者的最新貼文
    例如 twitter 的 lastest tweets
  2. processes 之間的資料傳輸,類似 consumer-producer pattern,producer 產生 item 到 list 中,consumer(worker) 取出 item 並執行動作

在某些使用情境中,我們只需要最新的 items,這在 redis 稱為 capped collection,就是只記錄最新的 N items,並用 ltrim 把舊的 item 刪掉。

127.0.0.1:6379> rpush mylist 1 2 3 4 5
(integer) 5
127.0.0.1:6379> ltrim mylist 0 2
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "1"
2) "2"
3) "3"

blocking operations on lists

當我們在 list 使用 producer consumer 模式時,會在 producer 端呼叫 lpush,在 consumer 端呼叫 rpop,但 rpop 可能會因為 list 空了,而回傳 nil。

redis 實做了 brpop, blpop 指令,可在 list 為空的時候,卡住 client 端,直到可取得新的 element 或是 timeout 時間到了。

127.0.0.1:6379> brpop tasks 5
... (client 端等待 5 秒)
(nil)
(5.04s)

127.0.0.1:6379> brpop tasks 5
... (在另一個 client 執行 lpush tasks mytask)
1) "tasks"
2) "mytask"
(3.74s)

rpoplpush, brpoplpush 是更安全的 list 操作指令

127.0.0.1:6379> rpush mylist "one"
(integer) 1
127.0.0.1:6379> rpush mylist "two"
(integer) 2
127.0.0.1:6379> rpush mylist "three"
(integer) 3
127.0.0.1:6379> rpoplpush mylist otherlist
"three"
127.0.0.1:6379> lrange mylist 0 -1
1) "one"
2) "two"
127.0.0.1:6379> lrange otherlist 0 -1
1) "three"

lpush 時,會判斷資料型別,如果不是 list,會回傳 error

127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> lpush foo 1 2 3
(error) WRONGTYPE Operation against a key holding the wrong kind of value

當 list 被 lpop 變成空字串時,就等於把該 key 刪除

127.0.0.1:6379> lpush mylist 1 2
(integer) 2
127.0.0.1:6379> exists mylist
(integer) 1
127.0.0.1:6379> lpop mylist
"2"
127.0.0.1:6379> lpop mylist
"1"
127.0.0.1:6379> lpop mylist
(nil)
127.0.0.1:6379> exists mylist
(integer) 0

hashes

hmset 設定 hash 的多個 fields
hget 取得某個特定的 field
hmget 類似 hget,同時取得多個 fields
hincrby 可直接增加某個 field 的數值

127.0.0.1:6379> hmset user:1000 username antirez birthyear 1977 verified 1
OK
127.0.0.1:6379> hget user:1000
(error) ERR wrong number of arguments for 'hget' command
127.0.0.1:6379> hget user:1000 username
"antirez"
127.0.0.1:6379> hget user:1000 birthyear
"1977"
127.0.0.1:6379> hget user:1000 birthday
(nil)
127.0.0.1:6379> hgetall user:1000
1) "username"
2) "antirez"
3) "birthyear"
4) "1977"
5) "verified"
6) "1"
127.0.0.1:6379> hmget user:1000 username birthyear no-such-field
1) "antirez"
2) "1977"
3) (nil)
127.0.0.1:6379> hincrby user:1000 birthyear 10
(integer) 1987
127.0.0.1:6379> hincrby user:1000 birthyear 10
(integer) 1997

sets

sets 是 unordered collection of strings,這些 strings 不會重複,可以檢測某個 string 是否有在 set 裡面

sadd 增加 set elements
smembers 取得此集合的所有 strings
sismember 檢測是否有存在某個 string

127.0.0.1:6379> sadd myset 1 2 3
(integer) 3
127.0.0.1:6379> smembers myset
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> sismember myset 3
(integer) 1
127.0.0.1:6379> sismember myset 30
(integer) 0

假設新聞 news:1000:tags 有四個 tags
另外有記錄每個 tag 分別都是由 1000 這個 user 標記的
使用 sinter 可取得這些 sets 的交集,結果為 1000

127.0.0.1:6379> sadd news:1000:tags 1 2 5 77
(integer) 4
127.0.0.1:6379> sadd tag:1:news 1000
(integer) 1
127.0.0.1:6379> sadd tag:2:news 1000
(integer) 1
127.0.0.1:6379> sadd tag:5:news 1000
(integer) 1
127.0.0.1:6379> sadd tag:77:news 1000
(integer) 1
127.0.0.1:6379> smembers news:1000:tags
1) "1"
2) "2"
3) "5"
4) "77"
127.0.0.1:6379> sinter tag:1:news tag:2:news tag:10:news tag:27:news
(empty list or set)
127.0.0.1:6379> sinter tag:1:news tag:2:news tag:5:news tag:77:news
1) "1000"

有一副撲克牌 deck,利用 sunionstore 複製一副撲克牌,再用 spop 以亂數抽牌,scard 可計算剩餘的 elements 數量,srandmember則是抽牌後,又將牌放回牌堆。

127.0.0.1:6379> sadd deck C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 CJ CQ CK D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 DJ DQ DK H1 H2 H3  H4 H5 H6 H7 H8 H9 H10 HJ HQ HK S1 S2 S3 S4 S5 S6 S7 S8 S9 S10 SJ SQ SK
(integer) 52
127.0.0.1:6379> sunionstore game:1:deck deck
(integer) 52
127.0.0.1:6379> spop game:1:deck
"S8"
127.0.0.1:6379> spop game:1:deck
"C10"
127.0.0.1:6379> spop game:1:deck
"D9"
127.0.0.1:6379> scard game:1:deck
(integer) 49
127.0.0.1:6379> srandmember game:1:deck
"D5"
127.0.0.1:6379> scard game:1:deck
(integer) 49

sorted sets

每個 sorted set 裡面的 element 都關聯到一個 floating number,稱為 score,此 set 利用以下的規則排序

  1. 如果 A.score > B.score 則 A > B
  2. 如果 A.score = B.score ,且 A string 的字串比 B 大(用字母及長度比較),

zadd 可增加 element,前面的數字是 score
zrange 可取得某個 index 範圍的 elements,最後面加上 withscores,可一併取得 scores
zrevrange 可取得某個 index 範圍的 elements,用反向的順序

127.0.0.1:6379> zadd hackers 1940 "Alan Kay"
(integer) 1
127.0.0.1:6379> zadd hackers 1957 "Sophie Wilson"
(integer) 1
127.0.0.1:6379> zadd hackers 1953 "Richard Stallman"
(integer) 1
127.0.0.1:6379> zadd hackers 1949 "Anita Borg"
(integer) 1
127.0.0.1:6379> zrange hackers 0 -1
1) "Alan Kay"
2) "Anita Borg"
3) "Richard Stallman"
4) "Sophie Wilson"
127.0.0.1:6379> zrevrange hackers 0 -1
1) "Sophie Wilson"
2) "Richard Stallman"
3) "Anita Borg"
4) "Alan Kay"
127.0.0.1:6379> zrange hackers 0 -1 withscores
1) "Alan Kay"
2) "1940"
3) "Anita Borg"
4) "1949"
5) "Richard Stallman"
6) "1953"
7) "Sophie Wilson"
8) "1957"

zrangebyscore 是以 score 的條件範圍取得 elements
zremrangebyscore 將某個score 範圍的元素刪除
zrank 可取得某個元素的 index
zrevrank 可取得某個元素反向的 index

127.0.0.1:6379> zrangebyscore hackers -inf 1950
1) "Alan Kay"
2) "Anita Borg"
127.0.0.1:6379> zremrangebyscore hackers 1940 1950
(integer) 2
127.0.0.1:6379> zrank hackers "Sophie Wilson"
(integer) 1
127.0.0.1:6379> zrevrange hackers 0 -1
1) "Sophie Wilson"
2) "Richard Stallman"

bitmaps

setbit 將第幾個 bit 設定為 1 或 0
getbit 取得某個 bit 的數值
bitop 有四種: AND, OR, XOR, NOT
bitcount 計算 bit 被設定為 1 的數量
bitpos 找到第一個設定為 0 或 1 的 bit 位置

127.0.0.1:6379> setbit key 0 1
(integer) 0
127.0.0.1:6379> setbit key 100 1
(integer) 0
127.0.0.1:6379> bitcount key
(integer) 2
127.0.0.1:6379> getbit key 100
(integer) 1

hyperloglogs (HLL)

這是一種 probabilistic data structure,用來計算某些特殊的東西,例如要計算一個集合的基數,某些精確的計算會耗費較多記憶體,但有時候,我們只需要一個大約的數字,在特別的演算法幫助下,我們就不需要 item 數量 x 單一 item 記憶體量這麼多記憶體。

使用情境

顯示最新的項目列表

在網頁應用中常遇到要顯示最新的 reply,如果是由 DB 取資料會寫成:

SELECT * FROM comments WHERE ... ORDER BY time DESC LIMIT 10

改用 redis,可在發表新 comment 時,就改用以下的方式

# 將新 comment 加入到 list
lpush latest.comments <userid>
# 只保留 5000 個 comments
ltrim latest.comments 0 5000

只有在超過第 5000 筆的情況下,才需要直接存取資料庫。

排行榜

常有線上遊戲,需要依照得分排序,並要能即時更新排序的結果,通常會需要列出前 100 名的選手,列出目前這個 user 的全球排名。

假設有百萬個 user,每分鐘都有百萬個新的分數,每次獲得新分數就

zadd leaderboard <score> <username>

也可以用 userid 換掉 username

前 100 名的選手列表

zrevrange leaderboard 0 99

全球排名

zrank leaderboard <username>

按照用戶投票和時間排序

新聞會根據時間以及使用者投票的得分來排序

score = points / time^alpha

因此時間越久,新聞所得到的分數越低。

每次有新的新聞,就用 LPUSH + LTRIM,保留前 1000筆新聞,並持續以一個 scheduler,計算這 1000個新聞的 score,計算結果由 zadd 排序。

過期項目處理

依照時間排序,每次有新的資料,就搭配時間的屬性 current_time 與 time_to_live,將資料放入 sorted set,用 zrange 查詢 sorted set,可取得最新的10個 item,過期的資料則刪除。

計算數量

搭配 incrby 指令,可計算 user 在網頁上,60s 內的頁面瀏覽量。

incr user:<id>
expire user:<id> 60

特定時間內的特定項目

統計在某段特點時間裡有多少特定用戶訪問了某個特定資源。例如我們想要知道某些特定的註冊用戶或IP地址,他們到底有多少人閱讀了某篇文章。

只要在每一次有人閱讀該文章,就

sadd page:day1:<page_id> <user_id>

想知道特定用戶的數量嗎?只需要使用

SCARD page:day1:<page_id>

需要測試某個特定用戶是否訪問了這個頁面?就用

SISMEMBER page:day1:<page_id> <user_id>

Pub/Sub

redis 實做了 list,因此就可以用來實做 publish/subscribe pattern 的 queue 工作

如果有個任務,需要依序執行,就可以用這功能實做。

利用 bitmap 進行 active user 統計

將 user 1 對應到第一個 bit,user 2 對應到第二個 bit,user 10 對應到第十個 bit,當某個 user 登入系統時,就將該 bit 設定為 1,每天更換一個 key,進行 active user 的記錄。

redis.setbit(play:yyyy-mm-dd, user_id, 1)

因為有每天的紀錄,我們可以利用 AND 運算,得到連續 N 天每天都有使用的 active user。

References

Redis: Zero to Master in 30 minutes - Part 1
[翻譯]Redis: 三十分鐘從入門到精通 - 第一部分

Redis: Zero to Master in 30 minutes - Part 2

使用Redis bitmap進行活躍用戶統計
用Redis bitmap統計活躍用戶、留存

幾點建議,讓Redis在你的系統中發揮更大作用
How to take advantage of Redis just adding it to your stack

各種不同程式語言的 redis clients

jedis