首頁  >  文章  >  資料庫  >  聊聊redis中的那些高可用方案!

聊聊redis中的那些高可用方案!

青灯夜游
青灯夜游轉載
2022-01-17 10:07:032477瀏覽

redis有哪些高可用方案?這篇文章為大家介紹一下redis中的那些高可用方案,希望對大家有幫助!

聊聊redis中的那些高可用方案!

redis通常不會是部署單一的,不然不會造成單點故障,那麼redis有哪些高可用方案呢?

主從複製

使用者可以透過SLAVEOF指令或配置,讓一個伺服器去複製另一個伺服器。被複製的伺服器稱為主伺服器,進行複製的伺服器稱為從伺服器。這樣你在主伺服器上增加鍵值,同時可以在從伺服器上讀取。 【相關推薦:Redis影片教學

複製的過程又分為同步與指令傳播兩個步驟。

同步

同步將從伺服器的資料庫狀態更新到主伺服器目前的資料庫狀態。

客戶端向從伺服器發送SLAVEOF指令時,從伺服器會向主伺服器發生SYNC指令進行同步,步驟如下:

  • 從伺服器向主伺服器發生SYNC指令。

  • 收到SYNC指令的主伺服器執行BGSAVE指令,在後台產生一個RDB文件,並用一個緩衝區記錄從現在開始執行的所有寫入指令。

  • 主伺服器的BGSAVE指令執行完畢後,主伺服器將BGSAVE產生的RDB檔案傳送給從伺服器,從伺服器接收並載入這個RDB文件,將從伺服器的資料庫狀態更新至主伺服器執行BGSAVE指令時的資料庫狀態

  • 主伺服器將緩衝區的所有寫入指令傳送給從伺服器,從伺服器執行這些寫入指令,將資料庫狀態更新至主伺服器目前資料庫狀態。

聊聊redis中的那些高可用方案!

指令傳播

#同步作業完成之後,主伺服器和從伺服器的資料庫狀態是一致的,但主伺服器又接收到客戶端寫入指令後,主從資料庫之間又產生了資料不一致,這時透過指令傳播達到資料庫一致。

PSYNC同步的最佳化

#2.8之前的同步每次都是全量同步,而如果是從伺服器只是斷開連接了一會兒,事實上是不用從頭開始同步的,只需要將斷開連接這會的資料同步即可。所以2.8版開始使用PSYNC來取代SYNC指令。

PSYNC分成全量同步和部分同步兩種情況,全量同步就是處理初次同步的狀態,而部分同步就是處理斷線重連這種情況。

部分同步的實作

部分同步主要使用了以下三個部分:

    ##主伺服器的複製偏移量和從伺服器的複製偏移量
  • 主伺服器的複製積壓緩衝區
  • 伺服器的運行ID

複製偏移

#主伺服器的複製偏移:主伺服器每次向從伺服器傳播N個位元組的資料時,就將自己的複製偏移量N從伺服器的複製偏移量:從伺服器每次收到主伺服器傳播的N個位元組數據,就將自己的複製偏移量 N 如果主從伺服器處於一致狀態,那麼它們的偏移量總是相同的,如果偏移量不相等,那麼表示它們處於不一致狀態。

複製積壓緩衝區

複製積壓緩衝區由主伺服器維護的一個固定長度的FIFO佇列,預設大小1MB,達到最大長度後,先入隊的會被彈出,給新入隊的元素讓位置。

redis指令傳播的時候不但會傳送給從伺服器,還會傳送給複製積壓緩衝區。

聊聊redis中的那些高可用方案!

當從伺服器重連上主伺服器時,從伺服器會透過PSYNC指令將自己的複製偏移量offset傳送給主伺服器,主伺服器會根據複製偏移量來決定使用部分同步還是全量同步。 如果offset偏移量之後的資料還在複製積壓緩衝區,那麼使用部分同步,反之使用全量同步。

(書上沒說是怎麼判斷的,我猜測應該是拿主複製偏移量減去從複製偏移量,如果大於1MB就表示有資料不在緩衝積壓區?)

