淺入淺出 Redis
Redis 可謂是當前最知名的 In-memory Database ,常常被拿來當快取資料庫。
由 C 語言撰寫的 Redis 為鍵值對( Key - Value )資料庫,資料皆儲存於 Memory 所以讀寫的速度非常快,只不過這導致 Redis 若不小心關閉,所有資料會直接消失。因此它也提供可選的持久化設定,開啟的話可以在某種程度上避免資料遺失。
可以想成 Redis 是一個獨立於程式外的高級 Map (或稱 Dict ),提供許多更進階的功能。
Redis 特色?
Redis 採用單執行緒設計,雖然 4.0 之後有選擇多執行緒,但這不改變核心概念,那就是:『操作命令皆為單執行緒處理』。
單執行緒?
單執行緒最大的一個特點就是不必擔心資料競爭。
假如使用了多執行緒,為了避免競爭問題勢必得加入鎖( Lock )的機制,有經驗的人都很清楚這個機制要妥善處理有多麻煩吧?除了每次上鎖與解鎖的額外消耗,還得注意忘了取鎖、解鎖引發的問題。
此外, Redis 性能瓶頸不是來自於 CPU ,官方的 FQA 也說了:
It’s not very frequent that CPU becomes your bottleneck with Redis, as usually Redis is either memory or network bound. For instance, using pipelining Redis running on an average Linux system can deliver even 1 million requests per second, so if your application mainly uses O(N) or O(log(N)) commands, it is hardly going to use too much CPU.
即使在普通的 Linux 系統上啟動 Redis ,也能在 1sec 的時間內服務 1,000,000 個請求。
引入多執行緒?
確實在 4.0 版本中 Redis 引入多執行緒,這是因為後續加入了可異步處理的刪除操作。
Async | Sync |
---|---|
UNLINK | DEL |
FLUSHALL ASYNC | FLUSHALL |
FLUSHDB ASYNC | FLUSHDB |
要刪除容量不大( Memory Size )的鍵值對不會花太多功夫,所以就算單執行緒同步地刪除也不太會阻塞到其它操作。
不過 Redis 也可能有巨型資料存在,動輒十幾 MB 或是幾百 MB 都可能,而這種龐大的資料不可能短時間內處理完,這樣會導致其它操作卡住。
異步版本的刪除命令,原理是先將 Key 給移除,而實際上 Value 的刪除則是透過其它執行緒來執行,這樣就可以在刪除大型資料時也不導致 Redis 卡住。
至於在 6.0 的多執行緒 I/O 主要是用來處理網路的讀寫,也就是實際上命令的執行依然維持在單執行緒。
啟動與連線
Redis 服務啟動時沒有傳入任何參數的話,預設會監聽 Port 6379 ,可以透過 --port
或是指定設定檔,但是設定檔要自己建立。
1 | # 透過參數指定 Port |
之後就可以藉由 redis-cli 來連線到 Redis ,直接執行時如果沒設定參數,會連向本地的 6379 。
1 | $ redis-cli |
要遠端連接需要透過 -h
指定 Host ; -p
指定 Port 。
密碼
Redis 是可以設置密碼的,有兩種方式,分別是設定檔與操作命令。
設定檔只需要加入 requirepass PASSWORD
這段訊息,並在 Redis Server 啟動時指定使用該設定檔即可。
透過命令的話先連線至已啟動的 Redis Server :
1 | # 先連線到 Redis Server |
當看到 OK 後如果在執行一次 GET 會出現錯誤訊息「 (error) NOAUTH Authentication required. 」,這樣就得透過認證命令來獲取權限:
1 | 127.0.0.1:6379> AUTH "PASSWORD" |
除了透過這種方式登入有設定密碼的 Redis ,也可以在執行 redis-cli 時透過 -a
來傳遞密碼:
1 | $ redis-cli -a PASSWORD |
但這種方式並不推薦,原因在於 -a
送出的密碼會是明文傳遞,很容易被他人攔截到,所以大多情況還是建議使用 AUTH
命令來驗證。
注意要移除密碼不是透過
DEL
,而是要透過SET
將密碼設定為 “” (空字串)
設定檔
Redis 啟動時都會使用一組設定檔,沒有指定時會使用預設值。
設定檔可以指定多種不同的資訊,如前述的密碼,或是開啟主從式架構的關係等,如果想知道當前的設定檔資訊,可以透過前綴 CONFIG
搭配 GET
。
1 | # 回傳的訊息中,奇數為設定的名稱;偶數為設定的值 |
另外也能用 INFO
命令可以看當前 Redis 的相關資訊。
常用操作
說起 Map 結構,其實核心操作不過就是 Get 與 Set 而已。
Redis 支援多種資料型態:
- String
- List
- Set
- Hash
- Sorted Set
- Bitmap
- HyperLogLog
後面有兩個詭異的型態,但這邊先不介紹,通常我們會使用的應該只有前幾個,大部分的命令可以到此查詢。
上列表中沒有出現 Number / Numeric 型態,因為 Redis 會把值都會轉換成 String 儲存,因此部份與數字相關的操作(
INCR
、DECR
),實際上都是先將其轉換為數字才處理
String
可以透過 SET
設置指定 Key 與其相對的 Value (型態為 String ),也能藉由 GET
取出。
1 | # 設定 name - Neko 鍵值對 |
如果值不包含空白,可以不加入雙引號:
1 | 127.0.0.1:6379> SET name Neko |
當儲存的 Value 是可以被轉換成數字( Integer 、 Float ),就可以使用數字相關的操作,好比說加減的 INCR
與 DECR
(各自等於 INCRBY 1
與 DECRBY 1
)。
如果增減的值屬於浮點數請改用
INCRBYFLOAT
,否則會出現型態轉換錯誤
1 | # 設置數字的 Value |
注意,對於 INCRBY
與 DECRBY
命令的回傳值都會做 Integer 轉換,所以假使你的值為 Float ,那麼回傳值可能會出現錯誤「 (error) ERR value is not an integer or out of range 」。
1 | 127.0.0.1:6379> SET f 0.5 |
也有命令可以一次存取多個鍵值對,這些命令前方會多 M ( MSET
、 MGET
),用法基本上是沒有差別的。
1 | # 透過空白隔離多個鍵值對 |
Redis 在設值時有提供多種方式,可以在特定條件下才設值。
原本這些命令都被拆分開來,但在 2.6.12 時可以透過 SET
參數方式使用。
Command | Option | Desc |
---|---|---|
SETEX | EX | 指定過期時間,到了以後會移除( second ) |
PSETEX | PX | 指定過期時間,到了以後會移除( millisecond ) |
SETNX | NX | 當 Key 不存在時才設值 |
- | XX | 當 Key 存在時才設值 |
透過參數形式的好處是可以混合多種 Option ,好比說我希望在沒有 Key 時設值,且這個值有效時間只有 10 秒,這種情況只能透過 Option 的方式設定。
官方已不再建議使用 Command 形式,上面的三個命令可能會在未來被移除
1 | # 下方兩種方式結果是一樣的 |
List
Redis 的 List 是單純的 String List ,使用時可以將元素放入頭或尾。
其實就是 Linked List ,據官方的說明可以儲存 2^32 - 1 個元素( 4,294,967,295 )。
增加元素時會確認是否已存在 List ,不存在時會自動建立,而透過命令移除元素時如果會使 List 為空,則會移除該 Key 。
意思是如果 List 被清空,那麼等於此 Key 沒被設置過
1 | # 元素放入的方式有左與右( LPUSH 、 RPUSH ),並回傳當前 List 長度 |
Index 的部分支援逆向,所以例子的 -1 意思是倒數最後一個元素,整條命令就是取出 List 上所有元素。
取出元素可以基於 Index 或是如 Queue 一樣在取值時會將其移出 List ,前者就如同程式語言內常用的 List[Index] 操作;後者則是 Queue 的 Pop 操作。
1 | # 以下會建立出 [10, 20, 30] 的陣列 |
Pop 相關的操作還有提供 Block 版本,表示如果 Pop 的對象沒有任何元素可以取出,那麼連線將 Block 直到有元素可以回傳或超時。
命令需給予超時時限( second ),但如果給 0 則表示無限制。
1 | # 如果 nums 左有元素可以取出,則效果同 LPOP 否則會卡在此處直到有元素可回傳或 30 秒後 |
單執行緒的 Redis 在處理 Block 時,會透過 Dict 來儲存 Key 與等待者的資訊,好比說 ClientID 。並在每次處理命令時都檢查是否有元素可以提供。
當然如果是這樣會導致每次跑命令都浪費時間,所以 Redis 利用兩個 Dict 來記錄,名稱分別為 blocking_keys 與 ready_keys 。
- blocking_keys
為 Key : List<Client> 的 Dict ,記錄著有人等待的 Key 與等待者相關訊息。 - ready_keys
當 Push 的 Key 為空 List 時會檢查該 Key 是否存在於 blocking_keys 內,有的話會將相關訊息放入 ready_keys ,也是每次 Redis 處理命令時會檢查的對象。
所以實際上只有在處理 Push 相關訊息時才會檢查 Block 名單,如果該 key 剛好在 Block 名單中就透過 Ready 名單通知 Server 與 Client 。
在
MULTI
/EXEC
中使用 Block 的命令,會因為交易原子性問題馬上回應,效用等同於非 Block 操作
Set
Redis 的 Set 為無序的 String 集合,據官方所說可儲存的數量與 List 同樣為 2^32 - 1 個( 4,294,967,295 )。
與常見的 Set 結構一樣,內部的元素是不允許重複的。
透過 SADD
可以替 Set 放入元素,要是想確認某個元素是否存在於 Set 需要透過 SISMEMBER
。
1 | # SADD 可以一次放入多樣值,回傳 Set 操作後的元素數量 |
移除 Set 內的資料可以透過 SPOP
與 SREM
,
但前者是隨機地移除指定 Set 的元素,後者才可以指定移除。
1 | 127.0.0.1:6379> SADD unique "A" "B" "C" "D" "E" |
如果想確認 Set 目前的數量可以用 SCARD
,不過想知道目前裡面所有的元素內容就得透過 SMEMBERS
。後者的速度比較慢,以時間複雜度來說前者為 O(1) 後者為 O(N) 。
Set 本身也有比較進階的操作,好比說交集( SINTER
)、聯集( SUNION
)、差集( SDIFF
)等,除了聯集以外,另外兩個都是以第一個 Set 為基礎去處理,而非把每個 Set 都視為平等。
Hash
Hash 就是鍵值皆為 String 的 Map 結構,也因此很適合用來模擬 Object 。當然,據官方所說 Hash 同樣可以儲存 2^32 - 1 組鍵值對。
以一個 JSON 物件來當例子可以更好理解 Hash 如何模擬物件:
1 | { |
上述為擁有編號、名稱、等級等資訊的 User 物件,透過 HSET
來設置這個物件。
1 | # 設置成功後會回傳成功加入的鍵值對數量 |
回傳的值是被新加入的鍵值對數量,換句話說修改是不包含在內的,這種情況會收到 0 。
Redis 建議命名 Key 時如果有不同區段訊息,應該透過
:
來區隔而不是_
雖說上面的例子是模擬 Object ,但是身為 Map 結構該有的操作都沒少,例如讀取某個 Key 對應的 Value ,或取出目前所有 Key 、 Value 等命令。
1 | # 單獨取用某個 Key 的值 |
如果要移除 Hash 的某組鍵值對,需要透過 HDEL
。
1 | # 移除時會回傳成功移除掉的鍵值對數量 |
如果 Hash 只儲存少數 Fields (一百個左右),只會佔用很小的空間,也就是說即使是一個微小的 Redis Server 也可以用來儲存百萬個 Object 而不用擔心。
Sorted Set
可排序的 Set ,
對,就這樣,連原理都蠻單純的。
Sorted Set 在儲存資料時會對應到一個分數( Score ),這個分數就是拿來做排序的關鍵,排序上通常會以低 -> 高。
1 | # 一般的 Set |
雖然 Set 結構不允許出現重複的元素,但是分數是可以重複的,官方對於分數重複的描述為:
While the same element can’t be repeated in a sorted set since every element is unique, it is possible to add multiple different elements having the same score. When multiple elements have the same score, they are ordered lexicographically (they are still ordered by score as a first key, however, locally, all the elements with the same score are relatively ordered lexicographically).
當分數一致時,會透過位元排序,也就是會將 String 視為 Byte Array 來做比對。
Sorted Set 除了最基礎的 Set 操作,還有許多獨自擁有的,例如說檢查某個元素當前排名 ZRANK
,查看指定元素目前的分數的 ZSCORE
等。
Sorted Set 有排序的關係,取出存在的元素不是 ZMEMBERS
,而是要改用 ZRANGE
。
1 | 127.0.0.1:6379> ZADD sorted 1 "A" 2 "B" 3 "C" |
也可以透過可排序功能與 Pop 操作,來對任務做權重分配,例如新增會員 Score = 1 而修改會員資料 Score = 3 ,配置對應的 Worker 來取出目前等待中的任務。
但這樣處理時需要注意,假如進來的任務全都是權重高的,就會變成權重低的任務一直沒有處理的情況。
1 | # 透過 Block 系列的 Pop 來等待任務, MIN 指的是 Score 最小的 |
Block 相關的原理請參考 List 末段 Block 操作。
事務
Transaction 是個術語,意思是操作是否能提供 ACID 特性。
這種需求通常跟交易有關,譬如轉賬是從 A 帳戶轉移金錢到 B 帳戶,雖然看起來只需要兩步驟:
- A 扣錢
- B 加錢
但這涉及到操作的不安全性就沒這麼簡單。
好比說 A 已經扣除 1000$ ,但這時幫 B 增加 1000$ 失敗呢?會變成 A 損失 1000$ ,
又好比 A 扣錢失敗,但 B 卻成功增加 1000$ 。
Redis 提供了開始事務的 MULTI
與執行事務的 EXEC
。
1 | # 先設置帳戶 A 與帳戶 B 的金額(各 5000 ) |
但是與大多數人理解的事務不同的是, Redis 透過 MULTI
/ EXEC
的操作只能保持原子性,卻無法保持成功一同成功,失敗一同失敗,因為它不具有 Roll Back (回朔)功能。
換句話說依然可能存在 A 扣款 B 沒入款的情況,而 Redis 之所以不支援 Roll Back 的原因有二:
- Redis 命令失敗的情況只有錯誤的語法使用(且無法於 QUEUED 時發現),這種情況屬於程式邏輯錯誤,不應該出現在生產環境中
- 因為不支援 Roll Back 可以使 Redis 保持簡單與快速
即使如此 Redis 也提供 WATCH
命令以確保與程式搭配時的 Check-And-Set ( CAS )行為,透過 WATCH
可以監視指定的 Keys 是否有被更改過,如果被更改會會導致 Transaction 執行失敗。
1 | # WATCH 必須在 MULTI 前使用 |
很明顯是種樂觀鎖,等於對事務執行設置了前置條件,以沒人改變監視的 Keys 為依據。
使用 WATCH
時要小心,一旦碰上 EXEC
與 DISCARD
都會取消目前所有監視的 Keys ,因為前者是執行事務後者則是放棄事務。
原理
實際上使用 MULTI
時會改變 Client 端的模式,也就是轉換為事務模式。
當處在事務模式的 Client 只要不執行 EXEC
、 DISCARD
都不會取消事務模式,這個模式中的所有命令都會被保存在一個 Queue 內,並回應訊息 『 QUEUED 』。
當執行 EXEC
後會將 Queue 內的命令傳送給 Server ,它便會依照順序依次執行與記錄結果,最終將事務的結果回傳給 Client 。
Lua Script
2.6 時 Redis 引入了 Lua 執行環境,也就是 2.6 版本之後就內建一個 Lua 的 Interpreter ,而 Lua 是一個極輕量的語言,它的目標是成為最容易嵌入其它語言的程式語言。
Redis 對於 Lua 的環境做了許多修改,以避免產生漏洞遭利用,此外也提供由 Lua 呼叫 Redis 命令的函式等。並且為了支援持久化功能, Redis 限制了 Lua 函式必須符合三點:
- 沒有副作用
- 沒有有害的隨機性(比如隨機寫一個鍵值對)
- 同樣的輸入必得出同樣的結果
Redis 保證了 Lua 的原子性,也就是當執行 Lua Script 時不會在中間執行其它 Lua Script 或 Redis Command 。
換句話說 Lua Script 本身就是一種 Transaction ,而且還比原本 MULTI
/ EXEC
更快更簡單。官方雖然短時間內不會移除舊版本的事務機制,但假使未來的使用者都傾向使用 Lua Script 來處理的話,最終可能會把舊有機制給移除。
我自己感覺 Lua Script 提供更多功能與靈活性,若沒特殊原因應該也會選擇 Lua ,但這邊不會再詳細介紹 Lua ,可能未來會找時間寫相關的雜學。