《Redis設計與實現》筆記4—獨立功能的實現

一、發佈與訂閱

Redis的發佈與訂閱功能由PUBLISH、SUBSCRIBE、PSUBSCRIBE等命令組成。通過執行SUBSCRIBE命令,客戶端可以訂閱一個或多個頻道,從而成爲這些頻道的訂閱者(subscriber):每當有其他客戶端向被訂閱的頻道發送消息時,頻道的訂閱者都會接收到這條消息。

除了訂閱頻道外,客戶端還可以通過執行PSUBSCRIBE命令訂閱一個或多個模式,從而成爲這些模式的訂閱者:每當有其他客戶端向某個頻道發送消息時,消息不僅會發送給訂閱這個頻道的訂閱者,還會發送給所有與這個頻道相匹配的模式的訂閱者。

1、頻道的訂閱與退訂

當一個客戶端執行SubScribe命令訂閱某個或某些頻道時,這個客戶端與被訂閱頻道之間就建立起了一種訂閱關係。Redis將所有頻道的訂閱關係保存在服務器狀態的pubsub_channels字典裏面,該字典的鍵是某個被訂閱的頻道,而鍵的值是一個鏈表,鏈表裏面記錄了所有訂閱這個頻道的客戶端。示例如下:

1)、訂閱頻道

當客戶端執行SubScribe命令訂閱某個或某些頻道時,服務器會將客戶端與被訂閱的頻道在pubsub_channels字典中進行關聯。根據頻道是否已經有其他訂閱者,關聯操作分爲兩種執行情況:

  • 頻道已經有其他訂閱者,那麼pubsub_channels字典中必然有相應的訂閱者鏈表,程序要做的就是將客戶端添加到訂閱者鏈表的末尾;

  • 頻道還未有任何訂閱者,那麼它必然不存在於pubsub_channels字典,程序會在pubsub_channels字典中爲頻道創建一個鍵,並將這個鍵的值設置爲空鏈表,然後將客戶端添加到鏈表,成爲鏈表的第一個元素

2)、退訂頻道

UnSubScribe命令的行爲和SubScribe命令的行爲正好相反,當一個客戶端退訂某個或某些頻道時,服務器將從pubsub_channels中解除客戶端與被退訂頻道之間的關聯:

  • 程序會根據被退訂頻道的名字,在pubsub_channels字典中找到頻道對應的訂閱者鏈表,然後從訂閱者鏈表中刪除退訂客戶端的信息;
  • 如果刪除退訂客戶端後,頻道的訂閱者鏈表變爲了空鏈表,說明頻道已經沒有對應的訂閱者了,程序將從pubsub_channels字典中刪除頻道對應的鍵

2、模式的訂閱與退訂

服務器將所有頻道的訂閱關係保存在服務器狀態的pubsub_channels屬性中,與之類似,服務器將所有模式的訂閱關係都保存在服務器狀態的pubsub_patterns屬性裏面。pubsub_patterns屬性是一個鏈表,鏈表中的每個節點都包含着一個pubsubPatterns結構,這個結構的pattern屬性記錄了被訂閱的模式,而client屬性記錄了訂閱模式的客戶端。示例如下:

1)、訂閱模式

每當客戶端執行PSubScribe命令訂閱某個或某些命令時,服務器會對每個被訂閱的模式執行以下兩個操作:

  • 新建一個pubsubPatterns結構,將結構的parttern屬性設置爲被訂閱的模式,client屬性設置爲訂閱模式的客戶端;
  • 將pubsubPatterns結構添加到pubsub_patterns鏈表的表尾

2)、退訂模式

模式的退訂命令PunSubScribe時PSubScribe命令的相反操作,當一個客戶端退訂某個或某些模式時,服務器將在pubsub_patterns鏈表中查找並刪除那些屬性爲退訂模式,並且client屬性爲執行退訂命令的客戶端的pubsubPatterns結構

3、發送消息

當一個Redis客戶端執行PUBLISH <channel> <message>命令將消息message發送給頻道channel時,服務器需要執行以下兩個動作:

  • 將message發送給channel頻道的所有訂閱者;
  • 如果有一個或多個模式pattern與頻道channel向匹配,消息message將發送給paettern模式的訂閱者;

1)、將消息發送給頻道訂閱者

因爲服務器狀態中的pubsub_channels字典記錄了所有頻道的訂閱關係,所以爲了將消息發送給channel頻道的所有訂閱者,PUBLISH命令要做的就是在遍歷pubsub_channels字典,找到頻道channel的訂閱者名單(一個鏈表),然後將消息發送給名單上的所有客戶端

2)、將消息發送給模式訂閱者

因爲服務器狀態中的pubsub_patterns鏈表記錄了所有模式的訂閱關係,所以爲了將消息發送給所有與channel頻道相匹配的模式的訂閱者,PUBLISH命令要做的就是在遍歷pubsub_patterns鏈表,查找與頻道channel相匹配的模式,並將消息發送給訂閱了這些模式的客戶端

4、查看訂閱信息

PUBSUB命令可以用來查看頻道或者模式的相關信息

1)、PubSub Channels

