redis底層設計(四)——功能的實現

redis中咱們會常常用到事務、訂閱與發佈、Lua腳本以及慢查詢日誌,接下來咱們就一一對他們進行探討學習。redis

4.1事務數據庫

  redis經過MULTI、DISCARD、EXEC和WATCH四個命令來實現事務功能。json

  4.1.1 事務數組

    事務提供了一種「將多個命令打包,一次性按順序地執行」的機制,而且事務在執行的期間不會主動中斷——服務器在執行完全部的命令以後,纔會繼續處理其餘客戶端的其餘命令。以下: 安全

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
redis> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"
2) "C++"
3) "Programming"

  一個事務從開始到執行會經歷3個過程:服務器

    1)開始事務網絡

    2)命令入隊數據結構

    3)執行事務dom

  下面分別介紹這三個過程:async

  4.1.2 開始事務

  MULTI命令標誌着事務的開始。該命令的做用是將客戶端的REDIS_MULTI選項打開,讓客戶端從非事務狀態切換到事務狀態。

  4.1.3 命令入隊

    當客戶端進入事務狀態時,服務器在收到來自客戶端的命令時,不會當即執行,而是將這些命令所有放進QUEUE事務隊列中,而後返回QUEUE,表示命令已入隊。

    事務隊列是一個數組,每一個數組包含3個屬性:

      1)要執行的命令(cmd);

      2)命令的參數(argv);

      3)參數的個數(argc);

  好比:

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

  那麼程序將爲客戶端建立如下事務隊列:

 

  4.1.4 執行事務

    當客戶端正處在事務狀態,若接收到EXEC、DISCARD、MULTI和WATCH這四個命令時仍是會直接執行,而其餘的命令是會被放到QUEUE隊列中去的。服務器會以先進先出(FIFO)的方式執行,將執行命令獲得的結果以FIFO的方式存放到一個回覆隊列中

  4.1.5 事務狀態下的DISCARD、MULTI和WATCH命令

    DISCARD 命令用於取消一個事務,它清空客戶端的整個事務隊列,而後將客戶端從事務狀態調整回非事務狀態,最後返回字符串OK 給客戶端,說明事務已被取消。
    Redis 的事務是不可嵌套的,當客戶端已經處於事務狀態,而客戶端又再向服務器發送MULTI時,服務器只是簡單地向客戶端發送一個錯誤,而後繼續等待其餘命令的入隊。MULTI 命令的發送不會形成整個事務失敗,也不會修改事務隊列中已有的數據。
    WATCH 只能在客戶端進入事務狀態以前執行,在事務狀態下發送WATCH 命令會引起一個錯誤,但它不會形成整個事務失敗,也不會修改事務隊列中已有的數據(和前面處理MULTI的狀況同樣)。

  4.1.6 帶WATCH的事務

    WATCH 命令用於在事務開始以前監視任意數量的鍵:當調用EXEC 命令執行事務時,若是任意一個被監視的鍵已經被其餘客戶端修改了,那麼整個事務再也不執行,直接返回失敗。

     以下:

  

  在時間T4 ,客戶端B 修改了name 鍵的值,當客戶端A 在T5 執行EXEC 時,Redis 會發現name 這個被監視的鍵已經被修改,所以客戶端A 的事務不會被執行,而是直接返回失敗。

  4.1.7 WATCH命令的實現

    在每一個表明數據庫的redis.h/redisDb結構類型中,都保存了一個watch_keys字典,字典的鍵是這個數據庫被監視的鍵,而字典的值是一個鏈表,鏈表中保存着全部監視這個鍵的客戶端。以下:

  

    其中,key1正在被client二、client五、client1三個客戶端監視,其餘鍵也在被其餘的客戶端監視着。而WATCH的做用就是將客戶端和要監視的鍵進行關聯。

  4.1.8 WATCH的觸發

    在任何對數據庫鍵空間(key space)進行修改的命令成功執行以後(好比FLUSHDB 、SET、DEL 、LPUSH 、SADD 、ZREM ,諸如此類),multi.c/touchWatchKey 函數都會被調用——它檢查數據庫的watched_keys 字典,看是否有客戶端在監視已經被命令修改的鍵,若是有的話,程序將全部監視這個/這些被修改鍵的客戶端的REDIS_DIRTY_CAS 選項打開   

    當客戶端發送EXEC 命令、觸發事務執行時,服務器會對客戶端的狀態進行檢查:
      • 若是客戶端的REDIS_DIRTY_CAS 選項已經被打開,那麼說明被客戶端監視的鍵至少有一個已經被修改了,事務的安全性已經被破壞。服務器會放棄執行這個事務,直接向客戶端返回空回覆,表示事務執行失敗。
      • 若是REDIS_DIRTY_CAS 選項沒有被打開,那麼說明全部監視鍵都安全,服務器正式執行事務。

  4.1.9 事務的ACID性質

    redis保證了事務的一致性(C)和隔離性(I),但並不保證原子性(A)和持久性(D)。

    1)原子性(A):

    單個redis命令的執行是原子性的,但redis沒有在事務上增長任何維持原子性的機制,因此redis事務的執行並非原子性的。

    若是redis服務器進程在執行任務的過程當中被中止——好比接到KILL命令、宿主機器停機等等,那麼事務執行失敗,當事務執行失敗時,redis是不會作任何的重試或回滾的。

    2)一致性(C):

    redis的一致性能夠從下面三個方面來討論:入隊錯誤、執行錯誤和redis進程被終結。

      a.入隊錯誤:

        在命令入隊的過程當中,若是客戶端想服務器發送了錯誤的命令,好比命令的參數數量錯誤等等,那麼服務器將想客戶端返回一個出錯的信息,而且將客戶端的事務狀態設爲REDIS_DIRTY_EXEC。當客戶端執行EXEC命令的時候,redis會拒絕執行   REDIS_DIRTY_EXEC的事務,並返回失敗信息。所以,帶有不正確的入隊命令的事務不會被執行,也不會影響數據庫的一致性。

      b.執行錯誤:

        若是命令在事務執行的過程當中發生錯誤,好比說,對一個不一樣類型的key 執行了錯誤的操做,那麼Redis 只會將錯誤包含在事務的結果中,這不會引發事務中斷或整個失敗,不會影響已執行事務命令的結果,也不會影響後面要執行的事務命令,因此它對事務的一致性也沒有影響。

      c.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 工具將部分紅功的事務命令移除以後,才能再次啓動服務器。還原以後的數據老是一致的,並且數據也是最新的(直到事務執行以前爲止)。

    3)隔離性(I):

    redis是但進程程序,而且它保證在執行事務時,不會對事務進行中斷,事務能夠運行直到執行完全部事務隊列中的命令爲止。所以,redis的事務老是帶有隔離性的。

    4)持久性(D):

    由於事務不過是用隊列包裹起了一組Redis 命令,並無提供任何額外的持久性功能,因此事務的持久性由Redis 所使用的持久化模式決定:
      • 在單純的內存模式下,事務確定是不持久的。
      • 在RDB 模式下,服務器可能在事務執行以後、RDB 文件更新以前的這段時間失敗,因此RDB 模式下的Redis 事務也是不持久的。
      • 在AOF 的「老是SYNC 」模式下,事務的每條命令在執行成功以後,都會當即調用fsync或fdatasync 將事務數據寫入到AOF 文件。可是,這種保存是由後臺線程進行的,主線程不會阻塞直到保存成功,因此從命令執行成功到數據保存到硬盤之間,仍是有一段
    很是小的間隔,因此這種模式下的事務也是不持久的。其餘AOF 模式也和「老是SYNC 」模式相似,因此它們都是不持久的。

  4.1.10 小結:

    * 事務提供了一種將多個命令打包,而後一次性、有序地執行的機制;

    * 事務在執行過程當中不會被打斷,全部事務命令執行完以後,事務纔會結束;

    * 多個命令會被入隊到事務隊列中,而後按先進先出(FIFO)的順序執行;

    * 帶WATCH命令的事務會在數據庫的WATCHED_KEYS字典中將客戶端和被監視的鍵進行關聯;當鍵被修改時,程序會將全部監視被修改鍵的客戶端的REDIS_DIRTY_CAS選項打開;

    * 只有在客戶端的REDIS_DIRTY_CAS選項未被打開時,才能執行事務,不然事務直接返回失敗;

    * redis保持了事務的一致性和隔離性,但並不保證事務的原子性和持久性;

 

  4.2 訂閱與發佈

    Redis 經過PUBLISH 、SUBSCRIBE 等命令實現了訂閱與發佈模式,這個功能提供兩種信息機制,分別是訂閱/發佈到頻道和訂閱/發佈到模式,下文先討論訂閱/發佈到頻道的實現,再討論訂閱/發佈到模式的實現。

    4.2.1 頻道的訂閱

      每一個Redis 服務器進程都維持着一個表示服務器狀態的redis.h/redisServer 結構,結構的pubsub_channels 屬性是一個字典,這個字典就用於保存訂閱頻道的信息:

