天天看點

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

本文已收錄到1.1K Star數開源學習指南——《大廠面試指北》,如果想要了解更多大廠面試相關的内容及擷取《大廠面試指北》離線PDF版,請掃描下方二維碼碼關注公衆号“大廠面試”,謝謝大家了!

《大廠面試指北》最佳閱讀位址:

http://notfound9.github.io/interviewGuide/

《大廠面試指北》項目位址:

https://github.com/NotFound9/interviewGuide

擷取《大廠面試指北》離線PDF版,請掃描下方二維碼關注公衆号“大廠面試”

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

《大廠面試指北》項目截圖:

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

第15章 複制

在Redis中,可以通過執行SLAVEOF指令或者設定slaveof選項,讓從伺服器來備份主伺服器上的資料。

Redis的複制功能主要分為同步和指令傳播。

同步主要是指從伺服器的狀态更新為主伺服器的狀态。同步具體又細分為完整重同步和部分重同步(Redis 2.8以後的版本才有)。

#####完整重同步

一般是從伺服器向主伺服器發送指令請求,主服務向從伺服器發送RDB檔案及緩沖區儲存的寫指令,從伺服器根據RDB檔案和緩沖區儲存的寫指令恢複資料庫狀态。

具體步驟如下:

1)從伺服器向主伺服器發送SYNC指令。

2)收到SYNC指令的主伺服器執行BGSAVE指令,在背景生成一個RDB檔案,并使用一個緩沖區記錄從現在開始執行的所有寫指令。

3)當主伺服器的BGSAVE指令執行完畢時,主伺服器會将BGSAVE指令生成的RDB檔案發送給從伺服器,從伺服器接收并載入這個RDB檔案,将自己的資料庫狀态更新至主伺服器執行BGSAVE指令時的資料庫狀态。

4)主伺服器将記錄在緩沖區裡面的所有寫指令發送給從伺服器,從伺服器執行這些寫指令,将自己的資料庫狀态更新至主伺服器資料庫目前所處的狀态。

如下圖所示:

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

#####指令傳播

在同步操作執行完畢之後的這個時間點,主從伺服器兩者的資料庫将達到一緻狀态。但之後當主伺服器執行用戶端發送的寫指令時,主伺服器的資料庫就有可能會被修改,并導緻主從伺服器狀态不再一緻,是以指令傳播就是主伺服器執行一條寫指令後,也會把這條寫指令發送給從伺服器執行。

#####部分重同步(Redis2.8以後版本才有)

對于從伺服器初次複制的場景來說,完整重同步+指令傳播已經可以完美得滿足需求了,但是如果從伺服器是斷線後重複制,因為從伺服器本身擁有主伺服器的資料,隻是缺少斷線期間的資料修改,采用完整重同步+指令傳播會效率比較低。是以就有了部分重同步隻會向從伺服器發送斷線期間的寫指令。

部分重同步的實作由三部分組成,複制偏移量,複制積壓緩沖區,伺服器運作ID。

複制偏移量

指的是執行複制的雙方——主伺服器和從伺服器會分别維護一個複制偏移量:

·主伺服器每次向從伺服器傳播N個位元組的資料時,就将自己的複制偏移量的值加上N。

·從伺服器每次收到主伺服器傳播來的N個位元組的資料時,就将自己的複制偏移量的值加上N。

這樣通過對比主從伺服器的複制偏移量可以知道主從伺服器目前的資料狀态。

複制積壓緩沖區

複制積壓緩沖區是由主伺服器維護的一個固定長度先進先出隊列,儲存了主伺服器執行的寫指令。預設大小為1MB。隊列長度是固定的,當元素滿了時,會将最先入隊的元素彈出,再将新元素放入隊列。

伺服器運作ID

每個Redis伺服器,不論主伺服器還是從服務,都會有自己的運作ID。當從伺服器對主伺服器進行初次複制時,主伺服器會将自己的運作ID傳送給從伺服器,而從伺服器則會将這個運作ID儲存起來。

當從伺服器斷線并重新連上一個主伺服器時,從伺服器将向目前連接配接的主伺服器發送之前儲存的運作ID:

·如果從伺服器儲存的運作ID和目前連接配接的主伺服器的運作ID相同,那麼說明從伺服器斷線之前複制的就是目前連接配接的這個主伺服器,主伺服器可以繼續嘗試執行部分重同步操作。

·相反地,如果從伺服器儲存的運作ID和目前連接配接的主伺服器的運作ID并不相同,那麼說明從伺服器斷線之前複制的主服務“·相反地,如果從伺服器儲存的運作ID和目前連接配接的主伺服器的運作ID并不相同,那麼說明從伺服器斷線之前複制的主伺服器并不是目前連接配接的這個主伺服器,主伺服器将對從伺服器執行完整重同步操作。

PSYNC指令的實作

PSYNC有兩種模式,可以進行完整重同步和部分重同步。從伺服器在開始一次新的複制時将向主伺服器發送PSYNC ? -1指令,主動請求主伺服器進行完整重同步。如果從伺服器已經複制過某個主伺服器,那麼從伺服器在開始一次新的複制時将向主伺服器發送PSYNC 指令:其中runid是上一次複制的主伺服器的運作ID,而offset則是從伺服器目前的複制偏移量,接收到這個指令的主伺服器會通過這兩個參數來判斷應該對從伺服器執行哪種同步操作。具體流程圖如下:

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)
複制的實作

向從服務發送SLAVEOF指令,可以讓一個從伺服器複制主服務。

例如:向從伺服器發送下面的指令,複制位址為127.0.0.1,端口為6379的主伺服器的資料。

SLAVEOF 127.0.0.1 6379
           

實作步驟如下:

1.在從服務redisServer的設定主伺服器的位址masterhost和端口masterport

struct redisServer {
    // 主伺服器的位址
  char *masterhost;
    // 主伺服器的端口
    int masterport;
};
           

2.建立Socket連接配接

3.發送PING指令

4.身份驗證

5.發送端口資訊

6.同步

7.指令傳播,心跳檢測,檢測主從伺服器的網絡連接配接狀态,“輔助實作min-slaves配置選項,檢測指令丢失。

第16章 哨兵

哨兵系統指的是由一個或多個哨兵執行個體組成的哨兵系統可以監視任意多個主伺服器及屬下的所有從伺服器,在被監視的主伺服器進入下線狀态時,自動将某個從伺服器更新為主伺服器,并且由新的主服務代替舊的主伺服器繼續處理指令請求。

#####啟動哨兵伺服器

哨兵伺服器本質上搜尋一個運作在特殊模式下的Redis伺服器,哨兵伺服器啟動的實作主要分為三步:

1.初始化伺服器。與普通Redis伺服器不同的是,初始化時不需要通過載入RDB檔案或者AOF檔案還原資料庫狀态。下圖為哨兵伺服器的主要功能:

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

2.使用哨兵專用代碼。也就是将一部分普通Redis伺服器使用的代碼替換為哨兵伺服器使用的代碼。例如:普通Redis伺服器使用redis.c/redisCommandTable作為伺服器的指令表,因為SET,SBSIZE等很多指令哨兵伺服器不會執行,是以哨兵伺服器使用sentinel.c/sentinelcmds作為伺服器的指令表,并且其中的INFO指令會使用Sentinel模式下的專用實作sentinel.c/sentinelInfoCommand函數,而不是普通Redis伺服器使用的實作redis.c/infoCommand函數。