#伺服器的運行ID

伺服器啟動時會產生一個40位元隨機的字元作為伺服器運行ID。

從伺服器初次複製主伺服器時,主伺服器會將自己的運行ID傳送給從伺服器,而從伺服器會將這個運行ID保存下來。從伺服器斷線重連的時候,會將已儲存的運行ID傳送過去,如果從伺服器保存的運行ID和目前主伺服器的運行ID相同,那麼會嘗試部分同步,如果不同會執行全量同步。

PSYNC的整體流程

聊聊redis中的那些高可用方案!

心跳偵測

在指令傳播階段,從伺服器會預設以每秒一次的頻率,向主伺服器發送指令:
REPLICONF ACK <replication_offset></replication_offset>
其中replication_offset就是從伺服器目前的複製偏移量。 傳送REPLICONF ACK指令對於主從伺服器有三個功能:

  • 偵測主從伺服器的網路連線狀態。

  • 輔助實作min-slaves選項。

  • 偵測指令遺失。

偵測主從伺服器的網路連線狀態

#主從伺服器可以透過傳送和接收REPLICONF ACK指令來檢查兩者之間的網路連線是否正常:如果主伺服器超過一秒鐘沒有收到從伺服器發來的REPLICONF ACK指令,那麼主伺服器就知道主從之間出現問題了。

輔助實作min-slaves選項

redis的min-slaves-to-writemin-slaves-max-lag 兩個選項可以防止主從伺服器在不安全的情況下執行寫入命令。

min-slaves-to-write 3
min-slaves-max-lag 10

如果配置如上,就表示如果從伺服器的數量少於3個,或者當3個從伺服器的延遲都大於或等於10秒時,那麼主伺服器就會拒絕執行寫入命令。

偵測指令遺失

如果因為網路故障,主伺服器傳播給從伺服器的寫入指令在半路遺失,那麼從伺服器傳送REPLICONF ACK指令到主伺服器時,主伺服器將發覺從伺服器目前的複製偏移量少於自己的偏移量,那麼主伺服器可以根據從伺服器的複製偏移量,在複製緩衝區當中找到從伺服器缺少的數據,將這些資料重寫入發送給從伺服器。

主從複製總結

其實主從複製就是多備份了一份數據,因為即使有RDB和AOF進行持久化,但是可能主伺服器上整個機器掛掉了,而主從複製可以將主從伺服器部署在兩台不同的機器上,這樣即使主伺服器的機器掛掉了,也可以手動切換到從伺服器繼續服務。

sentinel

主從雖然實現了資料的備份,但當主伺服器掛掉時,需要手動的將從伺服器切換成主伺服器.而sentinel就可以實作當主伺服器掛掉時,自動將從伺服器切換成主伺服器。

聊聊redis中的那些高可用方案!

sentinel系統可以監視所有的主從伺服器,假設server1現在下線。當server1的下線時長超過使用者設定的下線時長上限時,sentinel系統就會對server1執行故障轉移:

  • 首先sentinel系統會挑選server1下的其中一個從伺服器,並將這個選取的從伺服器升級成新的主伺服器。在

  • 之後,sentinel系統會向server1屬下的所有從伺服器發送新的複製指令,讓他們成為新主伺服器的從伺服器。當所有從伺服器複製新的主伺服器時,故障轉移操作執行完畢。

  • 另外,sentinel也會監視已下線的server1,在它重新上線時,將它設定為新的主伺服器的從伺服器。

初始化sentinel狀態