struct redisServer {
// ...
dict *pubsub_channels;
// ...
};

      其中,字典的鍵爲正在被訂閱的頻道,而字典的值則是一個鏈表,鏈表中保存了全部訂閱這個頻道的客戶端。

      經過pubsub_channels 字典,程序只要檢查某個頻道是否字典的鍵,就能夠知道該頻道是否正在被客戶端訂閱;只要取出某個鍵的值,就能夠獲得全部訂閱該頻道的客戶端的信息。

     4.2.2 發送信息到頻道

      瞭解了pubsub_channels 字典的結構以後,解釋PUBLISH 命令的實現就很是簡單了:當調用PUBLISH channel message 命令,程序首先根據channel 定位到字典的鍵,而後將信息發送給字典值鏈表中的全部客戶端。

    4.2.3 退訂頻道

      使用UNSUBSCRIBE 命令能夠退訂指定的頻道,這個命令執行的是訂閱的反操做:它從pubsub_channels 字典的給定頻道(鍵)中,刪除關於當前客戶端的信息,這樣被退訂頻道的信息就不會再發送給這個客戶端。

    4.2.4 模式的訂閱與信息發送

      當使用PUBLISH 命令發送信息到某個頻道時,不只全部訂閱該頻道的客戶端會收到信息,若是有某個/某些模式和這個頻道匹配的話,那麼全部訂閱這個/這些頻道的客戶端也一樣會收到信息。

  

    上圖展現了一個帶有頻道和模式的例子,當有信息發送到tweet.shop.kindle 頻道時,信息除了發送給clientX 和clientY 以外,還會發送給訂閱tweet.shop.* 模式的client123 和client256。

    2.4.5 訂閱模式

      redisServer.pubsub_patterns 屬性是一個鏈表,鏈表中保存着全部和模式相關的信息:

struct redisServer {
// ...
list *pubsub_patterns;
// ...
};

      鏈表中的每一個節點都包含一個redis.h/pubsubPattern 結構:

typedef struct pubsubPattern {
redisClient *client;
robj *pattern;
} pubsubPattern;

      client 屬性保存着訂閱模式的客戶端,而pattern 屬性則保存着被訂閱的模式。每當調用PSUBSCRIBE 命令訂閱一個模式時,程序就建立一個包含客戶端信息和被訂閱模式的pubsubPattern 結構,並將該結構添加到redisServer.pubsub_patterns 鏈表中。

      做爲例子,下圖展現了一個包含兩個模式的pubsub_patterns 鏈表,其中client123 和client256 都正在訂閱tweet.shop.* 模式:

      經過遍歷整個pubsub_patterns 鏈表,程序能夠檢查全部正在被訂閱的模式,以及訂閱這些模式的客戶端。

     2.4.6 發送信息到模式

      發送信息到模塊的工做是由PUBLISH命令完成的,PUBLISH除了將message發送到全部訂閱channel的客戶端上,他還會將channel和pubsub_patterns中的模式進行對比,若是channel和某種模式匹配的話,那麼也將message發送到訂閱那個模式的客戶端。

      例如:redis服務器的pubsub_patterns狀態以下:

      那麼當某個客戶端發送信息"Amazon Kindle, $69." 到tweet.shop.kindle 頻道時,除了全部訂閱了tweet.shop.kindle 頻道的客戶端會收到信息以外,客戶端client123 和client256也一樣會收到信息,由於這兩個客戶端訂閱的tweet.shop.* 模式和tweet.shop.kindle     頻道匹配。

     4.2.7 退訂模式

      使用PUNSUBSCRIBE 命令能夠退訂指定的模式,這個命令執行的是訂閱模式的反操做:程序會刪除redisServer.pubsub_patterns 鏈表中,全部和被退訂模式相關聯的pubsubPattern結構,這樣客戶端就不會再收到和模式相匹配的頻道發來的信息。

    4.2.8 小結

      * 訂閱消息由服務器進程維持的RedisServer.pubsub_channels字典保存,字典的鍵是被訂閱的頻道,字典的值是訂閱該頻道的全部客戶端;

      * 當有新消息發送到頻道時,程序遍歷頻道(鍵)所對應的客戶端(值),而後將信息發送到全部訂閱頻道的客戶端上;

      * 訂閱模式的信息由服務器進行維持的RedisServer_pubsub_patterns鏈表保存,鏈表的每一個節點都保存着一個pubsubPattern 結構,結構中保存着被訂閱的模式,以及訂閱模式的客戶端。程序經過遍歷鏈表來查詢某個頻道是否和某個模式匹配;

      * 當有新消息發送到頻道時,除了訂閱頻道的客戶端會接收到消息以外,全部與頻道模式相匹配的客戶端也會收到消息;

      * 退訂頻道和退訂模式都分別是訂閱頻道和訂閱模式的反操做。  

 

     4.3Lua腳本

      Lua是redis 2.6 版本最大的亮點,經過內嵌對Lua 環境的支持,Redis 解決了長久以來不能高效地處理CAS (check-and-set)命令的缺點,而且能夠經過組合使用多個命令,輕鬆實現之前很難實現或者不能高效實現的模式。

       4.3.1 初始化Lua環境

        在初始化redis服務器的時候,對Lua環境的初始化也會一併進行;  

         整個初始化Lua環境的步驟以下:

        1)調用lua_open函數,建立一個新的Lua環境;

        2)載入指定的Lua函數庫:基礎庫(base lib)、表格庫(table lib)、字符串庫(string lib)、數學庫(math lib)、調試庫(debug lib)、用於處理JSON對象的cjson庫、在Lua值和C結構之間進行切換的struct庫和處理MessagePack數據的cmsgpack庫

        3)屏蔽一些可能對Lua環境產生安全問題的函數,好比loadfile;

        4)建立一個redis字典,保存Lua腳本,並在複製腳本是使用,字典的鍵爲SHA1校驗和,字典的值爲Lua腳本;

        5)建立一個redis全局表格到Lua環境,表格中包含了各類對redis進行操做的函數;

        6)用redis本身定義的隨機生成函數,替換math表原有的math.random函數和math.randomseed函數,新的函數具備這樣的特質:每次執行Lua 腳本時,除非顯式地調用math.randomseed ,不然math.random 生成的僞隨機數序列老是相同的;

        7)建立一個對Redis 多批量回復(multi bulk reply)進行排序的輔助函數;

        8)對Lua 環境中的全局變量進行保護,以避免被傳入的腳本修改;

        9)由於Redis 命令必須經過客戶端來執行,因此須要在服務器狀態中建立一個無網絡鏈接的僞客戶端(fake client),專門用於執行Lua 腳本中包含的Redis 命令:當Lua 腳本須要執行Redis 命令時,它經過僞客戶端來向服務器發送命令請求,服務器在執行完命令以後,將結果返回給僞客戶端,而僞客戶端又轉而將命令結果返回給Lua 腳本;

        10)將Lua 環境的指針記錄到Redis 服務器的全局狀態中,等候Redis 的調用;

        以上就是Redis 初始化Lua 環境的整個過程,當這些步驟都執行完以後,Redis 就可使用Lua 環境來處理腳本了。嚴格來講,步驟1 至8 纔是初始化Lua 環境的操做,而步驟9 和10 則是將Lua 環境關聯到服務器的操做,爲了按順序觀察整個初始化過程,咱們將兩種操做放在了一塊兒。另外,步驟6 用於建立無反作用的腳本,而步驟7 則用於去除部分Redis 命令中的不肯定性(non deterministic),關於這兩點,請看下面一節關於腳本安全性的討論。

    4.3.2 腳本的安全性

      當將Lua 腳本複製到附屬節點,或者將Lua 腳本寫入AOF 文件時,Redis 須要解決這樣一個問題:若是一段Lua 腳本帶有隨機性質或反作用,當這段腳本在附屬節點運行時,或從AOF 文件載入從新運行時,它獲得的結果可能和以前運行的結果徹底不一樣。

      注意:只有在帶有隨機性的腳本進行寫入時,隨機性纔是有害的,若是一個腳本只是執行只讀操做,那麼隨機性是無害的

      和隨機性質相似,若是一個腳本的執行對任何反作用產生了依賴,那麼這個腳本每次執行的結果均可能會不同。爲了解決這個問題,Redis 對Lua 環境所能執行的腳本作了一個嚴格的限制——全部腳本都必須是無反作用的純函數(pure function)。

      爲此,Redis 對Lua 環境作了一些列相應的措施:
        • 不提供訪問系統狀態狀態的庫(好比系統時間庫)。
        • 禁止使用loadfile 函數。
        • 若是腳本在執行帶有隨機性質的命令(好比RANDOMKEY ),或者帶有反作用的命令(好比TIME )以後,試圖執行一個寫入命令(好比SET ),那麼Redis 將阻止這個腳本繼續運行,並返回一個錯誤。
        • 若是腳本執行了帶有隨機性質的讀命令(好比SMEMBERS ),那麼在腳本的輸出返回給Redis 以前,會先被執行一個自動的字典序排序,從而確保輸出結果是有序的。
        • 用Redis 本身定義的隨機生成函數,替換Lua 環境中math 表原有的math.random 函數地調用math.randomseed ,不然math.random 生成的僞隨機數序列老是相同的。
      通過這一系列的調整以後,Redis 能夠保證被執行的腳本:

        *  無反作用。
        *  沒有有害的隨機性。
        *  對於一樣的輸入參數和數據集,老是產生相同的寫入命令。

    4.3.3 腳本的執行

      在腳本環境的初始化工做完成之後,Redis 就能夠經過EVAL 命令或EVALSHA 命令執行Lua腳本了。