3.初始化哨兵伺服器的狀态。普通伺服器的一般狀态仍然由redis.h/redisServer結構儲存,哨兵伺服器會初始化一個sentinel.c/sentinelState結構,這個結構儲存了伺服器中所有和Sentinel功能有關的狀态。

struct sentinelState {
    // 目前紀元,用于實作故障轉移
    uint64_t current_epoch;
    // 儲存了所有被這個哨兵伺服器監視的主伺服器
    // 字典的鍵是主伺服器的名字
    // 字典的值則是一個指向sentinelRedisInstance結構的指針
    dict *masters;
    // 是否進入了TILT模式?
    int tilt;
    // 目前正在執行的腳本的數量
    int running_scripts;
    // 進入TILT模式的時間
    mstime_t tilt_start_time;
    // 最後一次執行時間處理器的時間
    mstime_t previous_time;
    // 一個FIFO隊列,包含了所有需要執行的使用者腳本
  list *scripts_queue;
} sentinel;
           

儲存了被監視的主伺服器資訊的sentinelRedisInstance結構

typedef struct sentinelRedisInstance {
    // 辨別值,記錄了執行個體的類型,以及該執行個體的目前狀态
    int flags;
    // 執行個體的名字
    // 主伺服器的名字由使用者在配置檔案中設定
    // 從伺服器以及Sentinel的名字由Sentinel自動設定
    // 格式為ip:port,例如"127.0.0.1:26379"
    char *name;
    // 執行個體的運作ID
    char *runid;
    // 配置紀元,用于實作故障轉移
    uint64_t config_epoch;
    // 執行個體的位址
    sentinelAddr *addr;
    // SENTINEL down-after-milliseconds選項設定的值
    // 執行個體無響應多少毫秒之後才會被判斷為主觀下線(subjectively down)
    mstime_t down_after_period;
    // SENTINEL monitor <master-name> <IP> <port> <quorum>
選項中的quorum參數
    // 判斷這個執行個體為客觀下線(objectively down
)所需的支援投票數量
    int quorum;
    // SENTINEL parallel-syncs <master-name> <number>
選項的值
    // 
在執行故障轉移操作時,可以同時對新的主伺服器進行同步的從伺服器數量
    int parallel_syncs;
    // SENTINEL failover-timeout <master-name> <ms>
選項的值
    // 重新整理故障遷移狀态的最大時限
    mstime_t failover_timeout;
    // ...
} sentinelRedisInstance;
           

4.建立連向主伺服器的網絡連接配接。哨兵伺服器會建立兩個連向被監視的主伺服器的異步網絡連接配接,一個指令連接配接,用于向主伺服器發送指令,并接受指令回複。另一個是訂閱連接配接,用于訂閱主伺服器的__sentinel__:hello頻道。通過這個頻道,哨兵伺服器可以通過主伺服器了解其他哨兵伺服器,從伺服器等資訊。

指令連接配接-擷取主伺服器資訊

哨兵伺服器會以每十秒一次的頻率,通過指令連結向被監視的主伺服器發送INFO指令,并根據回複來擷取主伺服器的目前資訊。

能擷取到的資訊如下:

1.主伺服器的相關資訊(運作ID,角色等)

2.主伺服器下屬的所有從伺服器的資訊(伺服器的角色,IP,端口,線上狀态等)

根據這些資訊,哨兵對象可以更新自身的sentinelRedisInstance結構中的主伺服器和從伺服器資訊。(可以自動發現從伺服器的資訊)

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)
指令連接配接-擷取從伺服器的資訊

當哨兵對象發現新的從伺服器出現時, 會為它建立執行個體結構,而且會建立連接配接到從伺服器的指令連接配接和訂閱連接配接。并且會以每十秒一次的頻率通過指令連接配接向從伺服器發送INFO指令,擷取從伺服器及它所屬的主伺服器的資訊。主要獲得的資訊如下:

從伺服器的運作ID,角色role。

主伺服器的IP位址,端口号master_port,主從伺服器的連接配接狀态,從伺服器的優先級slave_priority,從伺服器的複制偏移量slave_repl_offset。

下圖展示了根據上面的INFO指令回複對從伺服器的執行個體結構進行更新之後的情況:

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)
向主伺服器和從伺服器發送資訊

在預設情況下,Sentinel會以每兩秒一次的頻率,通過指令連接配接向所有被監視的主伺服器和從伺服器發送PUBLISH的指令,格式如下:

PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>
           

參數包括哨兵伺服器的IP,端口,運作ID,配置紀元。主伺服器的名字,IP,端口,配置紀元。

以下是一條Sentinel通過PUBLISH指令向主伺服器發送的資訊示例:

"127.0.0.1,26379,e955b4c85598ef5b5f055bc7ebfd5e828dbed4fa,0,mymaster,127.0.0.1,6379,0
           

這個示例包含了以下資訊:

·Sentinel的IP位址為127.0.0.1端口号為26379,運作ID為e955b4c85598ef5b5f055bc7ebfd5e828dbed4fa,目前的配置紀元為0。

·主伺服器的名字為mymaster,IP位址為127.0.0.1,端口号為6379,目前的配置紀元為0。

接收來自主伺服器和從伺服器的頻道資訊

當Sentinel與一個主伺服器或者從伺服器建立起訂閱連接配接之後,Sentinel就會通過訂閱連接配接,向伺服器發送以下指令:

SUBSCRIBE __sentinel__:hello
           

Sentinel對__sentinel__:hello頻道的訂閱會一直持續到Sentinel與伺服器的連接配接斷開為止。對于每個與Sentinel連接配接的伺服器,Sentinel既通過指令連接配接向伺服器的__sentinel__:hello頻道發送資訊,又通過訂閱連接配接從伺服器的__sentinel__:hello頻道接收資訊。當sentinel1向伺服器的__sentinel__:hello頻道發送一條資訊時,所有訂閱了__sentinel__:hello頻道的Sentinel(包括sentinel1自己在内)都會收到這條資訊,然後對相應主伺服器的執行個體結構進行更新。

更新sentinels字典

Sentinel為主伺服器建立的執行個體結構中的sentinels字典儲存了除Sentinel本身之外,還儲存了所有同樣監視這個主伺服器的其他Sentinel的資訊。當一個Sentinel收到其他Sentinel發來的資訊時,會對消息解析,更新sentinels字典。

·sentinels字典的鍵是其中一個Sentinel的名字,格式為ip:port,比如對于IP位址為127.0.0.1,端口号為26379的Sentinel來說,這個Sentinel在sentinels字典中的鍵就是"127.0.0.1:26379"。

·sentinels字典的值則是鍵所對應Sentinel的執行個體結構,比如對于鍵"127.0.0.1:26379"來說,這個鍵在sentinels字典中的值就是IP為127.0.0.1,端口号為26379的Sentinel的執行個體結構。

#####“建立連向其他Sentinel的指令連接配接

當Sentinel通過頻道資訊發現一個新的Sentinel時,它不僅會為新Sentinel在sentinels字典中建立相應的執行個體結構,還會建立一個連向新Sentinel的指令連接配接,而新Sentinel也同樣會建立連向這個Sentinel的指令連接配接,最終監視同一主伺服器的多個Sentinel将形成互相連接配接的網絡:Sentinel A有連向Sentinel B的指令連接配接,而Sentinel B也有連向Sentinel A的指令連接配接,如下圖所示:

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