#
struct sentinelState {
    char myid[CONFIG_RUN_ID_SIZE+1]; 
    // 当前纪元,用于实现故障转移
    uint64_t current_epoch;
    // 保存了所有被这个sentinel监视的主服务器
    // 字典的键是主服务器的名字
    // 字典的值是指向sentinelRedisInstance结构的指针
    dict *masters;
    // 是否进入了TILT模式
    int tilt;         
    // 目前正在执行的脚本数量
    int running_scripts;   
    // 进入TILT模式的时间
    mstime_t tilt_start_time;   
    // 最后一次执行时间处理器的时间
    mstime_t previous_time;     
    // 一个fifo队列,包含了所有需要执行的用户脚本
    list *scripts_queue;            
    char *announce_ip;  
    int announce_port; 
    unsigned long simfailure_flags; 
    int deny_scripts_reconfig;
    char *sentinel_auth_pass;   
    char *sentinel_auth_user;    
    int resolve_hostnames;      
    int announce_hostnames;     
} sentinel;

初始化sentinel狀態的masters屬性

#masters記錄了所有被sentinel監視的主伺服器的相關信息,其中字典的鍵是被監視伺服器的名字,而值是被監視伺服器對應著sentinelRedisInstance結構。 sentinelRedisInstance被sentinel伺服器監視的實例,可以是主伺服器、從伺服器或其他sentinel實例。

typedef struct sentinelRedisInstance {
    // 标识值,记录实例的类型,以及该实例的当前状态
    int flags;  
    // 实例的名字
    // 主服务器名字在配置文件中设置
    // 从服务器和sentinel名字由sentinel自动设置,格式是ip:port
    char *name; 
    // 运行id
    char *runid;   
    // 配置纪元,用于实现故障转移
    uint64_t config_epoch;  
    // 实例的地址
    sentinelAddr *addr; /* Master host. */
    // 实例无响应多少毫秒之后,判断为主观下线
    mstime_t down_after_period; 
    // 判断这个实例为客观下线所需的支持投票数量
    unsigned int quorum;
    // 执行故障转移,可以同时对新的主服务器进行同步的从服务器数量
    int parallel_syncs; 
    // 刷新故障迁移状态的最大时限
    mstime_t failover_timeout;  
    // 除了自己外,其他监视主服务器的sentinel
    // 键是sentinel的名字,格式是ip:port
    // 值是键对应的sentinel的实例结构
    dict *sentinels;  
    // ...
} sentinelRedisInstance;

建立連向主伺服器的網路連接

#初始化sentinel的最後一步是建立連向被監視主伺服器的網路連接,會建立兩個連向主伺服器的連線。

聊聊redis中的那些高可用方案!

指令連線:專門向主伺服器發送命令,並接收命令回覆。
訂閱連線:專門用於訂閱主伺服器的_sentinel_:hello頻道。

取得主伺服器資訊

sentinel預設會每10秒,透過指令連線向被監視的主伺服器發送INFO指令,並透過回覆取得主伺服器目前的資訊。回覆可以獲得以下資訊。

  • 主伺服器的run_id
  • 主伺服器下所有從伺服器的資訊。

根據這些資訊可以更新sentinelRedisInstance下的name字典和runid欄位。

取得從伺服器資訊

sentinel也會建立連接到從伺服器的命令連線和訂閱連線。

聊聊redis中的那些高可用方案!

sentinel預設會每10秒,透過指令連線向從伺服器發送INFO指令,並透過回覆取得從伺服器目前的資訊。回覆如下:

聊聊redis中的那些高可用方案!

  • 從伺服器的執行ID
  • 從伺服器的角色role
  • 主伺服器的ip和連接埠
  • 主伺服器的連線狀態master_link_status
  • 從伺服器的優先權slave_priority
  • 從伺服器的複製偏移變數

根據info的回覆訊息,sentinel可以更新從伺服器的實例結構。

向主伺服器和從伺服器的訂閱連線發送訊息

預設情況下,sentinel會每2秒一次,向被監視的主伺服器和從伺服器發送命令。

聊聊redis中的那些高可用方案!

s_ip:sentinel的ip位址
s_port:sentinel的連接埠號碼
s_runid:sentinel的運行id
s_epoch:sentinel目前的設定紀元
m_name:主伺服器的名字
m_ip:主伺服器的ip位址
m_port:主伺服器的連接埠號碼
m_epoch:主伺服器目前的設定紀元
向sentinel_:hello頻道傳送訊息,也會被監視同一個伺服器的其他sentinel監聽到(包括自己)。