PubSub Channel [pattern]子命令用於返回服務器當前被訂閱的頻道,其中pattern參數是可選的:

  • 如果給定pattern參數,那麼命令將返回服務器當前被訂閱的頻道中那些與pattern模式匹配的頻道;
  • 如果不給定pattern參數,那麼服務器將返回服務器當前被訂閱的所有頻道;

該命令是通過遍歷服務器pubsub_channels字典的所有鍵,然後記錄並返回所有符合條件的頻道來實現的;

2)、PubSub NumSub

PubSub Number [channel-1 channel-2 ... channel-n]子命令接受任意多個頻道作爲輸入參數,並返回這些頻道的訂閱者數量。這個命令是通過在pubsub_channels字典中找到頻道對應的訂閱者鏈表,然後返回訂閱者鏈表的長度(即頻道訂閱數量)來實現的。

3)、PubSub Numpat

PubSub Numpat子命令用於返回服務器當前被訂閱模式的數量。該命令是通過返回pubSub_patterns鏈表的長度來實現的,該鏈表的長度就是服務器被訂閱模式的數量

二、事務

Redis通過Multi、Exec、Watch等命令來實現事務功能。事務提供一種將多個命令請求打包、然後一次性順序執行的機制,並且在事務執行期間,服務器不會中斷事務去執行其他客戶端的命令請求,它會將命令執行完畢後,再入處理其他客戶端的命令請求。

1、事務的實現

事務從開始到結束通常會經歷以下三個階段:①事務開始;②命令入隊;③事務執行

1)、事務開始

Multi命令的執行標誌着事務的開始,該命令可以將執行該命令的客戶端從非事務狀態切換爲事務狀態,該命令是通過在客戶端的flags屬性中打開Redis_Multi標識來完成的

2)、命令入隊

當一個客戶端處於非事務狀態時,這個客戶端發送的命令會被服務器立即執行;而當客戶端切換到事務狀態後,服務器會根據這個客戶端發來的不同命令執行不同的操作:

  • 如果客戶端發送的命令爲Exec、Discard、Watch、Muti命令中的一個,那麼服務器會立即執行這個命令;
  • 如果客戶端發送的命令不屬於上述四個命令,那麼服務器並不會立即執行這個命令,而是將這個命令放入一個事務隊列裏面,然後向客戶端返回Queue回覆;

3)、事務隊列

每個Redis客戶端都有自己的事務狀態,這個事務狀態保存在客戶端狀態的mstate屬性裏面。事務狀態包含一個事務隊列,以及一個已入隊命令的計數器。

事務隊列是一個multiCmd類型的數組,數組中的每個multiCmd結構都保存了一個已入隊命令的相關信息,包含指向命令實現函數的指針、命令參數以及參數的數量。

事務隊列以先進先出的方式保存入隊的命令,較先入隊的命令會被存放到數組的前面,較後入隊的命令會放到數組的後面。

4)、執行事務

當一個處於事務狀態的客戶端向服務器發送Exec命令時,這個Exec命令將立即被服務器執行。服務器會遍歷這個客戶端的事務隊列,執行隊列中保存的所有命令,最後將執行命令所得的結果全部返回給客戶端。

2、Watch命令的實現

Watch命令是一個樂觀鎖,它可以在Exec命令執行前,監視任意數量的數據庫鍵,並在Exec命令執行時,檢查被監視的鍵是否至少有一個已經被修改,如果被修改,服務器將拒接執行事務,並向客戶端返回代表事務執行失敗的空回覆

1)、使用Watch命令監視數據庫鍵

每個Redis數據庫都保存着一個watched_keys字典,這個字典的鍵是某個被watch命令監視的數據庫鍵,而字典的值則是一個鏈表,鏈表中記錄了所有監視相應數據庫鍵的客戶端。通過watched_keys字典,服務器可以清楚的知道哪些數據庫鍵正在被監視,以及哪些客戶端正在監視這些數據庫鍵。執行完Watch命令之後,客戶端將與watched_keys字典中被監視的鍵相關聯。

2)、監視機制的觸發

所有對數據庫進行修改的命令,在執行之後都會調用multi.c/touchWatchKey函數對watched_keys字典進行檢查,查看是否有客戶端正在監視剛剛被命令修改過的數據庫鍵。如果有,touchWatchKey函數會將監視某個鍵的客戶端的Redis_Dirty_Cas標識打開,表示該客戶端的事務安全性已經被破壞。

3)、判斷事務是否安全

當服務器接受到一個客戶端發送的Exec命令時,服務器會根據這個客戶端是否打開了Redis_Dirty_Cas標識來決定是否執行事務:

  • 如果客戶端的Redis_Dirty_Cas標識已經被打開,那麼說明客戶端所監視的鍵當中至少存在一個鍵已經被修改,這意味着事務不再安全,所以服務器會拒絕執行客戶端提交的事務;
  • 如果客戶端的Redis_Dirty_Cas標識沒有被打開,那麼說明客戶端監視的鍵沒有被修改過,事務是安全的,服務器將執行這個事務;

3、事務的ACID性質

在Redis中,事務總是原子性、一致性和隔離性,並且當Redis運行在某種特定的持久化模式下時,事務也具有耐久性

1)原子性

事務的原子性是指,數據庫將事務中的多個操作當作一個整體來執行,要麼執行事務中的所有操作,要麼一個都不執行。