redis> EVAL "return 'hello world'" 0
"hello world"
redis> SCRIPT LOAD "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de910 // 上一個腳本的校驗和
"hello world"

    4.3.4 EVAL命令的實現

      EVAL 命令的執行能夠分爲如下步驟:
        1) 爲輸入腳本定義一個Lua 函數。
        2) 執行這個Lua 函數。

      定義Lua函數:

        全部被Redis 執行的Lua 腳本,在Lua 環境中都會有一個和該腳本相對應的無參數函數:當調用EVAL 命令執行腳本時,程序第一步要完成的工做就是爲傳入的腳本建立一個相應的Lua函數。
        舉個例子,當執行命令EVAL "return 'hello world'" 0 時,Lua 會爲腳本"return 'hello world'" 建立如下函數:

function f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91()
return 'hello world'
end     

        其中,函數名以f_ 爲前綴,後跟腳本的SHA1 校驗和(一個40 個字符長的字符串)拼接而成。而函數體(body)則是用戶輸入的腳本。以函數爲單位保存Lua 腳本有如下好處:
          • 執行腳本的步驟很是簡單,只要調用和腳本相對應的函數便可。
          • Lua 環境能夠保持清潔,已有的腳本和新加入的腳本不會互相干擾,也能夠將重置Lua環境和調用Lua GC 的次數降到最低。
          • 若是某個腳本所對應的函數在Lua 環境中被定義過至少一次,那麼只要記得這個腳本的SHA1 校驗和,就能夠直接執行該腳本——這是實現EVALSHA 命令的基礎,稍後在介紹EVALSHA 的時候就會說到這一點。
        在爲腳本建立函數前,程序會先用函數名檢查Lua 環境,只有在函數定義未存在時,程序才建立函數。重複定義函數通常並無什麼反作用,這算是一個小優化。

        另外,若是定義的函數在編譯過程當中出錯(好比,腳本的代碼語法有錯),那麼程序向用戶返回一個腳本錯誤,再也不執行後面的步驟。

      執行Lua函數:

        在定義好Lua 函數以後,程序就能夠經過運行這個函數來達到運行輸入腳本的目的了。不過,在此以前,爲了確保腳本的正確和安全執行,還須要執行一些設置鉤子、傳入參數之類的操做,整個執行函數的過程以下:
          1. 將EVAL 命令中輸入的KEYS 參數和ARGV 參數以全局數組的方式傳入到Lua 環境中。
          2. 設置僞客戶端的目標數據庫爲調用者客戶端的目標數據庫: fake_client->db =caller_client->db ,確保腳本中執行的Redis 命令訪問的是正確的數據庫。
          3. 爲Lua 環境裝載超時鉤子,保證在腳本執行出現超時時能夠殺死腳本,或者中止Redis服務器。
          4. 執行腳本對應的Lua 函數。
          5. 若是被執行的Lua 腳本中帶有SELECT 命令,那麼在腳本執行完畢以後,僞客戶端中的數據庫可能已經有所改變,因此須要對調用者客戶端的目標數據庫進行更新:caller_client->db = fake_client->db 。
          6. 執行清理操做:清除鉤子;清除指向調用者客戶端的指針;等等。
          7. 將Lua 函數執行所得的結果轉換成Redis 回覆,而後傳給調用者客戶端。
          8. 對Lua 環境進行一次單步的漸進式GC 。

        如下是執行EVAL "return 'hello world'" 0 的過程當中,調用者客戶端(caller)、Redis 服務器和Lua 環境之間的數據流表示圖:

      

    4.3.5 小結

      初始化Lua腳本環境須要一系列步驟,其中包括:

        * 建立Lua環境;

        * 載入Lua庫,好比字符串庫、數學庫、表格庫、調試庫等;

        * 建立redis全局表格,包含各類對redis進行操做的函數,好比redis.call 和 redis.log等;

        * 建立一個無網絡的僞客戶端,專門用於執行Lua腳本中的redis命令;

      redis經過一系列的措施保證被執行的Lua腳本無反作用,也沒有有害的寫隨機性:對於一樣的輸入參數和數據集,老是產生相同的寫入命令。

      EVAL命令爲輸入腳本定義一個Lua函數,而後經過執行這個函數來執行腳本。

      EVALSHA經過構建函數名,直接調用Lua中已定義的函數,從而執行相應的腳本。

  

   4.4 慢查詢日誌

    慢查詢日誌是redis提供的一個用於觀察系統性能的功能。一句話就是將那些執行時間比較長的命令記錄到日誌中。

     4.4.1 相關數據結構:

      每條慢查詢日誌都以一個slowlog.h/slowlogEntry結構定義:

typedef struct slowlogEntry {
// 命令參數
robj **argv;
// 命令參數數量
int argc;
// 惟一標識符
long long id; /* Unique entry identifier. */
// 執行命令消耗的時間,以納秒(1 / 1,000,000,000 秒)爲單位
long long duration; /* Time spent by the query, in nanoseconds. */
// 命令執行時的時間
time_t time; /* Unix time at which the query was executed. */
} slowlogEntry;

      記錄服務器狀態的redis.h/redisServer 結構裏保存了幾個和慢查詢有關的屬性:

struct redisServer {
// ... other fields
// 保存慢查詢日誌的鏈表
list *slowlog; /* SLOWLOG list of commands */
// 慢查詢日誌的當前id 值
long long slowlog_entry_id; /* SLOWLOG current entry ID */
// 慢查詢時間限制
long long slowlog_log_slower_than; /* SLOWLOG time limit (to get logged) */
// 慢查詢日誌的最大條目數量
unsigned long slowlog_max_len; /* SLOWLOG max number of items logged */
// ... other fields
};

      

      slowlog 屬性是一個鏈表,鏈表裏的每一個節點保存了一個慢查詢日誌結構,全部日誌按添加時間重新到舊排序,新的日誌在鏈表的左端,舊的日誌在鏈表的右端。

      slowlog_entry_id 在建立每條新的慢查詢日誌時增一,用於產生慢查詢日誌的ID (這個ID在執行SLOWLOG RESET 以後會被重置)。slowlog_log_slower_than 是用戶指定的命令執行時間上限,執行時間大於等於這個值的命令會被慢查詢日誌記錄。
      slowlog_max_len 慢查詢日誌的最大數量,當日志數量等於這個值時,添加一條新日誌會形成最舊的一條日誌被刪除。
      下圖展現了一個slowlog 屬性的實例:

      

    4.4.2 慢查詢日誌的記錄:

      在每次執行命令以前,Redis 都會用一個參數記錄命令執行前的時間,在命令執行完以後,再計算一次當前時間,而後將兩個時間值相減,得出執行命令所耗費的時間值duration ,並將duration 傳給slowlogPushEntryIfNeed 函數。
      若是duration 超過服務器設置的執行時間上限server.slowlog_log_slower_than 的話,slowlogPushEntryIfNeed 就會建立一條新的慢查詢日誌,並將它加入到慢查詢日誌鏈表裏。

    4.4.3 慢查詢日誌的操做:

      針對慢查詢日誌有三種操做,分別是查看、清空和獲取日誌數量:
        • 查看日誌:在日誌鏈表中遍歷指定數量的日誌節點,複雜度爲O(N) 。命令是:slowlog get
        • 清空日誌:釋放日誌鏈表中的全部日誌節點,複雜度爲O(N) 。命令是:slowlog reset
        • 獲取日誌數量:獲取日誌的數量等同於獲取server.slowlog 鏈表的數量,複雜度爲O(1) 。 命令是:slowlog len

     4.4.4 小結:

      * redis用一個鏈表以FIFO的順序保存着全部慢查詢日誌;

      * 每條慢查詢日誌以一個慢查詢節點表示,節點中記錄着執行超時的命令、命令的參數、命令執行時的時間,以及執行命令所消耗的時間等信息。   

相關文章
相關標籤/搜索