建立連向其他sentinel的指令連線

sentinel之間會互相建立指令連線。監視同一個囑咐其的多個sentinel將形成相互連接的網路。

聊聊redis中的那些高可用方案!

sentinel之間不會建立訂閱連線。

檢測主觀下線狀態

sentinel會每秒一次向所有與它建立了命令連接的實例(主伺服器、從伺服器、其他sentinel)發送ping指令,透過實例的回覆來判斷實例是否在線。
有效回應:實例傳回 PONG、-LOADING、-MASTERDOWN其中一種。
無效回復:以上三種回復以外的其他回复,或指定時長內沒回复。
某個實例在down-after-milliseconds毫秒內,連續向sentinel傳回無效回應。那麼sentinel就會修改這個實例對應的實例結構,在結構的flags屬性中開啟SRI_S_DOWN標識,標識實例進入主觀下線狀態。 (down-after-milliseconds可以在sentinel的設定檔中設定)

#偵測客觀下線狀態

##當sentinel將一個主伺服器判斷為主觀下線後,為了確認這個主伺服器是否真的下線,還會想其他同樣監視這個主伺服器的其他sentinel詢問,看其他sentinel是否也認為該主伺服器下線了。超過一定數量就將主伺服器判斷為客觀下線。

詢問其他sentinel是否同意該伺服器下線

#SENTINEL is-master-down-by-addr

#透過SENTINEL is-master-down-by-addr指令詢問,參數意義如下圖:

聊聊redis中的那些高可用方案!

#接收SENTINEL is-master-down-by-addr指令

其他sentinel接收到SENTINEL is-master-down-by-addr指令後,會根據其中主伺服器的ip和端口,檢查主伺服器是否下線,然後返回包含三個參數的Multi Bulk的回复。

10-聊聊redis中的那些高可用方案!

sentinel統計其他sentinel同意主伺服器已下線的數量,達到配置的數量後,則將主伺服器的flags屬性的SRI_O_DOWN標識打開,表示主伺服器已經進入客觀下線狀態。

選舉領頭sentinel

當一個主伺服器被判斷成客觀下線時,監視這個下線主伺服器的各個sentinel就會協商選舉一個新的領頭sentinel,由這個sentinel進行故障轉移操作。

1聊聊redis中的那些高可用方案!

確認主伺服器進入客觀下線狀態後,會再傳送SENTINEL is-master-down-by-addr指令來選出領頭sentinel。

選舉規則

  • 監視同一個主伺服器的多個線上sentinel中每一個都可能成為領頭sentinel。
  • 每次進行領頭sentinel選舉之後,無論選舉是否成功,所有sentinel的配置紀元(configuration epoch)的值都會自增一次。 (配置紀元,其實就是一個計數器)
  • 在一個配置紀元裡,所有sentinel都有將某個sentinel設定成局部sentinel的機會,一旦設定在這個配置紀元裡就不能再更改。
  • 所有發現主伺服器客觀下線的sentinel都會要求其他sentinel將自己設定為局部領頭sentinel,也就是都會發送SENTINEL is-master-down-by-addr指令,試著讓其他sentinel將自己設定成局部領頭sentinel。
  • 當一個sentinel向另一個sentinel發送SENTINEL is-master-down-by-addr指令時,如果runid參數的值不是*,而是源sentinel的runid,就表示要目標sentinel將自己設定成領頭sentinel。
  • sentinel設定局部領頭的規則是先到先得,第一個設定為局部領頭sentinel後,其他的請求都被拒絕。
  • 目標sentinel在接收到一條SENTINEL is-master-down-by-addr指令後,將會傳回來源sentinel一個指令。回復中leader_runid參數和leader_epoch參數分別記錄了目標sentinel的局部領頭sentinel的runid和配置紀元。
  • 來源sentinel接收到回復之後,會比較回傳的配置紀元是否和自己的配置紀元相同,如果一樣再繼續比較返回的局部領頭sentinel的runid是否和自己的runid相同,如果一致就表示目標sentinel將自己設定成了局部領頭sentinel。
  • 如果某個sentinel被半數以上的sentinel設定成了局部領頭sentinel,那麼它就變成領頭sentinel。
  • 領頭sentinel需要半數以上支持,並且每個配置紀元內只能設定一次,那麼一個配置紀元裡,只會出現一個領頭sentinel
  • 如果在一定時限內,每一個sentinel被選舉成領頭sentinel(沒人沒拿到半數以上選票),那麼各個sentinel在一段時間之後再次選舉,直到選出領頭sentinel

