Redis 和其餘不少 key-value 數據庫的不一樣之處在於,Redis 不只支持簡單的字符串鍵值對,它 還提供了一系列數據結構類型值,好比列表、哈希、集合和有序集,並在這些數據結構類型上 定義了一套強大的 API 。redis
經過對不一樣類型的值進行操做,Redis 能夠很輕易地完成其餘只支持字符串鍵值對的 key-value 數據庫很難(或者沒法)完成的任務。算法
在 Redis 的內部,數據結構類型值由高效的數據結構和算法進行支持,而且在 Redis 自身的構 建當中,也大量用到了這些數據結構。數據庫
1.1 簡單動態字符串數組
Sds (Simple Dynamic String,簡單動態字符串)是 Redis 底層所使用的字符串表示,它被用 在幾乎全部的 Redis 模塊中。安全
1.2 sds 的用途服務器
Sds 在 Redis 中的主要做用有如下兩個:
1. 實現字符串對象(StringObject);
2. 在 Redis 程序內部用做 char* 類型的替代品;數據結構
1.3 總結 SDS:app
• Redis 的字符串表示爲 sds ,而不是 C 字符串(以 \0 結尾的 char*)。 • 對比 C 字符串,sds 有如下特性:數據結構和算法
– 能夠高效地執行長度計算(strlen); – 能夠高效地執行追加操做(append); – 二進制安全;async
• sds 會爲追加操做進行優化:加快追加操做的速度,並下降內存分配的次數,代價是多佔 用了一些內存,並且這些內存不會被主動釋放。
2.1 雙端鏈表
鏈表做爲數組以外的一種經常使用序列抽象,是大多數高級語言的基本數據類型,由於 C 語言自己 不支持鏈表類型,大部分 C 程序都會本身實現一種鏈表類型,Redis 也不例外——它實現了一 個雙端鏈表結構。
Note: Redis 列表使用兩種數據結構做爲底層實現:
1. 雙端鏈表
2. 壓縮列表
由於雙端鏈表佔用的內存比壓縮列表要多,因此當建立新的列表鍵時,列表會優先考慮使用壓 縮列表做爲底層實現,而且在有須要的時候,才從壓縮列表實現轉換到雙端鏈表實現。
雙端鏈表的實現
雙端鏈表的實現由 listNode 和 list 兩個數據結構構成,下圖展現了由這兩個結構組成的一 個雙端鏈表實例:
• Redis 實現了本身的雙端鏈表結構。
• 雙端鏈表主要有兩個做用:
– 做爲 Redis 列表類型的底層實現之一;
– 做爲通用數據結構,被其餘功能模塊所使用; • 雙端鏈表及其節點的性能特性以下:
– 節點帶有前驅和後繼指針,訪問前驅節點和後繼節點的複雜度爲 O(1) ,而且對鏈表 的迭代能夠在從表頭到表尾和從表尾到表頭兩個方向進行;
– 鏈表帶有指向表頭和表尾的指針,所以對錶頭和表尾進行處理的複雜度爲 O(1) ;
– 鏈表帶有記錄節點數量的屬性,因此能夠在 O(1) 複雜度內返回鏈表的節點數量(長度);
3.1 跳躍表
跳躍表(skiplist)是一種隨機化的數據,由 William Pugh 在論文《Skip lists: a probabilistic alternative to balanced trees》中提出,這種數據結構以有序的方式在層次化的鏈表中保存元 素,它的效率能夠和平衡樹媲美——查找、刪除、添加等操做均可以在對數指望時間下完成, 而且比起平衡樹來講,跳躍表的實現要簡單直觀得多。
表頭(head):負責維護跳躍表的節點指針。
跳躍表節點:保存着元素值,以及多個層。
層:保存着指向其餘元素的指針。高層的指針越過的元素數量大於等於低層的指針,爲了 提升查找的效率,程序老是從高層先開始訪問,而後隨着元素值範圍的縮小,慢慢下降層 次。
表尾:所有由NULL組成,表示跳躍表的末尾。 由於跳躍表的定義能夠在任何一本算法或數據結構的書中找到,因此本章不介紹跳躍表的具體
實現方式或者具體的算法,而只介紹跳躍表在 Redis 的應用、核心數據結構和 API 。
跳躍表是一種隨機化數據結構,它的查找、添加、刪除操做均可以在對數指望時間下完
成。
跳躍表目前在 Redis 的惟一做用就是做爲有序集類型的底層數據結構(之一,另外一個構 成有序集的結構是字典)。
爲了適應自身的需求,Redis 基於 William Pugh 論文中描述的跳躍表進行了修改,包括:
1. score 值可重複。
2. 對比一個元素須要同時檢查它的score和memeber。
3. 每一個節點帶有高度爲 1 層的後退指針,用於從表尾方向向表頭方向迭代。
4. Redis 數據類型:
5, 事務
Redis 經過 MULTI 、DISCARD 、EXEC 和 WATCH 四個命令來實現事務功能,本章首先討 論使用 MULTI 、DISCARD 和 EXEC 三個命令實現的通常事務,而後再來討論帶有 WATCH 的事務的實現。
由於事務的安全性也很是重要,因此本章最後經過常見的 ACID 性質對 Redis 事務的安全性進 行了說明。
(1) .事務提供了一種「將多個命令打包,而後一次性、按順序地執行」的機制,而且事務在執行的期 間不會主動中斷——服務器在執行完事務中的全部命令以後,纔會繼續處理其餘客戶端的其餘 命令。
如下是一個事務的例子,它先以 MULTI 開始一個事務,而後將多個命令入隊到事務中,最後 由 EXEC 命令觸發事務,一併執行事務中的全部命令:
redis> MULTI OK
redis> SET book-name "Mastering C++ in 21 days"
81
Redis 設計與實現, 初版
QUEUED
redis> GET book-name QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series" QUEUED
redis> SMEMBERS tag QUEUED
redis> EXEC 1) OK 2) "Mastering C++ in 21 days" 3) (integer) 3 4) 1) "Mastering Series"
2) "C++" 3) "Programming"
一個事務從開始到執行會經歷如下三個階段:
1. 開始事務。
2. 命令入隊。
3. 執行事務。
(2) .命令入隊 當客戶端處於非事務狀態下時,全部發送給服務器端的命令都會當即被服務器執行:
可是,當客戶端進入事務狀態以後,服務器在收到來自客戶端的命令時,不會當即執行命令, 而是將這些命令所有放進一個事務隊列裏,而後返回 QUEUED ,表示命令已入隊:
如下流程圖展現了這一行爲:
Redis 設計與實現, 初版
redis> SET msg "hello moto" OK
redis> GET msg "hello moto"
redis> MULTI OK
redis> SET msg "hello moto" QUEUED
redis> GET msg QUEUED
舉個例子,若是客戶端執行如下命令:
redis> MULTI OK
redis> SET book-name "Mastering C++ in 21 days" QUEUED
redis> GET book-name QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series" QUEUED
redis> SMEMBERS tag QUEUED
那麼程序將爲客戶端建立如下事
數組索引 |
cmd |
argv |
argc |
0 |
SET |
["book-name", "Mastering C++ in 21 days"] |
2 |
1 |
GET |
["book-name"] |
1 |
2 |
SADD |
["tag", "C++", "Programming", "Mastering Series"] |
4 |
3 |
SMEMBERS |
["tag"] |
1 |
(3) 執行事務 前面說到,當客戶端進入事務狀態以後,客戶端發送的命令就會被放進事務隊列裏。
但其實並非全部的命令都會被放進事務隊列,其中的例外就是 EXEC 、DISCARD 、MULTI 和 WATCH 這四個命令——當這四個命令從客戶端發送到服務器時,它們會像客戶端處於非 事務狀態同樣,直接被服務器執行:
程序會首先執行 SET 命令,而後執行 GET 命令,再而後執行 SADD 命令,最後執行 SMEM- BERS 命令。
執行事務中的命令所得的結果會以 FIFO 的順序保存到一個回覆隊列中。 好比說,對於上面給出的事務隊列,程序將爲隊列中的命令建立以下回復隊列:
數組索引 |
回覆類型 |
回覆內容 |
0 |
status code reply |
OK |
1 |
bulk reply |
"Mastering C++ in 21 days" |
2 |
integer reply |
3 |
3 |
multi-bulk reply |
["Mastering Series", "C++", "Programming"] |
當事務隊列裏的全部命令被執行完以後,EXEC 命令會將回復隊列做爲本身的執行結果返回給 客戶端,客戶端從事務狀態返回到非事務狀態,至此,事務執行完畢。
事務的整個執行過程能夠用如下僞代碼表示:
def execute_transaction(): # 建立空白的回覆隊列
reply_queue = []
# 取出事務隊列裏的全部命令、參數和參數數量
for cmd, argv, argc in client.transaction_queue: # 執行命令,並取得命令的返回值
reply = execute_redis_command(cmd, argv, argc) # 將返回值追加到回覆隊列末尾
reply_queue.append(reply) # 清除客戶端的事務狀態
clear_transaction_state(client)
# 清空事務隊列 clear_transaction_queue(client)
# 將事務的執行結果返回給客戶端 send_reply_to_client(client, reply_queue)
4.1.5 在事務和非事務狀態下執行命令
不管在事務狀態下,仍是在非事務狀態下,Redis 命令都由同一個函數執行,因此它們共享很
多服務器的通常設置,好比 AOF 的配置、RDB 的配置,以及內存限制,等等。 不過事務中的命令和普通命令在執行上仍是有一點區別的,其中最重要的兩點是:
非事務狀態下的命令以單個命令爲單位執行,前一個命令和後一個命令的客戶端不必定 是同一個;
而事務狀態則是以一個事務爲單位,執行事務隊列中的全部命令:除非當前事務執行完 畢,不然服務器不會中斷事務,也不會執行其餘客戶端的其餘命令。
在非事務狀態下,執行命令所得的結果會當即被返回給客戶端; 而事務則是將全部命令的結果集合到回覆隊列,再做爲 EXEC 命令的結果返回給客戶端。
4.1.6 事務狀態下的 DISCARD 、MULTI 和 WATCH 命令
除了 EXEC 以外,服務器在客戶端處於事務狀態時,不加入到事務隊列而直接執行的另外三
個命令是 DISCARD 、MULTI 和 WATCH 。
86 Chapter 4. 功能的實現
Redis 設計與實現, 初版 DISCARD 命令用於取消一個事務,它清空客戶端的整個事務隊列,而後將客戶端從事務狀態
調整回非事務狀態,最後返回字符串 OK 給客戶端,說明事務已被取消。
Redis 的事務是不可嵌套的,當客戶端已經處於事務狀態,而客戶端又再向服務器發送 MULTI 時,服務器只是簡單地向客戶端發送一個錯誤,而後繼續等待其餘命令的入隊。MULTI 命令 的發送不會形成整個事務失敗,也不會修改事務隊列中已有的數據。
WATCH 只能在客戶端進入事務狀態以前執行,在事務狀態下發送 WATCH 命令會引起一個 錯誤,但它不會形成整個事務失敗,也不會修改事務隊列中已有的數據(和前面處理 MULTI 的狀況同樣)。
4.1.7 帶 WATCH 的事務
WATCH 命令用於在事務開始以前監視任意數量的鍵:當調用 EXEC 命令執行事務時,若是
任意一個被監視的鍵已經被其餘客戶端修改了,那麼整個事務再也不執行,直接返回失敗。 如下示例展現了一個執行失敗的事務例子:
redis> MULTI OK
redis> SET name peter QUEUED
redis> EXEC (nil)
如下執行序列展現了上面的例子是如何失敗的:
在時間 T4 ,客戶端 B 修改了 name 鍵的值,當客戶端 A 在 T5 執行 EXEC 時,Redis 會發現 name 這個被監視的鍵已經被修改,所以客戶端 A 的事務不會被執行,而是直接返回失敗。
下文就來介紹 WATCH 的實現機制,而且看看事務系統是如何檢查某個被監視的鍵是否被修 改,從而保證事務的安全性的。
4.1.8 WATCH 命令的實現
在每一個表明數據庫的 redis.h/redisDb 結構類型中,都保存了一個 watched_keys 字典,字典 的鍵是這個數據庫被監視的鍵,而字典的值則是一個鏈表,鏈表中保存了全部監視這個鍵的客 戶端。
4.1.9 WATCH 的觸發
在任何對數據庫鍵空間(key space)進行修改的命令成功執行以後(好比 FLUSHDB 、SET 、DEL 、LPUSH 、SADD 、ZREM ,諸如此類),multi.c/touchWatchKey 函數都會被調用 ——它檢查數據庫的 watched_keys 字典,看是否有客戶端在監視已經被命令修改的鍵,若是 有的話,程序將全部監視這個/這些被修改鍵的客戶端的 REDIS_DIRTY_CAS 選項打開:
最後,當一個客戶端結束它的事務時,不管事務是成功執行,仍是失敗,watched_keys 字典
中和這個客戶端相關的資料都會被清除。
4.1.10 事務的 ACID 性質
在傳統的關係式數據庫中,經常用 ACID 性質來檢驗事務功能的安全性。
Redis 事務保證了其中的一致性(C)和隔離性(I),但並不保證原子性(A)和持久性(D)。 如下四小節是關於這四個性質的詳細討論。
原子性(Atomicity)
單個 Redis 命令的執行是原子性的,但 Redis 沒有在事務上增長任何維持原子性的機制,因此
Redis 事務的執行並非原子性的。 若是一個事務隊列中的全部命令都被成功地執行,那麼稱這個事務執行成功。
另外一方面,若是 Redis 服務器進程在執行事務的過程當中被中止——好比接到 KILL 信號、宿主 機器停機,等等,那麼事務執行失敗。
當事務失敗時,Redis 也不會進行任何的重試或者回滾動做。 一致性(Consistency)
Redis 的一致性問題能夠分爲三部分來討論:入隊錯誤、執行錯誤、Redis 進程被終結。 入隊錯誤
在命令入隊的過程當中,若是客戶端向服務器發送了錯誤的命令,好比命令的參數數量 不對,等等,那麼服務器將向客戶端返回一個出錯信息,而且將客戶端的事務狀態設爲 REDIS_DIRTY_EXEC 。
當客戶端執行 EXEC 命令時,Redis 會拒絕執行狀態爲 REDIS_DIRTY_EXEC 的事務,並返回失 敗信息。
redis 127.0.0.1:6379> MULTI OK
redis 127.0.0.1:6379> set key (error) ERR wrong number of arguments for 'set' command
redis 127.0.0.1:6379> EXISTS key QUEUED
redis 127.0.0.1:6379> EXEC (error) EXECABORT Transaction discarded because of previous errors.
所以,帶有不正確入隊命令的事務不會被執行,也不會影響數據庫的一致性。
若是命令在事務執行的過程當中發生錯誤,好比說,對一個不一樣類型的 key 執行了錯誤的操做, 那麼 Redis 只會將錯誤包含在事務的結果中,這不會引發事務中斷或整個失敗,不會影響已執 行事務命令的結果,也不會影響後面要執行的事務命令,因此它對事務的一致性也沒有影響。
Redis 進程被終結
若是 Redis 服務器進程在執行事務的過程當中被其餘進程終結,或者被管理員強制殺死,那麼根
據 Redis 所使用的持久化模式,可能有如下狀況出現:
內存模式:若是 Redis 沒有采起任何持久化機制,那麼重啓以後的數據庫老是空白的,所
以數據老是一致的。
RDB 模式:在執行事務時,Redis 不會中斷事務去執行保存 RDB 的工做,只有在事務執 行以後,保存 RDB 的工做纔有可能開始。因此當 RDB 模式下的 Redis 服務器進程在事 務中途被殺死時,事務內執行的命令,無論成功了多少,都不會被保存到 RDB 文件裏。 恢復數據庫須要使用現有的 RDB 文件,而這個 RDB 文件的數據保存的是最近一次的數 據庫快照(snapshot),因此它的數據可能不是最新的,但只要 RDB 文件自己沒有由於 其餘問題而出錯,那麼還原後的數據庫就是一致的。
AOF 模式:由於保存 AOF 文件的工做在後臺線程進行,因此即便是在事務執行的中途, 保存 AOF 文件的工做也能夠繼續進行,所以,根據事務語句是否被寫入並保存到 AOF 文件,有如下兩種狀況發生:
1)若是事務語句未寫入到 AOF 文件,或 AOF 未被 SYNC 調用保存到磁盤,那麼當進 程被殺死以後,Redis 能夠根據最近一次成功保存到磁盤的 AOF 文件來還原數據庫,只 要 AOF 文件自己沒有由於其餘問題而出錯,那麼還原後的數據庫老是一致的,但其中的 數據不必定是最新的。
2)若是事務的部分語句被寫入到 AOF 文件,而且 AOF 文件被成功保存,那麼不完整的 事務執行信息就會遺留在 AOF 文件裏,當重啓 Redis 時,程序會檢測到 AOF 文件並不 完整,Redis 會退出,並報告錯誤。須要使用 redis-check-aof 工具將部分紅功的事務命令 移除以後,才能再次啓動服務器。還原以後的數據老是一致的,並且數據也是最新的(直 到事務執行以前爲止)。
隔離性(Isolation)
Redis 是單進程程序,而且它保證在執行事務時,不會對事務進行中斷,事務能夠運行直到執
行完全部事務隊列中的命令爲止。所以,Redis 的事務是老是帶有隔離性的。 持久性(Durability)
由於事務不過是用隊列包裹起了一組 Redis 命令,並無提供任何額外的持久性功能,因此事 務的持久性由 Redis 所使用的持久化模式決定:
在單純的內存模式下,事務確定是不持久的。
在 RDB 模式下,服務器可能在事務執行以後、RDB 文件更新以前的這段時間失敗,所
以 RDB 模式下的 Redis 事務也是不持久的。
在 AOF 的「老是 SYNC 」模式下,事務的每條命令在執行成功以後,都會當即調用 fsync
或 fdatasync 將事務數據寫入到 AOF 文件。可是,這種保存是由後臺線程進行的,主 4.1. 事務 91
Redis 設計與實現, 初版
Redis 設計與實現, 初版 線程不會阻塞直到保存成功,因此從命令執行成功到數據保存到硬盤之間,仍是有一段
很是小的間隔,因此這種模式下的事務也是不持久的。
其餘 AOF 模式也和「老是 SYNC 」模式相似,因此它們都是不持久的。
4.1.11 小結
事務提供了一種將多個命令打包,而後一次性、有序地執行的機制。
事務在執行過程當中不會被中斷,全部事務命令執行完以後,事務才能結束。
多個命令會被入隊到事務隊列中,而後按先進先出(FIFO)的順序執行。
帶WATCH命令的事務會將客戶端和被監視的鍵在數據庫的watched_keys字典中進行關 聯,當鍵被修改時,程序會將全部監視被修改鍵的客戶端的 REDIS_DIRTY_CAS 選項打開。
只有在客戶端的REDIS_DIRTY_CAS選項未被打開時,才能執行事務,不然事務直接返回 失敗。
Redis 的事務保證了 ACID 中的一致性(C)和隔離性(I),但並不保證原子性(A)和 持久性(D)。