昨天先把 SYNC 流程摸熟,今天開始寫從節點的背景同步 goroutine。目標是讓它可以自己連上主節點、送出 SYNC、讀取 RDB 快照,再把本機資料恢復出來。
當從節點收到 SLAVEOF host port 指令後,就要切換同步狀態。我把這段非同步流程放在 code/server/replication.go。
我們在 SetSlaveOf 中先關閉舊的同步 goroutine,並啟動 runReplica:
func (s *Server) SetSlaveOf(host, port string) error {
s.mu.Lock()
// 1. 如果有舊的同步 goroutine,先關閉它以中斷舊連線
if s.slaveQuit != nil {
close(s.slaveQuit)
s.slaveQuit = nil
}
if host == "NO" && port == "ONE" {
s.isSlave = false
s.masterAddr = ""
s.mu.Unlock()
return nil
}
s.isSlave = true
s.masterAddr = host + ":" + port
s.slaveQuit = make(chan struct{})
quitChan := s.slaveQuit
s.mu.Unlock()
// 2. 啟動背景同步 goroutine
go s.runReplica(quitChan)
return nil
}
runReplica這個goroutine負責連到主節點、送出 SYNC,接著讀取主節點回傳的第一包 RDB 資料:
func (s *Server) runReplica(quit chan struct{}) {
s.mu.Lock()
masterAddr := s.masterAddr
s.mu.Unlock()
// 1. 連線到主節點
conn, err := net.Dial("tcp", masterAddr)
if err != nil { return }
defer conn.Close()
// 2. 發送 SYNC 命令
syncCmd := resp.NewArray([]resp.Value{
resp.NewBulkString([]byte("SYNC")),
})
_, _ = conn.Write(syncCmd.Marshal())
reader := resp.NewReader(conn)
// 3. 讀取主節點回傳的第一個資料包(預期為 RDB 二進位快照,包裝在 Bulk String 中)
rdbVal, err := reader.ReadValue()
if err != nil || rdbVal.Type != resp.TypeBulkString {
return
}
// 4. 將 RDB 二進位數據寫入本機臨時檔案,並調用引擎的 LoadRDB 加載資料
tempRdbFile := "temp_replica.rdb"
_ = os.WriteFile(tempRdbFile, rdbVal.Bulk, 0644)
defer os.Remove(tempRdbFile)
err = s.dbEngine.LoadRDB(tempRdbFile) // 還原為記憶體結構
if err != nil {
return
}
log.Println("[Replica] RDB 快照加載成功,完成全量同步")
// 5. 進入增量同步接收迴圈 ...
}
在 Redis 官方的 SYNC 協定中,Master 生成 RDB 後,傳輸時會使用一種特殊的「Raw二進位傳輸協定」:先發送一個帶有長度前綴的二進位標頭,然後直接傳送二進位位元組,這不屬於標準的 RESP 格式。
在這個專案裡,我不想為 SYNC 另外寫一套私有二進位 parser,所以改成:主節點把 RDB 二進位資料包成標準 RESP Bulk String 回傳給從節點。
Day 5 寫的 resp.Reader 本來就能處理 Bulk String 的二進位安全讀取:先解析長度,再用 io.ReadFull 讀固定byte 數,最後吃掉 CRLF。拿它來讀 RDB bytes 剛好合適。
老實說,本來我想自己刻一個專門解 RDB binary stream的 parser,結果寫到一半發現直接塞進 RESP Bulk String 裡送過來不就得了,真的是走了不少冤枉路。
我們來驗證一下指令dispatcher是不是所有功能都接通了,來個全餐測試:
$ redis-cli
> SET user:1 "sky"
# 預期回覆:OK
> GET user:1
# 預期回覆:"sky"
> ZADD scores 100 "alice" 90 "bob"
# 預期回覆:(integer) 2
> ZRANGE scores 0 -1
# 預期回覆:
# 1) "bob"
# 2) "alice"
從基本的字串到複雜的跳躍表,所有的指令都能順利執行,dispatcher全通!
今天把從節點連線和全量載入流程先跑通。用 RESP Bulk String 傳 RDB,真的省掉不少額外 parser 的麻煩。
明天要處理主節點怎麼發送 SYNC 快照,以及後續寫指令要怎麼廣播出去,主從複製快收尾了。