故障轉移

故障轉移包含以下三個步驟:

  • 在已下線的主伺服器下所有從伺服器裡,挑選出一個從伺服器轉換成主伺服器。

  • 讓已下線的主伺服器屬下的所有從伺服器改為複製新的主伺服器。

  • 將已經下線的主伺服器設定為新伺服器的從伺服器,舊的主伺服器重新上線後,它就成為新的主伺服器的從伺服器。

選出新的主伺服器

#已下線的主伺服器下所有從伺服器裡,挑選出一個從伺服器,向這個從伺服器發送SLAVEOF no one指令,將這個從伺服器轉換成主伺服器。

挑選新主伺服器的規則

領先的sentinel會將已下線主伺服器的所有從伺服器儲存到一個清單裡面,然後對這個清單進行過濾,挑選出新的主伺服器。

  • 刪除清單中所有處於下線或斷線狀態的從伺服器。

  • 刪除清單中所有最近五秒內沒有回復過領頭sentinel的INFO命令的從伺服器

  • 刪除所有與離線伺服器連接斷開超過dwon-after-milliseconds * 10毫秒的伺服器

  • 然後根據從伺服器的優先級,對清單中剩餘的從伺服器進行排序,並選出其中優先級最高的伺服器。

  • 如果有多個相同最高優先權的從伺服器,那麼就根據複製偏移量進行排序,選出最大偏移量的從伺服器(複製偏移量最大也代表它保存的資料最新)

  • 如果複製偏移量也相同,那麼就根據runid進行排序,選其中runid最小的從伺服器

發送slaveof no one 指令之後,領頭sentinel會每秒一次向被升級的從伺服器發送info指令(平常每10秒一次),如果回傳的回覆role從原來的slave變成了master,那麼領頭sentinel就知道從伺服器已經升級成主伺服器了。

修改從伺服器的複製目標

通过SLAVEOF命令来使从服务器复制新的主服务器。当sentinel监测到旧的主服务器重新上线后,也会发送SLAVEOF命令使它成为新的主服务器的从服务器。

sentinel总结

sentinel其实就是一个监控系统,,而sentinel监测到主服务器下线后,可以通过选举机制选出一个领头的sentinel,然后由这个领头的sentinel将下线主服务器下的从服务器挑选一个切换成主服务器,而不用人工手动切换。

集群

哨兵模式虽然做到了主从自动切换,但是还是只有一台主服务器进行写操作(当然哨兵模式也可以监视多个主服务器,但需要客户端自己实现负载均衡)。官方也提供了自己的方式实现集群。

节点

每个redis服务实例就是一个节点,多个连接的节点组成一个集群。

CLUSTER MEET <ip><port></port></ip>

向另一个节点发送CLUSTER MEET命令,可以让节点与目标节点进行握手,握手成功就能将该节点加入到当前集群。

启动节点

redis服务器启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器集群模式。

1聊聊redis中的那些高可用方案!

集群数据结构

每个节点都会使用一个clusterNode结构记录自己的状态,并为集群中其他节点都创建一个相应的clusterNode结构,记录其他节点状态。