Sentinel之間不會建立訂閱連接配接

Sentinel在連接配接主伺服器或者從伺服器時,會同時建立指令連接配接和訂閱連接配接,但是在連接配接其他Sentinel時,卻隻會建立指令連接配接,而不建立訂閱連接配接。因為Sentinel需要通過接收主伺服器或者從伺服器發來的頻道資訊來發現未知的新Sentinel,是以才需要建立訂閱連接配接,而互相已知的Sentinel隻要使用指令連接配接來進行通信就足夠了。

檢測主觀下線狀态

在預設情況下,Sentinel會以每秒一次的頻率向所有與它建立了指令連接配接的執行個體(包括主伺服器、從伺服器、其他Sentinel在内)發送PING指令,并通過執行個體傳回的PING指令回複來判斷執行個體是否線上。

·有效回複:執行個體傳回+PONG、-LOADING、-MASTERDOWN三種回複的其中一種。

·無效回複:執行個體傳回除+PONG、-LOADING、-MASTERDOWN三種回複之外的其他回複,或者在指定時限内沒有傳回任何回複。

Sentinel配置檔案中的down-after-milliseconds選項指定了Sentinel判斷執行個體進入主觀下線所需的時間長度:如果一個執行個體在down-after-milliseconds毫秒内,連續向Sentinel傳回無效回複,那麼Sentinel會修改這個執行個體所對應的執行個體結構,在結構的flags屬性中打開SRI_S_DOWN辨別,以此來表示這個執行個體已經進入主觀下線狀态。

檢測客觀下線狀态

當Sentinel将一個主伺服器判斷為主觀下線之後,為了确認這個主伺服器是否真的下線了,它會向同樣監視這一主伺服器的其他Sentinel發送"SENTINEL is-master-down-by-addr”指令,看它們是否也認為主伺服器已經進入了下線狀态(可以是主觀下線或者客觀下線)。當Sentinel從其他Sentinel那裡接收到足夠數量的已下線判斷之後,Sentinel就會将從伺服器判定為客觀下線,并對主伺服器執行故障轉移操作。

客觀下線狀态的判斷條件

Sentinel配置中有一個quorum屬性,當有quorum個數的Sentinel認為主服務進入下線狀态時,Sentinel便将主伺服器判定位客戶下線。(每個Sentinel的quorum可以不同)。

選舉領頭Sentinel

當一個主伺服器被判斷為客觀下線時,監視這個下線主伺服器的各個Sentinel會進行協商,選舉出一個領頭Sentinel,并由領頭Sentinel對下線主伺服器執行故障轉移操作。

以下是Redis選舉領頭Sentinel的規則和方法:

1.每個Sentinel(源Sentinel)向另一個Sentinel(目标Sentinel)發送SENTINEL is-master-down-by-addr指令,要求後者将前者設定為局部領頭Sentinel,每個Sentinel設定局部領頭Sentinel規則都是先到先得,最先向目标Sentinel發送設定要求的源Sentinel将成為目标Sentinel的局部領頭Sentinel,而之後接收到的所有設定要求都會被目标Sentinel拒絕。

2.局部領頭一旦設定,目标Sentinel會将配置紀元+1,并且給源Sentinel回複局部領頭Sentinel的運作ID和配置紀元。

3.源Sentinel在接收到目标Sentinel傳回的指令回複之後,會檢查回複中leader_epoch參數的值和自己的配置紀元是否相同,如果相同的話,那麼源Sentinel繼續取出回複中的leader_runid參數,如果leader_runid參數的值和源Sentinel的運作ID一緻,那麼表示目标Sentinel将源Sentinel設定成了局部領頭Sentinel。

4.如果有某個Sentinel被半數以上的Sentinel設定成了局部領頭Sentinel,那麼這個Sentinel成為領頭Sentinel。

5.如果在給定時限内,沒有一個Sentinel被選舉為領頭Sentinel,那麼各個Sentinel将在一段時間之後再次進行選舉,直到選出領頭Sentinel為止。

故障轉移

在選舉産生出領頭Sentinel之後,領頭Sentinel将對已下線的主伺服器執行故障轉移操作,該操作包含以下三個步驟:

1.在已下線主伺服器屬下的所有從伺服器裡面,挑選出一個從伺服器,并将其轉換為主伺服器。

挑選規則:

1)删除清單中所有處于下線或者斷線狀态的從伺服器

2)删除清單中所有最近五秒内沒有回複過領頭Sentinel的INFO指令的從伺服器

3)删除所有與已下線主伺服器連接配接斷開超過down-after-milliseconds10毫秒的從伺服器:down-after-milliseconds選項指定了判斷主伺服器下線所需的時間,而删除斷開時長超過down-after-milliseconds10毫秒的從伺服器,則可以保證清單中剩餘的從伺服器都沒有過早地與主伺服器斷開連接配接,換句話說,清單中剩餘的從伺服器儲存的資料都是比較新的。

之後,領頭Sentinel将根據從伺服器的優先級,對清單中剩餘的從伺服器進行排序,并選出其中優先級最高的從伺服器。

2.領頭Sentinel向從伺服器發送SLAVEOF指令,讓已下線主伺服器屬下的所有從伺服器改為複制新的主伺服器。

3.當這個舊的主伺服器重新上線時,領頭Sentinel向它發送SLAVEOF指令将已下線主伺服器設定為新的主伺服器的從伺服器。

第17章 叢集

Redis叢集是一個分布式資料庫方案,可以通過分片來進行資料共享,并提供複制和故障轉移功能。

節點

節點隻是一個運作在叢集模式下的Redis伺服器,由啟動時配置中的cluster-enabled屬性決定。

叢集資料結構

使用clusterNode結構可以儲存了一個節點的目前狀态,例如建立時間,名字,配置紀元,IP,端口等。

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

clusterNode結構的link屬性是一個clusterLink結構,該結構儲存了連接配接節點所需的有關資訊,比如套接字描述符,輸入緩沖區和輸出緩沖區等。(有點類似于redisClient結構中用于連接配接用戶端的套接字,緩沖區)

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

每個節點都儲存着一個clusterState結構,這個結構記錄了在目前節點的視角下,叢集目前所處的狀态,例如叢集是線上還是下線,叢集包含多少個節點,叢集目前的配置紀元等。

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

#####槽指派

Redis叢集通過分片的方式來儲存資料庫中的鍵值對:叢集的整個資料庫被分為16384個槽(slot),配置設定給每個節點處理,資料庫中的每個鍵都屬于這16384個槽的其中一個。可以使用CLUSTER MEET指令對槽進行配置設定。

clusterNode結構中的slots屬性和numslot屬性記錄了節點處理哪些槽,numslot屬性記錄了目前節點處理的槽的數量,slots屬性是一個二進制位數組,一共有16384位,數組第i位上的值代表節點是否處理槽i。

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

下圖展示了一個slots數組示例:這個數組索引0至索引7上的二進制位的值都為1,其餘所有二進制位的值都為0,這表示節點負責處理槽0至槽7。

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)
記錄叢集所有槽的指派資訊

除了記錄自身節點負責的槽位資訊以外,clusterState結構中的slots數組記錄了叢集中所有16384個槽的指派資訊。

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

