Redisbook學習筆記(3)數據類型之列表

REDIS_LIST (列表) 是LPUSH 、LRANGE 等命令的操做對象, 它使用node

REDIS_ENCODING_ZIPLIST 和REDIS_ENCODING_LINKEDLIST 這兩種方式編碼:redis

wKioL1MITs2QL2gQAACErY08a4I028.jpg


編碼的選擇數據庫

建立新列表時Redis 默認使用REDIS_ENCODING_ZIPLIST 編碼,當如下任意一個條件被知足服務器

時,列表會被轉換成REDIS_ENCODING_LINKEDLIST 編碼:網絡

試圖往列表新添加一個字符串值, 且這個字符串的長度超過數據結構

server.list_max_ziplist_value (默認值爲64 )。ide

ziplist 包含的節點超過server.list_max_ziplist_entries (默認值爲512 )。函數

列表命令的實現編碼

由於兩種底層實現的抽象方式和列表的抽象方式很是接近,因此列表命令幾乎就是經過一對一spa

地映射到底層數據結構的操做來實現的。

咱們將焦點放在BLPOP 、BRPOP 和BRPOPLPUSH 這個幾個阻塞命令的實現原理上。

阻塞的條件

BLPOP 、BRPOP 和BRPOPLPUSH 三個命令均可能形成客戶端被阻塞,如下將這些命令統

稱爲列表的阻塞原語。

阻塞原語並非必定會形成客戶端阻塞:

只有當這些命令被用於空列表時,它們纔會阻塞客戶端。

若是被處理的列表不爲空的話,它們就執行無阻塞版本的LPOP 、RPOP 或RPOPLPUSH

命令。

做爲例子,如下流程圖展現了BLPOP 決定是否對客戶端進行阻塞過程:

wKioL1MITxuS-8G0AADG24Rbqgo857.jpg

阻塞

當一個阻塞原語的處理目標爲空鍵時,執行該阻塞原語的客戶端就會被阻塞。

阻塞一個客戶端須要執行如下步驟:

1. 將客戶端的狀態設爲「正在阻塞」 ,並記錄阻塞這個客戶端的各個鍵,以及阻塞的最長時限

(timeout)等數據。

2. 將客戶端的信息記錄到server.db[i]->blocking_keys 中(其中i 爲客戶端所使用的數

據庫號碼)。

3. 繼續維持客戶端和服務器之間的網絡鏈接,但再也不向客戶端傳送任何信息,形成客戶端

阻塞。

步驟2 是未來解除阻塞的關鍵,server.db[i]->blocking_keys 是一個字典,字典的鍵是那

些形成客戶端阻塞的鍵,而字典的值是一個鏈表,鏈表裏保存了全部由於這個鍵而被阻塞的客

戶端(被同一個鍵所阻塞的客戶端可能不止一個):

wKiom1MIT2aySd1_AAClIHH4ifM237.jpg

在上圖展現的blocking_keys 例子中,client2 、client5 和client1 三個客戶端就正被

key1 阻塞,而其餘幾個客戶端也正在被別的兩個key 阻塞。

當客戶端被阻塞以後,脫離阻塞狀態有如下三種方法:

1. 被動脫離:有其餘客戶端爲形成阻塞的鍵推入了新元素。

2. 主動脫離:到達執行阻塞原語時設定的最大阻塞時間。

3. 強制脫離:客戶端強制終止和服務器的鏈接,或者服務器停機。

如下內容將分別介紹被動脫離和主動脫離的實現方式。

阻塞因LPUSH 、RPUSH 、LINSERT 等添加命令而被取消

經過將新元素推入形成客戶端阻塞的某個鍵中,可讓相應的客戶端從阻塞狀態中脫離出來

(取消阻塞的客戶端數量取決於推入元素的數量)。

LPUSH 、RPUSH 和LINSERT 這三個添加新元素到列表的命令, 在底層都由一個

pushGenericCommand 的函數實現,這個函數的運做流程以下圖:

wKioL1MIT5agg5ZJAAFkgli-0eE983.jpg

當向一個空鍵推入新元素時,pushGenericCommand 函數執行如下兩件事:

1. 檢查這個鍵是否存在於前面提到的server.db[i]->blocking_keys 字典裏,若是是的

話,那麼說明有至少一個客戶端由於這個key 而被阻塞,程序會爲這個鍵建立一個

redis.h/readyList 結構,並將它添加到server.ready_keys 鏈表中。

2. 將給定的值添加到列表鍵中。

readyList 結構的定義以下:

typedef struct readyList {
redisDb *db;
robj *key;
} readyList;

readyList 結構的key 屬性指向形成阻塞的鍵,而db 則指向該鍵所在的數據庫。

舉個例子,假設某個非阻塞客戶端正在使用0 號數據庫,而這個數據庫當前的blocking_keys

屬性的值以下:

wKioL1MIT_Hwx0q9AACa453oTDI826.jpg