typedef struct clusterNode {
    // 创建节点的时间
    mstime_t ctime; 
    // 节点的名称
    char name[CLUSTER_NAMELEN];
    // 节点标识
    // 各种不同的标识值记录节点的角色(比如主节点或从节点)
    // 以及节点目前所处的状态(在线或者下线)
    int flags;     
    // 节点当前的配置纪元,用于实现故障转移
    uint64_t configEpoch;
    // 节点的ip地址
    char ip[NET_IP_STR_LEN];  
    // 保存建立连接节点的有关信息
    clusterLink *link;          
    
    list *fail_reports;  
    // ...
} clusterNode;

clusterLink保存着连接节点所需的相关信息

typedef struct clusterLink {
    // ...
    // 连接的创建时间
    mstime_t ctime;           
    // 与这个连接相关联的节点,没有就为null
    struct clusterNode *node;   
    // ...
} clusterLink;

每个节点还保存着一个clusterState结构,它记录了在当前节点视角下,集群目前所处的状态,例如集群在线还是下线,集群包含多少个节点等等。

typedef struct clusterState {
    // 指向当前节点clusterNode的指针
    clusterNode *myself;  
    // 集群当前的配置纪元,用于实现故障转移
    uint64_t currentEpoch;
    // 集群当前的状态,上线或者下线
    int state;           
    // 集群中至少处理一个槽的节点数量
    int size;      
    // 集群节点的名单(包括myself节点)
    // 字典的键是节点的名字,字典的值为节点对应的clusterNode结构
    dict *nodes; 
} clusterState;

CLUSTER MEET 命令的实现

CLUSTER MEET <ip><port></port></ip>

  • 节点 A 会为节点 B 创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes 字典里面。

  • 之后,节点 A 将根据 CLUSTER MEET 命令给定的 IP 地址和端口号,向节点 B 发送一条 MEET 消息。

  • 如果一切顺利,节点 B 将接收到节点 A 发送的 MEET 消息,节点 B 会为节点 A 创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面。

  • 之后,节点 B 将向节点 A 返回一条 PONG 消息。

  • 如果一切顺利,节点 A 将接收到节点 B 返回的 PONG 消息,通过这条 PONG 消息节点 A 可以知道节点 B 已经成功地接收到了自己发送的 MEET 消息。

  • 之后,节点 A 将向节点 B 返回一条 PING 消息。

  • 如果一切顺利,节点B将接收到节点A返回的PING消息,通过这条PING消息节点B知道节点A已经成功接收到自己返回的PONG消息,握手完成。

1聊聊redis中的那些高可用方案!

槽指派

集群的整个数据库被分为16384个槽,每个键都属于16384个槽的其中一个,集群中每个节点处理0个或16384个槽。当所有的槽都有节点在处理时,集群处于上线状态,否则就是下线状态。

CLUSTER ADDSLOTS

CLUSTER ADDSLOTS <slot>...</slot>
通过CLUSTER ADDSLOTS命令可以将指定槽指派给当前节点负责,例如:CLUSTER ADDSLOTS 0 1 2 3 4 可以将0至4的槽指派给当前节点

记录节点的槽指派信息

clusterNode结构的slots属性和numslot属性记录了节点负责处理哪些槽:

typedef struct clusterNode {
         
    unsigned char slots[CLUSTER_SLOTS/8];
    
    int numslots;
    // ...
} clusterNode;

slots:是一个二进制数组,一共包含16384个二进制位。当二进制位的值是1,代表节点负责处理该槽,如果是0,代表节点不处理该槽numslots:numslots属性则记录节点负责处理槽的数量,也就是slots中值为1的二进制位的数量。

传播节点的槽指派信息

节点除了会将自己负责的槽记录在clusterNode中,还会将slots数组发送给集群中的其他节点,以此告知其他节点自己目前负责处理哪些槽。

typedef struct clusterState {
    clusterNode *slots[CLUSTER_SLOTS];
} clusterState;

slots包含16384个项,每一个数组项都是指向clusterNode的指针,表示被指派给该节点,如果未指派给任何节点,那么指针指向NULL。

CLUSTER ADDSLOTS命令的实现

1聊聊redis中的那些高可用方案!

在集群中执行命令