slots數組包含16384個項,每個數組項都是一個指向對應的槽所在的clusterNode結構的指針,如果slots[i]指針指向NULL,那麼表示槽i尚未指派給任何節點。

clusterNode.slots 與 clusterState.slots

如果隻有clusterNode.slots ,想要知道某個槽被指派給哪個節點,需要以O(N)的複雜度對clusterState.Nodes字典中每個節點的slots數組進行周遊,而通過clusterState.slots查找隻需要O(1)複雜度。

如果隻有clusterState.slots ,想要将某個節點的槽指派資訊發送給其他節點,需要以O(N)的複雜度對clusterState.slots數組進行周遊,而通過clusterState.slots可以直接發送。

#####在叢集中執行指令

在對資料庫中的16384個槽都進行了指派之後,叢集就會進入上線狀态,這時用戶端就可以向叢集中的節點發送資料指令了。

當用戶端向節點發送與資料庫鍵有關的指令時,接收指令的節點會計算出指令要處理的資料庫鍵屬于哪個槽,并檢查這個槽所在的節點,如果鍵所在的槽正好就指派給了目前節點,那麼節點直接執行這個指令。如果鍵所在的槽并沒有指派給目前節點,那麼節點會向用戶端傳回一個MOVED錯誤,指引用戶端轉向(redirect)至正确的節點,并用戶端會向正确的節點再次發送之前想要執行的指令,并且之後這個槽的對應的鍵也會直接往正确的節點發送。

#####“計算鍵屬于哪個槽

節點使用以下算法來計算給定鍵key屬于哪個槽

def slot_number(key):
    return CRC16(key) & 16383
           

其中CRC16(key)語句用于計算鍵key的CRC-16校驗和,而&16383語句則用于計算出一個介于0至16383之間的整數作為鍵key的槽号。

#####“節點資料庫的實作

叢集節點儲存鍵值對以及鍵值對過期時間的方式,與的單機Redis伺服器儲存鍵值對以及鍵值對過期時間的方式完全相同。節點和單機伺服器在資料庫方面的一個差別是,節點隻能使用0号資料庫。

下圖展示了節點7000的資料庫狀态,資料庫中包含清單鍵"lst",哈希鍵"book",以及字元串鍵"date",其中鍵"lst"和鍵"book"帶有過期時間。

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

除了将鍵值對儲存在資料庫裡面之外,節點還會用clusterState結構中的slots_to_keys跳躍表來儲存槽和鍵之間的關系。slots_to_keys跳躍表每個節點的分值儲存鍵對應的槽位,每個節點的成員都是一個資料庫鍵,如下圖所示:

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)
客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)
重新分片

Redis叢集的重新分片操作可以将任意數量已經指派給某個節點(源節點)的槽改為指派給另一個節點(目标節點),并且相關槽所屬的鍵值對也會從源節點被移動到目标節點。

重新分片操作可以線上(online)進行,在重新分片的過程中,叢集不需要下線,并且源節點和目标節點都可以繼續處理指令請求。

“重新分片的實作原理

Redis叢集的重新分片操作是由Redis的叢集管理軟體redis-trib負責執行的,Redis提供了進行重新分片所需的所有指令,而redis-trib則通過向源節點和目标節點發送指令來進行重新分片操作。

redis-trib對叢集的單個槽slot進行重新分片的步驟如下:

1)redis-trib對目标節點發送CLUSTER SETSLOTIMPORTING<source_id>指令,讓目标節點準備好從源節點導入(import)屬于槽slot的鍵值對。

2)redis-trib對源節點發送CLUSTER SETSLOTMIGRATING<target_id>指令,讓源節點準備好将屬于槽slot的鍵值對遷移(migrate)至目标節點。

3)redis-trib向源節點發送CLUSTER GETKEYSINSLOT指令,獲得最多count個屬于槽slot的鍵值對的鍵名(key name)。

“重新分片的實作原理

Redis叢集的重新分片操作是由Redis的叢集管理軟體redis-trib負責執行的,Redis提供了進行重新分片所需的所有指令,而redis-trib則通過向源節點和目标節點發送指令來進行重新分片操作。

redis-trib對叢集的單個槽slot進行重新分片的步驟如下:

1)redis-trib對目标節點發送CLUSTER SETSLOTIMPORTING<source_id>指令,讓目标節點準備好從源節點導入(import)屬于槽slot的鍵值對。

2)redis-trib對源節點發送CLUSTER SETSLOTMIGRATING<target_id>指令,讓源節點準備好将屬于槽slot的鍵值對遷移(migrate)至目标節點。

3)redis-trib向源節點發送CLUSTER GETKEYSINSLOT指令,獲得最多count個屬于槽slot的鍵值對的鍵名(key name)。

4)對于步驟3獲得的每個鍵名,redis-trib都向源節點發送一個MIGRATE<target_ip><target_port><key_name>0指令,将被選中的鍵原子地從源節點遷移至目标節點。

5)重複執行步驟3和步驟4,直到源節點儲存的所有屬于槽slot的鍵值對都被遷移至目标節點為止。每次遷移鍵的過程如圖17-24所示。

6)redis-trib向叢集中的任意一個節點發送CLUSTER SETSLOTNODE<target_id>指令,将槽slot指派給目标節點,這一指派資訊會通過消息發送至整個叢集,最終叢集中的所有節點都會知道槽slot已經指派給了目标節點。

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

#####槽遷移相關

typedef struct clusterState {
  clusterNode *importing_slots_from[16384];
  clusterNode *migrating_slots_to[16384];
} clusterState;
           

clusterState結構的importing_slots_from數組記錄了目前節點正在從其他節點導入的槽,如果importing_slots_from[i]的值不為NULL,而是指向一個clusterNode結構,那麼表示目前節點正在從clusterNode所代表的節點導入槽i。同理migrating_slots_to數組記錄了目前節點正在導出到其他節點的槽。

當對叢集進行重新分片時,源節點會對migrating_slots_to數組進行更新,目标節點會對importing_slots_from數組進行更新。

#####ASK錯誤

在進行重新分片期間,源節點向目标節點遷移一個槽的過程中,可能會出現這樣一種情況:屬于被遷移槽的一部分鍵值對儲存在源節點裡面,而另一部分鍵值對則儲存在目标節點裡面。

當用戶端向源節點發送一個與資料庫鍵有關的指令,并且指令要處理的資料庫鍵恰好就屬于正在被遷移的槽時:

·源節點會先在自己的資料庫裡面查找指定的鍵,如果找到的話,就直接執行用戶端發送的指令。

·相反地,如果源節點沒能在自己的資料庫裡面找到指定的鍵,那麼這個鍵有可能已經被遷移到了目标節點,源節點将向用戶端傳回一個ASK錯誤,指引用戶端轉向正在導入槽的目标節點,在向目标節點發送之前想要執行的指令之前,需要先發送ASKING指令,将用戶端的REDIS_ASKING辨別打開,否則目标節點不會對正在遷移的槽執行相關的指令。(用戶端的REDIS_ASKING辨別是一個一次性辨別,當節點執行了一個帶有REDIS_ASKING辨別的用戶端發送的指令之後,用戶端的REDIS_ASKING辨別就會被移除。)

ASK

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)
客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

#####ASK錯誤和MOVED錯誤的差別

ASK錯誤和MOVED錯誤都會導緻用戶端轉向,它們的差別在于:

·MOVED錯誤代表槽的負責權已經從一個節點轉移到了另一個節點:在用戶端收到關于槽i的MOVED錯誤之後,用戶端每次遇到關于槽i的指令請求時,都可以直接将指令請求發送至MOVED錯誤所指向的節點,因為該節點就是目前負責槽i的節點。

·與此相反,ASK錯誤隻是兩個節點在遷移槽的過程中使用的一種臨時措施:在用戶端收到關于槽i的ASK錯誤之後,用戶端隻會在接下來的一次指令請求中将關于槽i的指令請求發送至ASK錯誤所訓示的節點,但這種轉向不會對用戶端今後發送關于槽i的指令請求産生任何影響,用戶端仍然會将關于槽i的指令請求發送至目前負責處理槽i的節點,除非ASK錯誤再次出現。

#####複制和故障轉移

Redis叢集中的節點分為主節點(master)和從節點(slave),其中主節點用于處理槽,而從節點則用于複制某個主節點,并在被複制的主節點下線時,代替下線主節點繼續處理指令請求。

故障檢測

叢集中的每個節點都會定期地向叢集中的其他節點發送PING消息,以此來檢測對方是否線上,如果接收PING消息的節點沒有在規定的時間内,向發送PING消息的節點傳回PONG消息,那麼發送PING消息的節點就會将接收PING消息的節點标記為疑似下線(probable fail,PFAIL)。

叢集中的各個節點會通過互相發送消息的方式來交換叢集中各個節點的狀态資訊,例如某個節點是處于線上狀态、疑似下線狀态(PFAIL),還是已下線狀态。

一個主節點A通過消息得知主節點B認為主節點C進入了疑似下線狀态時,主節點A會在自己的clusterState.nodes字典中找到主節點C所對應的clusterNode結構,并将主節點B的下線報告(failure report)添加到clusterNode結構的fail_reports連結清單裡面

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

#####故障轉移

當一個從節點發現自己正在複制的主節點進入了已下線狀态時,從節點将開始對下線主節點進行故障轉移,以下是故障轉移的執行步驟:

1)複制下線主節點的所有從節點裡面,會有一個從節點被選中。

2)被選中的從節點會執行SLAVEOF no one指令,成為新的主節點。

3)新的主節點會撤銷所有對已下線主節點的槽指派,并将這些槽全部指派給自己。

4)新的主節點向叢集廣播一條PONG消息,這條PONG消息可以讓叢集中的其他節點立即知道這個節點已經由從節點變成了主節點,并且這個主節點已經接管了原本由已下線節點負責處理的槽。

5)新的主節點開始接收和自己負責處理的槽有關的指令請求,故障轉移完成。

#####選舉新的主節點

新的主節點是通過選舉産生的,這個選舉新主節點的方法和第16章介紹的選舉領頭Sentinel的方法非常相似,因為兩者都是基于Raft算法的領頭選舉(leader election)方法來實作的。

以下是叢集選舉新的主節點的方法:

1)叢集的配置紀元是一個自增計數器,它的初始值為0。

2)當叢集裡的某個節點開始一次故障轉移操作時,叢集配置紀元的值會被增一。

3)對于每個配置紀元,叢集裡每個負責處理槽的主節點都有一次投票的機會,而第一個向主節點要求投票的從節點将獲得主節點的投票。

4)當從節點發現自己正在複制的主節點進入已下線狀态時,從節點會向叢集廣播一條CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到這條消息、并且具有投票權的主節點向這個從節點投票。

5)“如果一個主節點具有投票權(它正在負責處理槽),并且這個主節點尚未投票給其他從節點,那麼主節點将向要求投票的從節點傳回一條CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示這個主節點支援從節點成為新的主節點。

6)每個參與選舉的從節點都會接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根據自己收到了多少條這種消息來統計自己獲得了多少主節點的支援。

7)如果叢集裡有N個具有投票權的主節點,那麼當一個從節點收集到大于等于N/2+1張支援票時,這個從節點就會當選為新的主節點。

8)因為在每一個配置紀元裡面,每個具有投票權的主節點隻能投一次票,是以如果有N個主節點進行投票,那麼具有大于等于N/2+1張支援票的從節點隻會有一個,這確定了新的主節點隻會有一個。

9)如果在一個配置紀元裡面沒有從節點能收集到足夠多的支援票,那麼叢集進入一個新的配置紀元,并再次進行選舉,直到選出新的主節點為止。

#####消息

叢集中的各個節點通過發送和接收消息(message)來進行通信,

節點發送的消息主要有以下五種:

·MEET消息:

當發送者接到用戶端發送的CLUSTER MEET指令時,發送者會向接收者發送MEET消息,請求接收者加入到發送者目前所處的叢集裡面。

PING消息

叢集裡的每個節點預設每隔一秒鐘就會從已知節點清單中随機選出五個節點,然後對這五個節點中最長時間沒有發送過PING消息的節點發送PING消息,以此來檢測被選中的節點是否線上。除此之外,如果節點A最後一次收到節點B發送的PONG消息的時間,“距離目前時間已經超過了節點A的cluster-node-timeout選項設定時長的一半,那麼節點A也會向節點B發送PING消息,這可以防止節點A因為長時間沒有随機選中節點B作為PING消息的發送對象而導緻對節點B的資訊更新滞後。

PONG消息

當接收者收到發送者發來的MEET消息或者PING消息時,為了向發送者确認這條MEET消息或者PING消息已到達,接收者會向發送者傳回一條PONG消息。另外,一個節點也可以通過向叢集廣播自己的PONG消息來讓叢集中的其他節點立即重新整理關于這個節點的認識,例如當一次故障轉移操作成功執行之後,新的主節點會向叢集廣播一條PONG“消息,以此來讓叢集中的其他節點立即知道這個節點已經變成了主節點,并且接管了已下線節點負責的槽。

FAIL消息

當一個主節點A判斷另一個主節點B已經進入FAIL狀态時,節點A會向叢集廣播一條關于節點B的FAIL消息,所有收到這條消息的節點都會立即将節點B标記為已下線。

PUBLISH消息

當節點接收到一個PUBLISH指令時,節點會執行這個指令,并向叢集廣播一條PUBLISH消息,所有接收到這條PUBLISH消息的節點都會執行相同的PUBLISH指令。

第 18 章 釋出與訂閱

在Redis中,用戶端可以訂閱一個或多個頻道,之後其他用戶端往這個頻道發送消息時,所有訂閱者都可以收到這條消息。

SUBSCRIBE訂閱一個頻道。

PSUBSCRIBE是訂閱一個符合規則的一個或多個頻道。

PUBLISH是往頻道釋出一條消息。

#####頻道的訂閱與退訂

當一個用戶端執行SUBSCRIBE指令訂閱某個或某些頻道的時,

Redis将所有頻道的訂閱關系都儲存在redisServer的pubsub_channels字典裡面,這個字典的鍵是某個被訂閱的頻道,而鍵的值則是一個連結清單,連結清單裡面記錄了所有訂閱這個頻道的用戶端,如下圖所示

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

·client-1、client-2、client-3三個用戶端正在訂閱"news.it"頻道。

·用戶端client-4正在訂閱"news.sport"頻道。

·client-5和client-6兩個用戶端正在訂閱"news.business"頻道。

退訂頻道