Redis的事務和傳統的關係型數據庫事務的最大區別在於,Redis不支持事務回滾機制,即使事務隊列中的某個命令在執行期間出現了錯誤,整個事務也會繼續執行下去,直到事務隊列中的所有命令都執行完畢爲止。

2)一致性

事務的一致性是指,如果數據庫在執行事務之前是一致的,那麼在事務執行之後,無論事務是否成功,數據庫也應該是一致的。一致指的是數據符合數據庫本身的定義和要求,沒有包含非法或無效的錯誤數據。

Redis通過錯誤檢測和簡單的設計來保證事務的一致性:

  • 入隊錯誤:如果一個事務在入隊過程中,出現了命令不存在,或者命令的格式不正確的情況,那麼Redis將拒絕執行這個事務;
  • 執行錯誤:執行過程中發生的錯誤都是一些不能子啊入隊時被服務器發現的錯誤,這些錯誤只會在命令實際執行時被觸發;即使在事務的執行過程中發生了錯誤,服務器也不會中斷事務的執行,它會繼續執行事務中餘下的其他命令,已執行的命令也不受錯誤命令的影響;
  • 服務器停機:如果服務器在執行事務的過程中停機,那麼根據服務器所使用的持久化模式,可能出現以下情況:①服務器運行在無持久化的內存模式下,重啓後數據庫將是空白的,因爲數據總是一致的;②服務器運行在RDB模式下,那麼事務中途停機也一樣不會造成數據的不一致,因爲服務器可以根據現有的RDB文件來恢復數據,從而將數據庫還原到一個一致的狀態;③服務器運行在AOF模式下,情況同RDB模式

3)隔離性

事務的隔離性是指,即使數據庫中有多個事務併發執行,各個事務之間也不會互相影響,併發狀態下的執行結果與串行執行事務的結果完全一致。因爲Redis使用單線程的方式來執行事務,並且服務器保證執行期間不會對事務進行中斷,因爲Redis事務總是以串行的模式運行

4)耐久性

事務的耐久性是指,當一個事務執行完畢後,執行這個事務的結果會被保存到永久性存儲介質中,即使服務器在執行完事務之後停機,執行事務所得的結果也不會丟失。

Redis沒有爲事務提供額外的持久化操作,所以Redis事務的耐久性由Redis所使用的持久化模式決定:

  • 服務器在無持久化的內存模式下運作時,事務不具有耐久性,一旦服務器停機,所有數據都將丟失;
  • 服務器在RDB持久化模式下運行時,服務器只有在特定的條件下才會對數據庫進行保存操作,而且保存的數據並不能保證事務數據是最新的,所以RDB持久化模式下事務不具有耐久性;
  • 服務器在AOF持久化模式下運作時,並且appendfsync選項的值爲always時,程序總會在執行命令之後調用同步函數將數據保存到本地硬盤中,因而這種模式下務具有耐久性;
  • 服務器在AOF持久化模式下運作時,並且appendfsync選項的值爲everysec時,程序每秒同步一次,但若停機恰好在等待同步的一秒內,可能造成數據的丟失,所以這種模式下事務不具有耐久性;
  • 服務器在AOF持久化模式下運作時,並且appendfsync選項的值爲no時,程序會交由操作系統決定何時將命令數據保存到本地,因而同樣會有數據丟失的可能,所以這種模式下事務不具有耐久性;

注意:no-appendfsync-on-rewrite配置打開時,在執行BGSAVE或BGREWRITEAOF命令時服務器會暫停對AOF文件的同步,所以該選項會對事務的耐久性造成影響;在事務Exec提交前使用SAVE命令可以保證事務的耐久性,但效率太低,一般不使用

三、Lua腳本

Redis從2.6開始引入對Lua腳本的支持,通過在服務器種嵌入Lua環境,Redis客戶端可以使用Lua腳本,直接在服務器端原子的執行多個Redis命令。本章將對腳本管理命令SCRIPT FLUSH、SCRIPT EXISTS、SCRIPT LOAD、SCRIPT KILL命令進行介紹。

1、創建並修改Lua環境

爲了在Redis服務器種執行Lua腳本,Redis在服務器內嵌了一個Lua環境,並對這個Lua環境進行了一系列修改,確保這個Lua環境可以滿足Redis服務器的需要。Redis創建並修改Lua環境的整個過程如下:

  1. 創建一個基礎的Lua環境;
  2. 載入多個函數庫到Lua環境中;
  3. 創建全局表格Redis,表格包含了對Redis進行操作的函數;
  4. 使用Redis自制的隨機函數替換Lua原有的帶副作用的隨機函數;
  5. 創建排序輔助函數,Lua環境使用這個輔助函數對一部分Redis命令的結果進行排序,從而消除這些命令的不確定性;
  6. 創建redis.pcall函數的錯誤報告輔助函數,來提供更爲詳細的錯誤信息;
  7. 對Lua環境中的全局環境進行保護,防止用戶在執行Lua腳本的過程中帶入額外的全局變量;
  8. 將完成修改的Lua環境保存到服務器狀態的Lua屬性中,等待執行服務器傳遞的Lua腳本

1)創建Lua環境

服務器調用Lua的C API函數lua_open,創建一個新的Lua環境,該環境只是一個基礎的環境,下面將對這個環境進行一系列修改