客户端向节点发送与数据库有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查该槽是否指派给了自己。
如果指派给了自己,那么该节点直接执行该命令。如果没有,那么该节点会向客户端返回一个MOCED的错误,指引客户端转向正确的节点,并再次发送执行的命令。

1聊聊redis中的那些高可用方案!

计算键属于那个槽

1聊聊redis中的那些高可用方案!

CRC16(key)是计算出键key的CRC16的校验和,而 & 16383就是取余,算出0-16383之间的整数作为键的槽号。

判断槽是否由当前节点负责处理

计算出键所属的槽号i后,节点就能判断该槽号是否由自己处理。
如果clusterState.slots[i]等于如果clusterState.myself,那么由自己负责该节点可以直接执行命令。
如果不相等,那么可以获取clusterState.slots[i]指向如果clusterNode的ip和端口,向客户端返回MOVED错误,指引客户端转向负责该槽的节点。

集群模式下不会打印MOVED错误,而是直接自动转向。

重新分片

redis集群重新分配可以将任意数量已经指派给某个节点的槽改为指派给另一个节点,相关槽所属的键值对也会从源节点移动到目标节点。
重新分片操作是在线进行的,在重新分片的过程中,集群不用下线,源节点和目标节点都可以继续处理命令请求。 redis集群的重新分片操作是由redis-trib负责执行。重新分片执行步骤如下:

  • redis-trib对目标节点发送CLUSTER SETSLOT <slot> IMPORTING <source_id></source_id></slot>命令,让目标节点准备好从源节点导入槽slot的键值对。

  • redis-trib对源节点发送CLUSTER SETSLOT <slot> MIGRTING <target_id></target_id></slot>命令,让源节点准备好将属于槽slot的键值对迁移至目标节点。

  • redis-trib向源节点发送CLUSTER GETKEYSINSLOT <slot> <count></count></slot>命令,获取最多count个属于槽的键值对的键名称。

  • 对于步骤3获取的每个键名,redis-trib都向源节点发送一个MIGRTING <target_ip> <target_port> <key_name> 0 <timeout></timeout></key_name></target_port></target_ip>命令,将被选中的键值对从源节点迁移至目标节点。

  • 重复执行步骤3和步骤4,直到源节点保存的所以属于槽slot的键值对都被迁移至目标节点。

  • redis-trib向集群中任何一个节点发送CLUSTER SETSLOT <slot> NODE <target_id></target_id></slot>命令,将槽指派给目标节点。这一信息最终会通过消息发送至整个集群。

1聊聊redis中的那些高可用方案!

CLUSTER SETSLOT IMPORTING 命令实现

typedef struct clusterState {
    // ...
    clusterNode *importing_slots_from[CLUSTER_SLOTS];

} clusterState;

importing_slots_from记录了当前节点正在从其他节点导入的槽。importing_slots_from[i]不为null,则指向CLUSTER SETSLOT <slot> IMPORTING <source_id></source_id></slot>命令,所代表的clusterNode结构。

CLUSTER SETSLOT MIGRTING 命令实现

typedef struct clusterState {
    // ...
    clusterNode *migrating_slots_to[CLUSTER_SLOTS];

} clusterState;

migrating_slots_to记录了当前节点正在迁移至其他节点的槽。migrating_slots_to[i]不为null,则指向迁移至目标节点所代表的clusterNode结构。

ASK错误

在重新分片期间,源节点向目标节点迁移槽的过程中,可能属于这个槽的一部分键值对一部分保存在源节点当中,而另一部分保存在目标节点当中。
客户端向源节点发送一个与数据库键有关的命令,恰好这个槽正在被迁移。
源节点现在自己的数据库中查找指定的键,如果找到,直接执行。
如果没有找到,节点会检查migrating_slots_to[i]查看键是否正在迁移,如果在迁移就返回一个ask错误,引导客户端转向目标节点。

ASKING