UNSUBSCRIBE指令的行為和SUBSCRIBE指令的行為正好相反,當一個用戶端退訂某個或某些頻道的時候,伺服器将在pubsub_channels字典中找到頻道對應的訂閱者連結清單,然後從訂閱者連結清單中删除退訂用戶端的資訊。

模式的訂閱與退訂

伺服器将所有頻道的訂閱關系都儲存在伺服器狀态的pubsub_channels屬性裡面,與此類似,伺服器也将所有模式的訂閱關系都儲存在伺服器狀态的pubsub_patterns屬性裡面,pubsub_patterns屬性是一個連結清單,連結清單中的每個節點都包含着一個pubsub Pattern結構,這個結構的pattern屬性記錄了被訂閱的模式,而client屬性則記錄了訂閱模式的用戶端。

如下圖所示:

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

展示了一個pubsub_patterns連結清單示例,這個連結清單記錄了以下資訊:

·用戶端client-7正在訂閱模式"music."。

·用戶端client-8正在訂閱模式"book."。

·用戶端client-9正在訂閱模式"news.*"。

退訂模式

模式的退訂指令PUNSUBSCRIBE是PSUBSCRIBE指令的反操作:當一個用戶端退訂某個或某些模式的時候,伺服器将在pubsub_patterns連結清單中查找并删除那些pattern屬性為被退訂模式,并且client屬性為執行退訂指令的用戶端的pubsubPattern結構。

發送消息

當一個Redis用戶端執行PUBLISH指令将消息message發送給頻道channel的時候,伺服器需要執行以下兩個動作:

1)将消息message發送給channel頻道的所有訂閱者。

2)如果有一個或多個模式pattern與頻道channel相比對,那麼将消息message發送給pattern模式的訂閱者。

将消息發送給頻道訂閱者

因為伺服器狀态中的pubsub_channels字典記錄了所有頻道的訂閱關系,是以為了将消息發送給channel頻道的所有訂閱者,PUBLISH指令要做的就是在pubsub_channels字典裡找到頻道channel的訂閱者名單(一個連結清單),然後将消息發送給名單上的所有用戶端。

檢視訂閱資訊

PUBSUB指令是Redis 2.8新增加的指令之一,用戶端可以通過這個指令來檢視頻道或者模式的相關資訊,比如某個頻道目前有多少訂閱者,又或者某個模式目前有多少訂閱者。

PUBSUB CHANNELS[pattern]

這個指令用于傳回伺服器目前被訂閱的頻道(如果沒有pattern參數,那麼指令傳回伺服器目前被訂閱的所有頻道)

PUBSUB NUMSUB

PUBSUB NUMSUB[channel-1 channel-2…channel-n]子指令接受任意多個頻道作為輸入參數,并傳回這些頻道的訂閱者數量。

PUBSUB NUMPAT

PUBSUB NUMPAT子指令用于傳回伺服器目前被訂閱模式的數量。

這個子指令是通過傳回pubsub_patterns連結清單的長度來實作的,因為這個連結清單的長度就是伺服器中,使用訂閱模式訂閱頻道的用戶端的數量。

第19章 事務

Redis可以使用事務來将多個指令打包成一條指令,然後一次性,按順序地在服務端執行,在事務執行期間,伺服器不會中斷事務去執行其他用戶端的指令請求。

以下是一個事務執行的過程,該事務首先以一個MULTI指令為開始,接着将多個指令放入事務當中,最後由EXEC指令将這個事務送出(commit)給伺服器執行:

redis> MULTI
OK
redis> SET "name" "Practical Common Lisp"
QUEUED
redis> GET "name"
QUEUED
redis> SET "author" "Peter Seibel"
QUEUED
redis> GET "author"
QUEUED
redis> EXEC
1) OK
2) "Practical Common Lisp"
3) OK
4) "Peter Seibel
           

####事務的實作

#####事務的開始

MULTI指令的執行标志着事務的開始,MULTI指令可以将執行該指令的用戶端從非事務狀态切換至事務狀态,這一切換是通過改變redisClient的flags屬性中的REDIS_MULTI辨別來完成的。

#####指令入隊

當一個用戶端處于非事務狀态時,這個用戶端發送的指令會立即被當一個用戶端切換到事務狀态之後,伺服器會根據這個用戶端發來的不同指令執行不同的操作:

·如果用戶端發送的指令為EXEC、DISCARD、WATCH、MULTI四個指令的其中一個,那麼伺服器立即執行這個指令。

·與此相反,如果用戶端發送的指令是EXEC、DISCARD、WATCH、MULTI四個指令以外的其他指令,那麼伺服器并不立即執行這個指令,而是将這個指令放入一個事務隊列裡面,然後向用戶端傳回QUEUED回複。

伺服器判斷指令是該入隊還是該立即執行的過程如下圖所示:

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

#####事務隊列

redisClient中儲存了自己的事務狀态,這個事務狀态儲存在用戶端狀态的mstate屬性裡面:

typedef struct redisClient {
//事務狀态
  multiState mstate;    /* MULTI/EXEC state */
  // ...
} redisClient;
//事務狀态包含一個事務隊列,以及一個已入隊指令的計數器(也可以說是事務隊列的長度)
typedef struct multiState {
  // 事務隊列,FIFO順序
  multiCmd *commands;
  // 已入隊指令計數
  int count; 
} multiState;
//事務隊列是一個multiCmd類型的數組,數組中的每個multiCmd結構都儲存了一個已入隊指令的相關資訊,包括指向指令實作函數的指針、指令的參數,以及參數的數量:
typedef struct multiCmd { 
  // 參數
  robj **argv;
 // 參數數量
  int argc;
  // 指令指針
  struct redisCommand *cmd;
} multiCmd;
           

事務隊列以先進先出(FIFO)的方式儲存入隊的指令,較先入隊的指令會被放到數組的前面,而較後入隊的指令則會被放到數組的後面。

#####執行事務

當一個處于事務狀态的用戶端向伺服器發送EXEC指令時,這個EXEC指令将立即被伺服器執行。伺服器會周遊這個用戶端的事務隊列,執行隊列中儲存的所有指令,最後将執行指令所得的結果全部傳回給用戶端。

#####WATCH指令的實作

WATCH指令是一個樂觀鎖(optimistic locking),它可以在EXEC指令執行之前,監視任意數量的資料庫鍵,并在EXEC指令執行時,檢查被監視的鍵是否至少有一個已經被修改過了,如果是的話,伺服器将拒絕執行事務,并向用戶端傳回代表事務執行失敗的空回複。

樂觀鎖與悲觀鎖

悲觀鎖就是假定最壞的情況,每次取資料時,都假設資料會被修改,是以取資料時都進行加鎖,這樣其他線程取資料時就會阻塞,直到擷取到鎖,然後取資料。java中的synchronized和ReentrantLock等獨占鎖就是悲觀鎖思想的實作。

樂觀鎖就是假定最好的情景,每次取資料時,都假設資料不會被修改,隻有真正修改資料時會判斷之前取到的值與變量目前在記憶體中的值是否一緻,隻有一緻時才進行更新,否則就不更新。一般通過版本号或者CAS算法實作(CAS算法就是當且僅當目前記憶體中的的值等于預期值時,CAS通過原子方式用新值來更新記憶體中的值,否則不會執行任何操作(比較和替換是一個原子操作)。一般情況下是一個自旋操作,即不斷的重試)

樂觀鎖适合寫比較少的場景,悲觀鎖适合寫比較多的場景。