2)載入函數庫

服務器會將以下函數庫載入到Lua環境中,供其使用:

3)創建Redis全局表格

服務器將在Lua環境中創建一個Redis表格,並將它設爲全局變量,這個表格包含以下函數。其中最重要的是redis.call函數和redis.pcall函數,通過這兩個函數,用戶可以直接在Lua腳本中執行Redis命令

4)使用Redis自制的隨機函數替換Lua原有的隨機函數

之前載入函數庫的math函數庫中,用於生成隨機數的math.random和math.randomseed函數都是帶有副作用的,因而Redis使用自帶的隨機函數來替換它們,替換後的兩個函數有如下特徵:

  • 對於相同的Seed,math.Random總產生相同的隨機數序列,且函數爲純函數;
  • 除非在腳本中使用math.randomseed顯式的修改seed,否則每次運行腳本,Lua環境都使用固定的math.randomseed(0)初始化seed

5)創建排序輔助函數

對於一個集合鍵來說,因爲集合元素的排列是無序的,所以即使集合內的元素完全相同,它們的輸出結果也可能不同。爲了消除這種不確定性,服務器會爲Lua環境創建一個排序輔助函數_redis_compare_helper,當Lua腳本執行完一個帶有不確定性的命令後,程序會使用這個函數作爲對比函數,自動調用redis.Sort函數對命令的返回值進行排序後輸出

6)創建redis.pcall函數的錯誤報告輔助函數

服務器將爲Lua環境創建一個名爲_redis_err_handler的錯誤處理函數,當Lua腳本調用redis.pcall函數執行redis命令且出現錯誤時,錯誤處理函數將會打印出錯代碼的來源和行數

7)保護Lua的全局環境

服務器將對Lua環境中的全局環境進行保護,確保傳入服務器的腳本不會因忘記使用local關鍵字而將額外的全局變量添加到Lua環境中。需要注意的是,執行Lua腳本時,要避免錯誤修改了已存在的全局變量,因爲Redis並未禁止用戶修改已存在的全局變量。

8)將Lua環境保存到服務器狀態的lua屬性

在執行完上述步驟後,Redis服務器就已經完成了對Lua環境的修改操作,最後一步就是將Lua環境和服務器狀態的lua屬性關聯起來。因爲Redis使用串行化的方式執行Redis命令,所以最多隻會有一個腳本能夠被放進Lua環境中運行,因此整個Redis服務器只需要創建一個Lua環境即可。

2、Lua環境協作組件

除了創建並修改Lua環境外,Redis服務器還創建了兩個用於與Lua環境進行協作的組件,分別是負責執行Lua腳本中的Redis命令的僞客戶端,以及用於保存Lua腳本的lua_scripts字典

1)僞客戶端

爲了執行Lua腳本中包含的Redis命令,Redis服務器爲Lua環境創建了一個僞客戶端,由這個僞客戶端負責處理Lua腳本中包含的所有Redis命令。Lua腳本使用redis.call函數或redis.pcall函數執行一個Redis命令,需要完成以下步驟:

  1. Lua環境將要redis.call函數或redis.pcall函數要執行的命令傳送給僞客戶端;
  2. 僞客戶端將腳本要執行的命令傳給命令執行器;
  3. 命令執行器執行僞客戶端傳送的命令,並將執行結果返回給僞客戶端;
  4. 僞客戶端接受命令執行器返回的執行結果,並將命令的結果返回給Lua環境;
  5. Lua環境接受到命令結果之後,將結果返回給redis.call函數或redis.pcall函數
  6. 接受到結果的redis.call函數或redis.pcall函數將命令結果作爲函數返回值返回給腳本的調用者;

2)lua_scripts字典

lua_scripts字典的鍵是爲某個Lua腳本的SHA1校驗和,字典的值是SHA1校驗和對應的Lua腳本。Redis服務器將所有被EVAL命令執行過的Lua腳本,以及所有被SCRIPT LOAD命令載入過的Lua腳本保存到lua_scripts字典中。

lua_scripts字典有兩個作用,一是實現SCRIPT EXISTS命令,二是實現腳本複製功能。

3、EVAL命令的實現

EVAL命令的執行過程分爲以下三步:①根據客戶端給定的Lua腳本,在Lua環境中定義一個Lua函數;②將客戶端給定的腳本保存到lua_scripts字典中,等待將來使用;③執行剛剛在Lua環境中定義的函數,以此來執行客戶端給定的Lua腳本

1)定義腳本函數

當客戶端向服務器發送EVAL命令,要求執行某個Lua腳本時,服務器首先要做的就是在Lua環境中,爲傳入的腳本定義一個和這個腳本對應的Lua函數,函數的名字有f_爲前綴加上腳本的SHA1校驗和(四十個字符)組成,函數體則時腳本本身。

使用函數保存客戶端傳入的腳本有以下好處:

  • 執行腳本的步驟非常簡單,只需要直接調用函數就可以了;
  • 通過函數的局部性讓Lua環境保持清潔,減少了垃圾回收的工作量,避免使用全局變量的情況;
  • 若某個腳本對應的函數在Lua環境中被定義過一次,那麼只需要使用腳本的SHA1校驗和,就可以在不知道腳本本身的情況下,直接調用Lua函數來執行腳本,這也是EVALSHA命令的實現原理