客户端收到ask错误之后,会先执行ASKING命令,再向目标节点发送命令。ASKING命令就是打开发送该命令的客户端的REDIS_ASKING标识。一般来说客户端发送的键如果不属于自己负责会返回MOVED错误(槽只迁移部分,这时槽还不属于目标节点负责),但还会检查importing_slots_from[i],如果显示节点正在导入槽i,并且发送命令的客户端带有REDIS_ASKING标识,那么它就会破例执行一次该命令。

1聊聊redis中的那些高可用方案!

集群的故障转移

集群的故障转移效果和哨兵模式类似,也是将从节点升级成主节点。旧的主节点重新上线后将会成为新主节点的从节点。

故障检测

集群中每个节点会定期的向集群中其他节点发送PING消息,检测对方是否在线,如果指定时间内没有收到PONG消息,那么就将该节点标记为疑似下线。clusterState.nodes字典中找到该节点的clusterNode结构,将flags属性修改成REDIS_NODE_PFAIL标识。
集群中各个节点会互相发送消息来交换集群中各个节点的状态,例如:主节点A得知主节点B认为主节点C进入了疑似下线状态,主节点A会在clusterState.nodes字典中找到节点C的clusterNode结构,并将主节点B的下线报告添加到clusterNode结构的fail_reports链表当中。
每一个下线报告由一个clusterNodeFailReport结构表示

typedef struct clusterNodeFailReport {
    struct clusterNode *node; 
    // 最后一次收到下线报告的时间
    mstime_t time;            
} clusterNodeFailReport;

如果一个集群当中,半数以上负责处理槽的主节点都将某个主节点X报告为疑似下线。那么这个主节点X将被标记为已下线。将主节点X标记成已下线的节点会向集群广播一条关于主节点X的FAIL消息。所有收到这条FAIL消息的节点都会将主节点X标记成已下线。

故障转移

当一个从节点发现自己正在复制的主节点进入了已下线状态,从节点将开始对下线主节点进行故障转移。

  • 复制下线主节点的所有从节点,会有一个主节点被选中。

  • 被选中的从节点会执行SLAVEOF no one 命令,成为新的主节点。

  • 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。

  • 新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成主节点。这个主节点已经接管了已下线节点负责处理的槽。

  • 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。

选举新的主节点

新的主节点通过选举产生

  • 集群的配置纪元是一个自增计数器,它的初始值为0。

  • 当集群的某个节点开始一次故障转移操作,集群的配置纪元的值加1。

  • 对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,第一个想主节点要求投票的从节点将获得主节点的投票。

  • 当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息,并具有投票权的主节点向这个从节点投票。

  • 如果一个主节点具有投票权(它正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。

  • 每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。

  • 如果集群里有 N 个具有投票权的主节点,那么当一个从节点收集到大于等于 N / 2 + l 张支持票时,这个从节点就会当选为新的主节点。

  • 因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有 N 个主节点进行投票,那么具有大于等于 N / 2 + l 张支持票的从节点只会有一个,这确保了新的主节点只会有一个。

  • 如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进人一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

主节点选举的过程和选举领头sentinel的过程非常相似。

数据丢失

主从复制数据丢失

主从复制之间是异步执行的,有可能master的部分数据还没来得及同步到从数据库,然后master就挂了,这时这部分未同步的数据就丢失了。

脑裂

脑裂就是说,某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着。此时哨兵可能就会认为master 宕机了,然后开启选举,将其他slave切换成了master,这个时候,集群里面就会有2个master,也就是所谓的脑裂。
此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续向旧master的写数据。
master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据将会清空,重新从新的master复制数据,导致数据丢失。

减少数据丢失的配置

min-slaves-to-writ 1
min-slaves-max-lag 10

上述配置表示,如果至少有1个从服务器超过10秒没有给自己ack消息,那么master不再执行写请求。

主从数据不一致

当从数据库因为网络原因或者执行复杂度高命令阻塞导致滞后执行同步命令,导致数据同步延迟,造成了主从数据库不一致。

都看到这了,点个赞再走了吧:)

更多编程相关知识,请访问:编程入门!!

以上是聊聊redis中的那些高可用方案!的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除