基於前面介紹的 Redis 內容,Redis 只能做爲一個單機內存數據庫,一旦服務器宕機即不能提供服務,即使能經過持久化策略重啓恢復數據,每每也作不到百分之百還原。再一個就是,單機的 Redis 須要處理全部的客戶端請求,包括讀和寫操做,壓力很大。java
說了這麼多,Redis 固然也提供瞭解決方案,主從複製技術是實現 Redis 集羣的最基本架構,集羣中有一臺或多臺主節點服務器(master),多臺從節點服務器(slave),slave 持續不斷的同步 master 上的數據。一旦 master 宕機,咱們能夠切換 salve 成爲新的 master 穩定提供服務,也不用擔憂 master 宕機致使的數據丟失。git
下面咱們就一塊兒來看看主從複製技術的設計與應用,先看理論再看源碼實現。程序員
主從複製技術有兩個版本,2.8 之前的版本,設計上有缺陷,在 slave 斷線後重連依然須要 master 從新發送 RDB 從新進行數據更新,效率很是低。2.8 版本之後作了從新設計,經過引入偏移量同步,相對而言很是的高效,咱們這裏不去討論舊版本的設計了,直接看新版本的主從複製技術設計。github
每個 Redis 啓動後,都會認爲本身是一個 master 節點,你能夠經過如下命令通知它成爲 slave 並向 master 同步數據:redis
slaveof [masterip] [masterport]
複製代碼
另外一種方式就是在 Redis 啓動配置文件中直接指明讓它做爲一個 slave 節點啓動,並在啓動後同步 master 節點數據。配置項和命令是同樣的。數據庫
若是 master 配置了密碼鏈接,那麼還須要在 slave 的配置文件中指明 master 的鏈接密碼:bash
masterauth <password>
複製代碼
除此以外,salve 節點默認是隻讀的,不容許寫入數據,由於若是支持寫入數據,那麼與 master 就沒法保持數據一致性,因此咱們通常會把 slave 節點做爲讀寫分離中讀服務提供者。固然,你也能夠修改是否容許 slave 寫入數據:服務器
slave-read-only yes/no
複製代碼
固然若是你的 master 宕機了,你須要把某個 slave 上線成 master,你能夠經過命令取消 slave 的數據同步,成爲單獨的一個 master:微信
slaveof no one
複製代碼
slave 同步 master 的數據主要分爲兩個大步驟,全量複製和部分複製。當咱們執行 slaveof 命令的時候,咱們的 slave 會做爲一個客戶端鏈接上 master 並向 master 發送 PSYNC 命令。markdown
master 收到命令後,會調用 bgsave fork 一個後臺子進程生產 RDB 文件,待合適的時候,在 serverCron 循環的時候發送給 slave節點。
slave 收到 RDB 文件後,丟棄目前內存中全部的數據並阻塞本身,專心作 RDB 讀取,數據恢復。
以上就是主從複製的一個全量複製的大概流程,可是一次全量複製並不能永遠的保持主從節點數據一致,master 還須要將實時的修改命令同步到從節點才行,這就是部分複製。
在介紹部分複製以前,這裏先介紹幾個概念。第一個是複製緩衝區(repl_backlog),這是一個 FIFO 的隊列,裏面存的是最近的一些寫命令,大小默認在 1M,複製偏移量(offset),這個偏移量實際上是對應複製緩衝區中的字符偏移。複製緩衝區的結構大體是這樣的:
在主從節點完成第一輪全量複製之後,主從節點之間已經初步實現了數據同步,日後的 master,會將收到的每一條寫命令發送給 slave 並 添加到複製緩衝區並根據字節數計算更新本身的偏移量,slave 收到傳輸過來的命令後也同樣更新本身的偏移量。
這樣,只要主從節點的偏移量相同就說明主從節點之間的數據是同步的。複製緩衝區大小是固定的,新的寫命令進來之後,舊的數據就會出隊列。若是某個 slave 斷線重連以後,依然向 master 發送 PSYNC 命令並攜帶本身的偏移量,master 判斷該偏移量是否還在緩衝區區間內,若是在則直接將該偏移量日後的全部偏移量對應的命令發送給 slave,無需從新進行全量複製。
這是新版同步複製的一個優化的設計,若是該斷線重連的 slave 的偏移量已經不在緩衝區區間內,那麼說明 master 可能已經沒法找到自上次斷線後的完整更新記錄了,因而進行全量複製並將最新的偏移量發到 slave,算是完成了新的數據同步。
這就是主從複製的一個完整的設計邏輯,設計思路很是的優秀,很值得咱們借鑑,下面咱們看源碼的一些實現狀況。
serverCron 定時函數中有這麼一段代碼:
run_with_period(1000) replicationCron();
複製代碼
按照默認的 server.hz 配置,每秒就須要執行一次 replicationCron。咱們就來看看這個方法究竟作了什麼。
void replicationCron(void) { static long long replication_cron_loops = 0; //slave 鏈接 master 超時,取消鏈接 if (server.masterhost && (server.repl_state == REPL_STATE_CONNECTING || slaveIsInHandshakeState()) && (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout) { serverLog(LL_WARNING,"Timeout connecting to the MASTER..."); cancelReplicationHandshake(); } //.rdb 文件響應超時,取消鏈接 if (server.masterhost && server.repl_state == REPL_STATE_TRANSFER && (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout) { serverLog(LL_WARNING,"Timeout receiving bulk data from MASTER... If the problem persists try to set the 'repl-timeout' parameter in redis.conf to a larger value."); cancelReplicationHandshake(); } //已經創建鏈接的狀況下,某個操做超時,斷開鏈接 if (server.masterhost && server.repl_state == REPL_STATE_CONNECTED && (time(NULL)-server.master->lastinteraction) > server.repl_timeout) { serverLog(LL_WARNING,"MASTER timeout: no data nor PING received..."); freeClient(server.master); } //檢查配置項,是否須要向 master 發起鏈接 if (server.repl_state == REPL_STATE_CONNECT) { serverLog(LL_NOTICE,"Connecting to MASTER %s:%d", server.masterhost, server.masterport); if (connectWithMaster() == C_OK) { serverLog(LL_NOTICE,"MASTER <-> SLAVE sync started"); } } //向 master 發送本身的偏移量 //master 判斷是否須要進行命令傳播給 slave if (server.masterhost && server.master && !(server.master->flags & CLIENT_PRE_PSYNC)) replicationSendAck(); 。。。。。。。 } 複製代碼
由於無論 master 仍是 slave,都是一個服務端的 Redis 程序,他們既能夠成爲主節點,又能夠成爲從節點。以上的代碼段是當前 redis 做爲一個 slave 時須要作的操做,replicationCron 後面的代碼是當前 redis 做爲一個主節點須要作的處理邏輯。
void replicationCron(void) { 。。。。。 listIter li; listNode *ln; robj *ping_argv[1]; //給全部的 slave 發送 ping if ((replication_cron_loops % server.repl_ping_slave_period) == 0 && listLength(server.slaves)) { ping_argv[0] = createStringObject("PING",4); replicationFeedSlaves(server.slaves, server.slaveseldb, ping_argv, 1); decrRefCount(ping_argv[0]); } //發送 '\n' 給全部正在等待 rdb 文件的 slave,防止他們斷定 master 超時 listRewind(server.slaves,&li); while((ln = listNext(&li))) { client *slave = ln->value; int is_presync = (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_START || (slave->replstate == SLAVE_STATE_WAIT_BGSAVE_END && server.rdb_child_type != RDB_CHILD_TYPE_SOCKET)); if (is_presync) { if (write(slave->fd, "\n", 1) == -1) { /* Don't worry about socket errors, it's just a ping. */ } } } //全部的 slave 並斷開全部超時的 slave if (listLength(server.slaves)) { listIter li; listNode *ln; listRewind(server.slaves,&li); while((ln = listNext(&li))) { client *slave = ln->value; if (slave->replstate != SLAVE_STATE_ONLINE) continue; if (slave->flags & CLIENT_PRE_PSYNC) continue; if ((server.unixtime - slave->repl_ack_time) > server.repl_timeout) { serverLog(LL_WARNING, "Disconnecting timedout slave: %s", replicationGetSlaveName(slave)); freeClient(slave); } } } //在沒有 slave 節點鏈接後的 N 秒,釋放複製緩衝區 if (listLength(server.slaves) == 0 && server.repl_backlog_time_limit && server.repl_backlog && server.masterhost == NULL) { time_t idle = server.unixtime - server.repl_no_slaves_since; if (idle > server.repl_backlog_time_limit) { changeReplicationId(); clearReplicationId2(); freeReplicationBacklog(); serverLog(LL_NOTICE, "Replication backlog freed after %d seconds " "without connected slaves.", (int) server.repl_backlog_time_limit); } } 。。。。。。 } 複製代碼
總結一下,replicationCron 默認每一秒調用一次,分爲兩個部分,本身若是是 slave 節點的話,那麼會判斷與 master 之間的鏈接狀況,若是等待 rdb 超時或其餘鏈接超時,那麼 slave 會斷開與 master 的鏈接,若是發現配置文件中配置了 slaveof ,則會主動鏈接 master 發送 PSYNC 命令而且會發送本身的偏移量,期待 master 向本身傳播命令。
若是本身是一個 master 的話,它會首先向全部的 slave 發送 ping,以避免 slave 由於超時斷開與本身的鏈接,而且還會主動斷開一些超時鏈接的 slave。
除此以外咱們須要補充一點的就是 redis 中很是重要的函數調用 call 函數,這個函數是全部命令對應的實現函數的前置調用。這個函數的具體邏輯我這裏暫時不去詳細介紹,可是其中有兩個重要的步驟你須要明確,一是會調用執行命令的實現函數,二是會將修改命令添加到 AOF 文件並傳播給全部的 slave 節點。
這樣,咱們關於主從複製的完整邏輯就基本解釋通了,以上還只是一個基本的雛形,後面咱們還將基於此介紹高可用的主從複製,藉助哨兵(Sentinel)完成主從節點的高可用切換,故障轉移等等,敬請期待~
關注公衆不迷路,一個愛分享的程序員。 公衆號回覆「1024」加做者微信一塊兒探討學習! 每篇文章用到的全部案例代碼素材都會上傳我我的 github github.com/SingleYam/o… 歡迎來踩!