2)將腳本保存到lua_scripts字典

服務器將在lua_scripts字典中新增一個鍵值對,其中鍵位Lua腳本的SHA1校驗和,值爲Lua腳本本身。

3)執行腳本函數

腳本保存完成後,服務器還需要進行一些設置一些鉤子,傳入參數等操作才能正式執行腳本,過程如下:

  1. 將Eval命令中傳入的鍵名參數和腳本參數分別保存到KEYS數組和ARGV數組,並將這兩個數組作爲全局變量傳入Lua環境中;
  2. 爲Lua環境裝載超時處理鉤子,可以在腳本出現超時運行的情況下,讓客戶端通過Script Kill命令停止腳本,或者通過SHUTDOWM命令關閉服務器;
  3. 執行腳本函數;
  4. 移除之前載入的超時鉤子;
  5. 將執行腳本函數得到的結果保存到客戶端狀態的輸出緩衝區中,等待服務器將結果返回給客戶端;
  6. 對Lua環境進行垃圾回收操作;

4、EVALSHA命令的實現

同本章3.1介紹的內容,服務器會根據客戶端輸入的SHA1校驗和,檢查函數是否存在於Lua環境中,若存在則直接調用執行,將結果返回

5、腳本管理命令的實現

除了Eval命令和EVALSHA命令外,Redis中與Lua腳本有關的命令還有4個,分別是SCRIPT FLUSH、SCRIPT EXISTS、SCRIPT LOAD、SCRIPT KILL,下面將對它們進行介紹

1)SCRIPT FLUSH

SCRIPT FLUSH命令用於清除服務器中所有和Lua腳本相關的信息,這個命令會釋放並重建lua_scripts字典,關閉現有的Lua環境並重新創建一個新的Lua環境

2)SCRIPT EXISTS

SCRIPT EXISTS命令根據輸入的SHA1校驗和,檢查校驗和對應的腳本是否存在於服務器中,即是否存在於lua_scripts字典中來實現的。存在用1表示,不存在用0表示(這裏只用到了lua_scripts字典的鍵,它的值實際上是爲了實現腳本複製功能而存在的,後續介紹)

3)SCRIPT LOAD

SCRIPT LOAD命令做的事和EVAL命令執行腳本的前兩步一樣,即在Lua環境中爲腳本創建對應的函數,再將腳本保存到lua_scripts字典中。完成這些後,客戶端可以使用EVALSHA命令來執行前面保存的腳本。

4)SCRIPT KILL

如果服務器設置了lua_time_limit配置選項,那麼每次執行Lua腳本前,服務器都會再Lua環境中設置一個超時處理鉤子。它會在腳本運行期間,檢查腳本的運行事件,一旦超出了lua_time_limit選項設置的時長,鉤子將定期再腳本運行的間隙,查看是否有SCRIPT KILL命令或SHUTDOWN命令到達服務器。

  • 如果超時處理的腳本未執行過寫操作,那麼客戶端可以通過SCRIPT KILL命令讓服務器停止執行該腳本,並返回一個錯誤回覆。處理完SCRIPT KILL命令後,服務器將繼續運行。
  • 如果超時處理的腳本執行過寫操作,那麼客戶端只能使用SHUTDOWN nosave命令來停止服務器,防止不合法的數據寫入數據庫。

6、腳本複製

與其他普通的Redis命令一樣,當服務器運行在複製模式下時,具有寫性質的腳本命令也會被複制到從服務器,這些命令包括EVAL命令、EVALSHA命令、SCRIPT FLUSH命令以及SCRIPT LOAD命令

1)複製EVAL命令、SCRIPT FLUSH命令以及SCRIPT LOAD命令

Redis複製複製EVAL命令、SCRIPT FLUSH命令以及SCRIPT LOAD命令的方法和複製其他普通Redis命令的方法一樣,當主服務器執行完以上三個命令中的一個時,主服務器架構直接將被執行的命令傳播給從服務器:

  • 對於EVAL命令來說,在主服務器執行的Lua腳本同樣會在從服務器中執行;
  • 對於SCRIPT FLUSH命令來說,從服務器接受到該命令後和主服務器一樣會重置Lua環境;
  • 對於SCRIPT LOAD命令來說,從服務器會載入和主服務器一樣的Lua腳本

2)複製EVALSHA命令

EVALSHA命令是所有與Lua腳本有關的命令中操作最複雜的,因爲主從服務器載入Lua腳本的情況可能有所差異,所以主服務器沒有像上述的三個命令一樣直接將EVALSHA命令傳播給從服務器。主服務上成功執行的EVALSHA命令在從服務執行時可能會出現腳本未找到的情況。另外不同的從服務器載入Lua腳本的情況也可能不同,因而不能直接傳播EVALSHA命令給從服務器。