樂觀鎖的缺點

1.ABA問題,就是變量先被改成B值,然後改成A值,JDK1.5的AtomicStampedReference的compareAndSet會判斷對象的引用是否相同,相同才進行更新。

2. 循環時間長開銷大,執行不成功會一直重試,長時間執行會給CPU帶來壓力

3.隻能保證一個共享變量的原子操作,多個共享變量時 CAS 無效。但是JDK 1.5以後,提供了AtomicReference類來保證了引用對象之間的原子性,可以将多個變量放在同一個對象中,來保證原子性。

#####使用WATCH指令監視資料庫鍵

每個Redis資料庫都儲存着一個watched_keys字典,這個字典的鍵是某個被WATCH指令監視的資料庫鍵,而字典的值則是一個連結清單,連結清單中記錄了所有監視相應資料庫鍵的用戶端。

typedef struct redisDb {
  // 正在被WATCH指令監視的鍵
  dict *watched_keys;
} redisDb;
           

下圖是一個watched_keys字典的示例,從這個watched_keys字典中可以看出:

·用戶端c1和c2正在監視鍵"name"。

·用戶端c3正在監視鍵"age"。

·用戶端c2和c4正在監視鍵"address"。

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)
監視機制的觸發

所有對資料庫進行修改的指令,比如SET、LPUSH、SADD、ZREM、DEL、FLUSHDB等等,在執行之後都會調用multi.c/touchWatchKey函數對watched_keys字典進行檢查,檢視是否有用戶端正在監視剛剛被指令修改過的資料庫鍵,如果有的話,那麼touchWatchKey函數會将監視被修改鍵的用戶端的REDIS_DIRTY_CAS辨別打開,表示該用戶端的事務安全性已經被破壞。

判斷事務是否安全

當伺服器接收到一個用戶端發來的EXEC指令時,伺服器會根據這個用戶端是否打開了REDIS_DIRTY_CAS辨別來決定是否執行事務:

·如果用戶端的REDIS_DIRTY_CAS辨別已經被打開,那麼說明用戶端所監視的鍵當中,至少有一個鍵已經被修改過了,在這種情況下,用戶端送出的事務已經不再安全,是以伺服器會拒絕執行用戶端送出的事務,否則伺服器将執行用戶端送出的事務。

#####事務的ACID性質

在傳統的關系式資料庫中,常常用ACID性質來檢驗事務功能的可靠性和安全性。在Redis中,事務總是具有原子性(Atomicity)、一緻性(Consistency)和隔離性(Isolation),并且當Redis運作在某種特定的持久化模式下時,事務也具有耐久性(Durability)。

原子性

事務具有原子性指的是,資料庫将事務中的多個操作當作一個整體來執行,伺服器要麼就執行事務中的所有操作,要麼就一個操作也不執行。

對于Redis的事務功能來說,事務隊列中的指令要麼就全部都執行,要麼就一個都不執行,是以,Redis的事務是具有原子性的。

Redis的事務和傳統的關系型資料庫事務的最大差別在于,Redis不支援事務復原機制(rollback),即使事務隊列中的某個指令在執行期間出現了錯誤,整個事務也會繼續執行下去,直到将事務隊列中的所有指令都執行完畢為止。(原書作者認為,Redis事務的執行時錯誤通常都是程式設計錯誤産生的,是以沒必要)

一緻性

事務具有一緻性指的是,如果資料庫在執行事務之前是一緻的,那麼在事務執行之後,無論事務是否執行成功,資料庫也應該仍然是一緻的。Redis事務的一緻性主要在于解決一些錯誤,防止事務執行破壞資料庫。例如入隊錯誤,執行錯誤,伺服器停機。

伺服器停機

如果Redis伺服器在執行事務的過程中停機,那麼根據伺服器所使用的持久化模式,可能有以下情況出現:

·如果伺服器運作在無持久化的記憶體模式下,那麼重新開機之後的資料庫将是空白的,是以資料總是一緻的。

·如果伺服器運作在RDB模式下,那麼在事務中途停機不會導緻不一緻性,因為伺服器可以根據現有的RDB檔案來恢複資料,進而将資料庫還原到一個一緻的狀态。如果找不到可供使用的RDB檔案,那麼重新開機之後的資料庫将是空白的,而空白資料庫總是一緻的。

·如果伺服器運作在AOF模式下,那麼在事務中途停機不會導緻不一緻性,因為伺服器可以根據現有的AOF檔案來恢複資料,進而将資料庫還原到一個一緻的狀态。如果找不到可供使用的AOF檔案,那麼重新開機之後的資料庫将是空白的,而空白資料庫總是一緻的。

綜上所述,無論Redis伺服器運作在哪種持久化模式下,事務執行中途發生的停機都不會影響。

隔離性

事務的隔離性指的是,即使資料庫中有多個事務并發地執行,各個事務之間也不會互相影響,并且在并發狀态下執行的事務和串行執行的事務産生的結果完全相同。

因為Redis使用單線程的方式來執行事務(以及事務隊列中的指令),并且伺服器保證,在執行事務期間不會對事務進行中斷,是以,Redis的事務總是以串行的方式運作的,并且事務也總是具有隔離性的。

耐久性

事務的耐久性指的是,當一個事務執行完畢時,執行這個事務所得的結果已經被儲存到永久性存儲媒體(比如硬碟)裡面了,即使伺服器在事務執行完畢之後停機,執行事務所得的結果也不會丢失。

第 21 章 排序

在Redis中使用SORT指令可以對清單鍵、集合鍵或者有序集合鍵的值進行排序。

SORT指令的實作

SORT指令為每個被排序的鍵都建立一個與鍵長度相同的數組,數組的每個項都是一個redisSortObject結構,obj指針會指向清單或集合中的單個元素,score代表分值,根據score來對數組進行排序(因為涉及到字元串的排序及其他複雜情況,是以才單獨使用score存儲分值,而不是直接取元素的值)

以下是redisSortObject結構的完整定義:

typedef struct _redisSortObject {
   // 被排序鍵的值
   robj *obj;
   // 權重
  union {
       // 排序數字值時使用
       double score;
       // 排序帶有BY選項的字元串值時使用
       robj *cmpobj;
   } u;
} redisSortObject;
           

伺服器執行SORT指令的步驟:

1)建立一個和numbers清單長度相同的數組,該數組的每個項都是一個redis.h/redisSortObject結構,如圖所示

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

2)周遊數組,将各個數組項的obj指針分别指向numbers清單的各個項,構成obj指針和清單項之間的一對一關系,如圖21-2所示。

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

3)周遊數組,将各個obj指針所指向的清單項轉換成一個double類型的浮點數,并将這個浮點數儲存在相應數組項的u.score屬性裡面,如圖所示。

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

4)根據數組項u.score屬性的值,對數組進行數字值排序,排序後的數組項按u.score屬性的值從小到大排列,如圖所示。

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

5)周遊數組,将各個數組項的obj指針所指向的清單項作為排序結果傳回給用戶端,程式首先通路數組的索引0,傳回u.score值為1.0的清單項"1";然後通路數組的索引1,傳回u.score值為2.0的清單項"2";最後通路數組的索引2,傳回u.score值為3.0的清單項"3"。

ALPHA選項的實作

通過使用ALPHA選項,SORT指令可以對包含字元串值的鍵進行排序,會根據字母表的順序來對鍵進行排序。

