iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Software Development

手刻 Redis:用 Go 從零打造高效能高併發的記憶體資料庫系列 第 27

Day 27:實作主從複製 - 從節點背景同步 goroutine與 RDB 全量加載

  • 分享至 

  • xImage
  •  

昨天先把 SYNC 流程摸熟,今天開始寫從節點的背景同步 goroutine。目標是讓它可以自己連上主節點、送出 SYNC、讀取 RDB 快照,再把本機資料恢復出來。


從節點同步流程的實作

當從節點收到 SLAVEOF host port 指令後,就要切換同步狀態。我把這段非同步流程放在 code/server/replication.go

1. 主從狀態切換與goroutine喚起

我們在 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
}

2. 從節點背景同步: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. 進入增量同步接收迴圈 ...
}

為什麼將 RDB 包裝在 RESP Bulk String 中傳輸?

在 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 快照,以及後續寫指令要怎麼廣播出去,主從複製快收尾了。


上一篇
Day 26:Redis 主從複製(Replication)原理解析與 SYNC 協定
下一篇
Day 28:實作主從複製 - 增量複製流傳播與寫指令廣播
系列文
手刻 Redis:用 Go 從零打造高效能高併發的記憶體資料庫30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言