爲了防止上述的情況,Redis要求主服務器在傳播EVALSHA命令時,必須要確保EVALSHA命令要執行的腳本已經被所有從服務器載入過,否則主服務器會將EVALSHA命令轉換成等價的EVAL命令,然後傳播EVAL命令來代替EVALSHA命令。傳播EVALSHA命令或將EVALHA命令轉換爲EVAL命令,都需要用到服務器狀態的lua_script字典和repl_scriptcache_dict字典

  1. 判斷傳播EVALSHA命令是否安全的辦法:主服務器使用服務器狀態的repl_scriptcache_dict字典記錄自己已經傳播給所有從服務器的腳本信息;repl_scriptcache_dict字典的鍵是Lua腳本的SHA1校驗和,值對應爲null:①當一個校驗和出現在repl_scriptcache_dict字典時,說明這個校驗和對應的Lua腳本已經傳播給了所有從服務器,主服務器可以直接向從服務器傳播EVALSHA命令,從服務器也可以避免找不到腳本的錯誤情況;②而如果一個腳本的SHA1校驗和存在於lua_scripts字典,但是不存在於repl_scriptcacha_dict字典,那麼說明校驗和對應的Lua腳本已經被主服務器載入,但是沒有傳播給所有從服務器,如果嘗試傳播這條EVALSHA命令,至少一個從服務器會出現腳本找不到的情況。

  2. 清空repl_scriptcacha_dict字典:每當主服務器添加一個從服務器,主服務器都會清空自己的repl_scriptcache_dict字典,這是因爲新的從服務器的出現,導致repl_scriptcacha_dict字典中記錄的信息將不再是被所有從服務器載入過的狀態。

  3. EVALSHA命令轉換爲EVAL命令的方法:通過EVALSHA命令指定的SHA1校驗和,以及lua_scripts字典保存的Lua腳本,服務器可以將一個EVALSHA命令EVALSHA <sha1> <numKeys> [key...] [arg...]轉換爲等價的EVAL命令EVAL <script> <numkeys> [key...] [arg...];步驟如下:①根據SHA1校驗和在lus_scripts字典中查找校驗和對應的Lua腳本②將原來的EVALSHA命令請求改寫成EVAL命令請求,並且將校驗和改成腳本script,其他numkeys、key、arg參數不變。轉換傳播給所有從服務器後,系統會將被傳播腳本的SHA1校驗和(原本的EVALSHA命令校驗和)添加到repl_scriptcache_dict字典中,後面再傳播時就不需要進行轉換。

  4. 傳播EVALSHA命令的方法:當主服務器執行完一個EVALSHA命令後,它將根據EVALSHA命令指定的校驗和來確認是否存在於repl_scriptcacha_dict字典中,若存在則傳播EVALSHA命令,不存在則轉換後傳播EVAL命令

四、排序

Redis的Sort命令可以對列表鍵、集合鍵、或者有序集合鍵進行排序

1、Sort 命令的實現

Sort命令最簡單的執行形式爲Sort ,它可以對包含數字值的key進行排序。如sort numbers命令的詳細執行步驟如下:

  1. 創建一個和numbers列表長度相同的數組,數組的每一項都是redis.h/redisSortObject結構;
  2. 遍歷數組,將各個數組項的obj指針指向numbers列表的各個項,構成一對一的關係;
  3. 遍歷數組,將各個obj指針指向的列表項準換爲一個double類型浮點數,並將這個浮點數保存在相應數組項的u.score屬性中;
  4. 根據數組項u.score屬性的值,對數組進行數字值排序,排序後的數組按照u.score屬性的值從小到大排列;
  5. 遍歷數組,將各個數組項的obj指針所指向的列表項作爲排序結果返回給客戶端,通過依次訪問索引返回u.score的值

SORT命令爲每個被排序鍵都創建一個與鍵長度相同的數組,數組的每個項都是一個redisSortObject結構,根據SORT命令使用的選項不同,程序使用redisSortObject結構的方式也有所不同

2、ALPHA選項的實現

通過使用ALPHA選項,SORT命令可以對包含字符串值的鍵進行排序:SORT <key> ALPHA。如執行Sort fruits ALPHA命令,步驟如下:

  1. 創建一個和fruits集合大小相同的redisSortObject結構數組;
  2. 遍歷數組,將各個數組項的obj指針指向fruits集合的各個元素;
  3. 根據obj指針所指向的集合元素,對數組進行字符串排序,排序後的數組按照集合元素字符串的值從小到大排列;
  4. 遍歷數組,將各個數組項的obj指針所指向的元素返回給客戶端;

3、ASC選項和DESC選項的實現

默認情況下,SORT命令執行升序排列,排列後的值按從小到大排序,所以命令Sort <key>Sort <key> ASC是等價的;而使用DESC後,排序會按值從大到小進行排列。

升序排列和降序排列都由相同的排序算法執行,不同之處在於對比函數產生的是升序對比結果還是降序對比結果

4、BY選項的實現

默認情況下,SORT命令使用被排序鍵包含的元素作爲排序的權重,元素本身決定了元素在排序後的位置。但通過BY選項,可以指定某些字符串鍵、或者哈希鍵所包含的某些域(field)來作爲元素的權重,對一個鍵進行排序。如下指令會根據*-price對應的權重進行排序

5、帶有ALPHA選項的BY選項的實現

BY選項默認假設權重鍵保存的值爲數字值,但如果權重鍵保存的是字符串值,那麼就需要使用By選項的同時,配合使用ALPHA選項。示例如下:

6、LIMIT選項的實現

在默認情況下,SORT命令會將排序後的所有元素返回給客戶端;但是通過LIMIT選項可以只返回一部分已排序的元素,命令格式如下:LIMIT <offset> <count>,offset參數表示要跳過已排序元素的數量,count參數表示跳過已排序數量後需要返回的已排序數量

