REDIS_LIST (列表) 是LPUSH 、LRANGE 等命令的操做對象, 它使用node
REDIS_ENCODING_ZIPLIST 和REDIS_ENCODING_LINKEDLIST 這兩種方式編碼:redis
編碼的選擇數據庫
建立新列表時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 決定是否對客戶端進行阻塞過程:
阻塞
當一個阻塞原語的處理目標爲空鍵時,執行該阻塞原語的客戶端就會被阻塞。
阻塞一個客戶端須要執行如下步驟:
1. 將客戶端的狀態設爲「正在阻塞」 ,並記錄阻塞這個客戶端的各個鍵,以及阻塞的最長時限
(timeout)等數據。
2. 將客戶端的信息記錄到server.db[i]->blocking_keys 中(其中i 爲客戶端所使用的數
據庫號碼)。
3. 繼續維持客戶端和服務器之間的網絡鏈接,但再也不向客戶端傳送任何信息,形成客戶端
阻塞。
步驟2 是未來解除阻塞的關鍵,server.db[i]->blocking_keys 是一個字典,字典的鍵是那
些形成客戶端阻塞的鍵,而字典的值是一個鏈表,鏈表裏保存了全部由於這個鍵而被阻塞的客
戶端(被同一個鍵所阻塞的客戶端可能不止一個):
在上圖展現的blocking_keys 例子中,client2 、client5 和client1 三個客戶端就正被
key1 阻塞,而其餘幾個客戶端也正在被別的兩個key 阻塞。
當客戶端被阻塞以後,脫離阻塞狀態有如下三種方法:
1. 被動脫離:有其餘客戶端爲形成阻塞的鍵推入了新元素。
2. 主動脫離:到達執行阻塞原語時設定的最大阻塞時間。
3. 強制脫離:客戶端強制終止和服務器的鏈接,或者服務器停機。
如下內容將分別介紹被動脫離和主動脫離的實現方式。
阻塞因LPUSH 、RPUSH 、LINSERT 等添加命令而被取消
經過將新元素推入形成客戶端阻塞的某個鍵中,可讓相應的客戶端從阻塞狀態中脫離出來
(取消阻塞的客戶端數量取決於推入元素的數量)。
LPUSH 、RPUSH 和LINSERT 這三個添加新元素到列表的命令, 在底層都由一個
pushGenericCommand 的函數實現,這個函數的運做流程以下圖:
當向一個空鍵推入新元素時,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
屬性的值以下:
若是這時客戶端對該數據庫執行PUSH key3 value ,那麼pushGenericCommand 將建立一個
db 屬性指向0 號數據庫、key 屬性指向key3 鍵對象的readyList 結構,並將它添加到服務器
server.ready_keys 屬性的鏈表中:
在咱們這個例子中,到目前爲止,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 依然處於
阻塞狀態:
阻塞因超過最大等待時間而被取消
前面提到過,當客戶端被阻塞時,全部形成它阻塞的鍵,以及阻塞的最長時限會被記錄在客戶
端裏面,而且該客戶端的狀態會被設置爲「正在阻塞」 。
每次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) # 其餘操做...