若是這時客戶端對該數據庫執行PUSH key3 value ,那麼pushGenericCommand 將建立一個

db 屬性指向0 號數據庫、key 屬性指向key3 鍵對象的readyList 結構,並將它添加到服務器

server.ready_keys 屬性的鏈表中:

wKiom1MIUDTRmqrEAACEjWBbm8A032.jpg

在咱們這個例子中,到目前爲止,pushGenericCommand 函數完成了如下兩件事:

1. 將readyList 添加到服務器。

2. 將新元素value 添加到鍵key3 。

雖然key3 已經再也不是空鍵,但到目前爲止,被key3 阻塞的客戶端尚未任何一個被解除阻塞

狀態。

爲了作到這一點,Redis 的主進程在執行完pushGenericCommand 函數以後,會繼續調用

handleClientsBlockedOnLists 函數,這個函數執行如下操做:

1. 若是server.ready_keys 不爲空, 那麼彈出該鏈表的表頭元素, 並取出元素中的

readyList 值。

2. 根據readyList 值所保存的key 和db ,在server.blocking_keys 中查找全部由於key

而被阻塞的客戶端(以鏈表的形式保存)。

3. 若是key 不爲空,那麼從key 中彈出一個元素,並彈出客戶端鏈表的第一個客戶端,然

後將被彈出元素返回給被彈出客戶端做爲阻塞原語的返回值。

4. 根據readyList 結構的屬性,刪除server.blocking_keys 中相應的客戶端數據,取消

客戶端的阻塞狀態。

5. 繼續執行步驟3 和4 ,直到key 沒有元素可彈出,或者全部由於key 而阻塞的客戶端都

取消阻塞爲止。

6. 繼續執行步驟1 ,直到ready_keys 鏈表裏的全部readyList 結構都被處理完爲止。

用一段僞代碼描述以上操做可能會更直觀一些:

def handleClientsBlockedOnLists():
# 執行直到ready_keys 爲空
while server.ready_keys != NULL:
# 彈出鏈表中的第一個readyList
rl = server.ready_keys.pop_first_node()
# 遍歷全部由於這個鍵而被阻塞的客戶端
for client in all_client_blocking_by_key(rl.key, rl.db):
# 只要還有客戶端被這個鍵阻塞,就一直從鍵中彈出元素
# 若是被阻塞客戶端執行的是BLPOP ,那麼對鍵執行LPOP
# 若是執行的是BRPOP ,那麼對鍵執行RPOP
element = rl.key.pop_element()
if element == NULL:
# 鍵爲空,跳出for 循環
# 餘下的未解除阻塞的客戶端只能等待下次新元素的進入了
break
else:#
清除客戶端的阻塞信息
server.blocking_keys.remove_blocking_info(client)
# 將元素返回給客戶端,脫離阻塞狀態
client.reply_list_item(element)

先阻塞先服務(FBFS)策略

值得一提的是,當程序添加一個新的被阻塞客戶端到server.blocking_keys 字典的鏈表中

時,它將該客戶端放在鏈表的最後,而當handleClientsBlockedOnLists 取消客戶端的阻塞

時,它從鏈表的最前面開始取消阻塞:這個鏈表造成了一個FIFO 隊列,最早被阻塞的客戶端

總值最早脫離阻塞狀態,Redis 文檔稱這種模式爲先阻塞先服務(FBFS,first-block-first-serve)。

舉個例子,在下圖所示的阻塞情況中,若是客戶端對數據庫執行PUSH key3 value ,那麼只有

client3 會被取消阻塞,client6 和client4 仍然阻塞;若是客戶端對數據庫執行PUSH key3

value1 value2 ,那麼client3 和client4 的阻塞都會被取消,而客戶端client6 依然處於

阻塞狀態:

wKioL1MIUM_QyeHRAACpMpSE3CI368.jpg

阻塞因超過最大等待時間而被取消

前面提到過,當客戶端被阻塞時,全部形成它阻塞的鍵,以及阻塞的最長時限會被記錄在客戶

端裏面,而且該客戶端的狀態會被設置爲「正在阻塞」 。

每次Redis 服務器常規操做函數(server cron job)執行時,程序都會檢查全部鏈接到服務器

的客戶端,查看那些處於「正在阻塞」狀態的客戶端的最大阻塞時限是否已通過期,若是是的話,

就給客戶端返回一個空白回覆,而後撤銷對客戶端的阻塞。

能夠用一段僞代碼來描述這個過程:

def server_cron_job():
# 其餘操做...
# 遍歷全部已鏈接客戶端
for client in server.all_connected_client:
# 若是客戶端狀態爲「正在阻塞」,而且最大阻塞時限已到達
if client.state == BLOCKING and \
client.max_blocking_timestamp < current_timestamp():
# 那麼給客戶端發送空回覆, 脫離阻塞狀態
client.send_empty_reply()
# 並清除客戶端在服務器上的阻塞信息
server.blocking_keys.remove_blocking_info(client)
# 其餘操做...
相關文章
相關標籤/搜索