ASC選項和DESC選項的實作

在預設情況下,SORT指令執行ASC升序排序,排序後的結果按值的大小從小到大排列。ASC升序排序和DESC降序排序都由相同的快速排序算法執行,它們之間的不同之處在于:

·在執行升序排序時,排序算法使用的對比函數産生升序對比結果。

·而在執行降序排序時,排序算法所使用的對比函數産生降序對比結果。

BY選項的實作

通過使用BY選項,SORT指令可以指定某些字元串鍵,或者某個哈希鍵所包含的某些域(field)來作為元素的權重,對一個鍵進行排序。

以下這個例子就使用蘋果、香蕉、櫻桃三種水果的價錢,對集合鍵fruits進行了排序,排序時會根據BY選項所給定的模式*-price,查找相應的權重鍵,例如對于"apple"元素,查找程式傳回權重鍵"apple-price"。

redis> MSET apple-price 8 banana-price 5.5 cherry-price 7
OK
redis> SORT fruits BY *-price
1) "banana"
2) "cherry"
3) "apple”
           

#####LIMIT選項的實作

可以通過LIMIT選項,我們可以讓SORT指令隻傳回其中一部分已排序的元素。

LIMIT選項的格式為LIMIT:

·offset參數表示要跳過的已排序元素數量。

·count參數表示跳過給定數量的已排序元素之後,要傳回的已排序元素數量。

GET選項的實作

通過使用GET選項,我們可以讓SORT指令在對鍵進行排序之後,根據被排序的元素,以及GET選項所指定的模式,查找并傳回其他鍵的值。

#####STORE選項的實作

在預設情況下,SORT指令隻向用戶端傳回排序結果,而不儲存排序結果:

redis> SADD students "peter" "jack" "tom"
(integer) 3
redis> SORT students ALPHA
1) "jack"
2) "peter"
3) "tom"
           

但是,通過使用STORE選項,我們可以将排序結果儲存在指定的鍵裡面,并在有需要時重用這個排序結果。

#####選項的執行順序

如果按照選項來劃分的話,一個SORT指令的執行過程可以分為以下四步:

1)排序:在這一步,指令會使用ALPHA、ASC或DESC、BY這幾個選項,對輸入鍵進行排序,并得到一個排序結果集。

2)限制排序結果集的長度:在這一步,指令會使用LIMIT選項,對排序結果集的長度進行限制,隻有LIMIT選項指定的那部分元素會被保留在排序結果集中。

3)擷取外部鍵:在這一步,指令會使用GET選項,根據排序結果集中的元素,以及GET選項指定的模式,查找并擷取指定鍵的值,并用這些值來作為新的排序結果集。

4)儲存排序結果集:在這一步,指令會使用STORE選項,将排序結果集儲存到指定的鍵上面去。

5)向用戶端傳回排序結果集:在最後這一步,指令周遊排序結果集,并依次向用戶端傳回排序結果集中的元素。

第 22 章 二進制位數組

位數組是一個數組,數組元素是一個二進制位,在Redis中可以使用字元串對象來代表二進制位數組,字元串對象是一個SDS結構,SDS結構的buf是一個char數組,數組的每一位是一個char字元,每個char字元是8位。

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

#####GETBIT指令的實作

GETBIT指令用于傳回位數組bitarray在offset偏移量上的二進制位的值。

GETBIT指令的執行過程如下:

1)計算byte= offset÷8」,byte值記錄了offset偏移量指定的二進制位儲存在位數組的哪個位元組。

2)計算bit=(offset mod 8)+1,bit值記錄了offset偏移量指定的二進制位是byte位元組的第幾個二進制位。

3)根據byte值和bit值,在位數組bitarray中定位offset偏移量指定的二進制位,并傳回這個位的值。

SETBIT指令的實作

同理,SETBIT用于将位數組bitarray在offset偏移量上的二進制位的值設定為value,并向用戶端傳回二進制位被設定之前的舊值。

#####BITCOUNT指令的實作

BITCOUNT指令用于統計給定位數組中,值為1的二進制位的數量。

實作BITCOUNT指令可能使用的幾種算法進行介紹:

1.周遊算法,對每個位進行周遊,統計1的個數,時間複雜度為O(N)

2.查表算法,因為8個二進制位的1和0的排列方式是有限的,可以進行建表,将所有情況進行計算好。然後每次擷取8個二進制位,然後進行查表比對,擷取到這8個二進制位中1的個數。時間複雜度為O(N/8)。鍵長為8位的表如圖所示,但是需要占用一定的記憶體空間來存儲表。

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

3.二進制位統計算法(3):variable-precision SWAR算法

“swar函數每次執行可以計算32個二進制位的漢明重量,它比之前介紹的周遊算法要快32倍,比鍵長為8位的查表法快4倍,比鍵長為16位的查表法快2倍,并且因為swar函數是單純的計算操作,是以它無須像查表法那樣,使用額外的記憶體。複雜度為O(N/32)。

“另外,因為swar函數是一個常數複雜度的操作,是以我們可以按照自己的需要,在一次循環中多次執行swar,進而按倍數提升計算漢明重量的效率:

·例如,如果我們在一次循環中調用兩次swar函數,那麼計算漢明重量的效率就從之前的一次循環計算32位提升到了一次循環計算64位。

#####Redis中BITCOUNT的實作

在執行BITCOUNT指令時,程式會根據未處理的二進制位的數量來決定使用那種算法:

·如果未處理的二進制位的數量大于等于128位,那麼程式使用variable-precision SWAR算法來計算二進制位的漢明重量。在variable-precision SWAR算法方面,BITCOUNT指令在每次循環中載入128個二進制位,然後調用四次32位variable-precision SWAR算法來計算這128個二進制位的漢明重量。

·如果未處理的二進制位的數量小于128位,那麼程式使用查表算法來計算二進制位的漢明重量。

BITOP指令的實作

因為C語言直接支援對位元組執行邏輯與(&)、邏輯或(|)、邏輯異或(^)和邏輯非(~)操作,是以BITOP指令的AND、OR、XOR和NOT四個操作都是直接基于這些邏輯操作實作的。

例如:

BITOP AND result x y
           

使用BITOP指令對x和y進行與運算,并将結果儲存在result鍵上。

第 23 章 慢查詢日志

Redis的慢查詢日志功能用于記錄執行時間超過給定時長的指令請求,使用者可以通過這個功能産生的日志來監視和優化查詢速度。

伺服器配置有兩個和慢查詢日志相關的選項:

·slowlog-log-slower-than選項指定執行時間超過多少微秒(1秒等于1 000 000微秒)的指令請求會被記錄到日志上。

·slowlog-max-len選項指定伺服器最多儲存多少條慢查詢日志。如果滿了會将最舊的慢查詢日志删除。

第 24 章 螢幕

通過執行MONITOR指令,用戶端可以将自己變為一個螢幕,實時地接收并列印出伺服器目前處理的指令請求的相關資訊。

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)

伺服器收到MONITOR指令後,會将這個用戶端的REDIS_MONITOR标志會被打開,并且這個用戶端本身會被添加到monitors連結清單的表尾,之後伺服器在每次處理指令請求之前,都會調用replicationFeedMonitors函數,由這個函數将被處理的指令請求的相關資訊發送給各個螢幕。

客官,這是一份精心編寫的《Redis設計與實作》讀書心得(下篇)