7、GET選項的實現

在默認情況下,SORT命令在對鍵進行排序後,總是返回被排序鍵本身所包含的元素。通過Get選項,可以讓SORT命令對鍵進行排序後,根據被排序的元素,以及Get選項所指定的模式,查找並返回某些鍵的值,示例如下:

一個SORT命令可以帶有多個GET選項,所以隨着GET選項的增多,命令要執行的查找操作也會增多,示例如下:

8、STORE選項的實現

默認情況下,SORT命令只返回排序結果,而不保存排序結果;通過使用STORE選項,我們可以將排序結果保存在指定的鍵裏面,並在有需要時重用這個排序結果。

如命令SORT Students ALPHA STORE sorted_students的存儲步驟如下:通過SORT排序後,會首先檢查sorted_students這個鍵是否存在,若存在會刪除該鍵;刪除後將該鍵設置爲空白的列表鍵;再將排序後的元素依次插入到sorted_students列表的末尾

9、多個選項的執行順序

通常情況下,一個SORT命令請求通常會用到多個選項,而這些選項的執行順序是有先後之分的

1)選項的執行順序

按照選項劃分,執行過程可以分爲以下四步:

  1. 排序:命令使用ALPHA、ASC、DESC、BY選項,對輸入鍵進行排序,並得到一個排序結果集;
  2. 限制排序結果集長度:通過LIMIT選項,對排序結果集的長度進行限制,只有LIMIT選項指定的那部分元素會被保存到結果集中;
  3. 獲取外部鍵:命令使用GET選項及指定的模式,根據排序結果集中的元素,查找並獲取指定鍵的值,並將這些值作爲新的結果集;
  4. 保存結果集:命令使用STORE選項,將排序結果集保存到指定的鍵上去;
  5. 向客戶端返回排序結果集:遍歷結果集,依次向客戶端返回排序結果集中的元素;

2)選項的擺放順序

調用SORT命令時,除了GET選項外,改變選項的拜訪位置不會影響SORT命令執行這些選項的順序

五、二進制位數組

Redis提供了SETBIT、GETBIT、BITCOUNT、BITOP四個命令用於處理二進制數組:

  • SETBIT命令用於爲位數組指定偏移量上的二進制位設置值,位數組的偏移量從0開始計數,二進制的值可以是1或0;
  • GETBIT命令用於獲取位數組指定偏移量上的二進制位的值;
  • BITCOUNT命令用於統計數組裏面,值爲1的二進制位的數量;
  • BITTOP命令可以對多個位數組進行按位與、按位或、按位異或或按位取反操作;

1、位數組的表示

Redis使用字符串對象來表示位數組,因爲字符串對象使用的SDS數據結構是二進制安全,所以程序可以直接使用SDS結構來保存位數組,並使用SDS結構的操作函數來處理位數組。需要注意的是爲了簡化SETBIT命令的實現,系統使用逆序來保存位數組。示例如下:

2、GETBIT命令的實現

GETBIT命令用於返回位數組在offset偏移量上的二進制位的值;命令GETBIT <bitarray> <offset>的執行過程如下:

  1. 計算byte=offset除以8後向下取整,byte則可以記錄偏移量指定的二進制位保存在位數組的哪個字節;
  2. 計算bit=offset除以8的餘數+1,bit值記錄了offset偏移量指定的二進制位是byte字節的第幾個二進制位;
  3. 根據byte值和bit值,在位數組bitarray中定位offset偏移量指定的二進制位,並返回這個位的值

3、SETBIT命令的實現

SETBIT用於將位數組bitarray在offset偏移量上的二進制位的值設置位value,並向客戶端返回二進制位被設置前的舊值,步驟如下:

  1. 計算len=offset除以8向下取整後+1,len值記錄了保存offset偏移量指定的二進制位至少需要多少字節;
  2. 檢查bitarray鍵保存的位數組(SDS)的長度是否小於len,若小於則擴展長度至len,並將新擴展空間的二進制位的值設備爲0;
  3. 計算byte=offset除以8向下取整,byte可以記錄偏移量指定的二進制位保存在位數組的哪個字節;
  4. 計算bit=offset除以8的餘數+1,bit值記錄了offset偏移量指定的二進制位是byte字節的第幾個二進制位;
  5. 根據byte值和bit值,在位數組bitarray中定位offset偏移量指定的二進制位,首先將指定二進制位的值保存在oldValue變量,再將新增value設置爲這個二進制位的值;
  6. 向客戶端返回這個二進制位的值;

上面提到若位數組長度小於所需字節數會自動擴展,但SDS的空間預分配策略會額外分配空間;另外逆序保存數buf數組的優勢在這裏體現,寫入操作可以在新擴展的二進制位中完成,而不變改動位數組原有的二進制位,否則需要將位數組已有的位進行移動後才能執行寫操作,這會帶來一定的CPU消耗

4、BITCOUNT命令的實現

BITCOUNT命令用於統計給定數組中,值爲1的二進制位的數量。功能看上去很簡單,但要高效的實現這個命令需要用到一些算法

1)遍歷算法

遍歷算法最爲簡單直接,但每次循環只能檢查一個二進制位的值,若位數組長度過大的情況下重複執行二進制位的檢查顯然不是很好的方法,所以程序必須儘可能的增加每次檢查所能處理的二進制位的數量,減少執行次數

2)查表算法

對於一個有限集合或有限長度的位數組來說,其排序方式是有限的,只需要列出所有可能的二進制位排序就可以知道當前二進制位數組包含的1的個數。對於8位長的位數組,一次表查找就可以檢查8個二進制位,效率比遍歷算法提升了8倍。但這種做法是典型的空間換時間,且會收到內存消耗和CPU緩存的影響

3)variable-precision SWAR算法

統計一個位數組中非0二進制位的數量,在數學上被稱爲Hamming Weight,一些處理器直接帶有計算漢明重量的指令,對於不具備特殊指令的普通處理器,目前已知效率最好的算法是variable-precision SWAR算法,它通過一系列位移和位運算操作,可以在常數時間內計算多個字節的漢明重量,並且不需要額外的內存。實現如下:

swar函數每次執行可以計算32個二進制位的漢明重量,比遍歷算法快32倍;此外它是一個常數複雜度的操作,所以我們可以按照自己的需要,在一次循環中多次執行swar,從而按倍提升效率,如一次循環調用2次swar算法會再次提升2倍效率,但是一旦處理位數組的大小超過了緩存的大小,優化效果就會消失。

4)Redis的實現

BITCOUNT命令的實現用到了表查找和Swar查找算法:

  • 表查找算法使用鍵長爲8位的表,表中記錄了從0000 0000到1111 1111在內的所有二進制位的漢明重量;
  • Swar算法,BITCOUNT命令在每次循環中載入128個二進制位,然後調用4次32位的Swar算法來計算128個二進制位的漢明重量;

在執行BITCOUNT命令時,程序會根據未處理的二進制位數量來決定使用哪種算法;如果未處理的二進制位的數量大於等於128位,程序會使用Swar算法,否則使用表查找算法。具體邏輯如下,BITCOUNT實現的算法複雜度爲O(n),n爲輸入二進制位的數量

5、BITOP命令的實現

因爲C語言支持邏輯與&、邏輯或|、邏輯異或^和邏輯非~的操作,所以BITOP命令的AND、OR、XOR和NOT操作都是直接基於這裏邏輯操作實現的。因爲BITOP OR、BITOP XOR、BITOP XOR三個命令可以接受多個位數組輸入,所以程序需要遍歷輸入的每個位數組的每個字節進行計算,所以命令的複雜度位O(n^2);而BITOP NOT命令只接受一個位數組輸入,所以它的複雜度位O(n)

六、慢日誌查詢

Redis的慢日誌查詢功能用於記錄執行時間超過給定時長的命令請求,用戶可以通過這個功能產生的日誌來監視和優化查詢速度。服務器配置有兩個與慢日誌查詢相關的選項:

  • slowlog-log-slower-than選項指定執行時間超過多少微秒;通過Config set slowlog-log-slower-than xxx設定;
  • slowlog-max-len選項指定服務器最多保存多少條慢查詢日誌;通過Config set slowlog-max-len xxx設定;當超過設定的數量後會採用先進先出原則刪除舊日誌

可以使用SlowLog Get命令查看服務器保存的慢查詢日誌

1、慢查詢記錄的保存

服務器狀態中包含了幾個和慢日誌日誌功能有關的屬性:

  • slowlog_entry_id屬性初始值爲0,每創建一條慢日誌,這個屬性的值就會用作新日誌的Id,之後程序會對這個屬性的值增1;
  • slowlog鏈表保存了服務器中所有的慢查詢日誌,鏈表中的每個節點都保存了一個slowlogEntry結構,該結構代表一條慢查詢日誌;

2、慢查詢日誌的閱覽和刪除

  • 查看慢查詢日誌的ShowLog Get命令定義如下:

  • 查看慢查詢日誌數量的ShowLog Len命令定義如下:

  • 清楚所有慢查詢日誌的ShowLog Reset命令定義如下:

3、添加新日誌

每次執行命令之前或之後,程序都以微妙格式記錄UNIX時間戳,兩個時間戳的相隔時間就是執行命令耗費的時長,服務器會將這個時長作爲參數之一傳給slowLogPushEntryIfNeeded函數,該函數複製檢查是否需要爲這次執行的命令創建慢查詢日誌。該函數會確認是否創建慢日誌,若創建會將新的日誌插入到slowLog鏈表的表頭;它還會檢查日誌的長度是否超過slowlog_max_len選項所設置的長度,如超出則將多餘的日誌從slowLog鏈表中刪除

七、監視器

通過Monitor命令,客戶端可以將自己變爲一個監視器,實時的接受並打印出服務器當前處理的命令請求的相關信息。每當一個客戶端向服務器發送一條命令請求,服務器處理處理這條命令外,還會將這條命令請求的信息發送給所有的監視器

1、成爲監視器

客戶端發送Monitor命令後,會將自身的Redis_Monitor標誌打開,並且這個客戶端會被添加到monitors鏈表的表尾;

2、向監視器發送命令請求

服務器每次處理命令請求之前,都會調用replicationFeedMonitors函數,由這個函數將被處理命令的信息發送給所有的監視器

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章