iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0
Software Development

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

Day 28:實作主從複製 - 增量複製流傳播與寫指令廣播

  • 分享至 

  • xImage
  •  

從節點全量同步跑通後,今天接著處理主從複製的第二段:增量複製流傳播(Replication Stream Propagation)

主要工作是讓主節點能處理 SYNC、管好從節點連線,而且一有寫操作成功,就馬上廣播給所有小弟。


1. 主節點處理 SYNC 命令

主節點(Master)收到 SYNC 命令時,大致要做這幾件事:

  1. 生成快照:呼叫 SaveRDB("dump_sync.rdb") 將記憶體資料打包保存。
  2. 連線升級:將從節點的 TCP 連線(client.Conn)加入主節點內部的從節點連線集合中。
  3. 發送快照:將 RDB 快照做為 Bulk String 回傳給從節點。

我們在 code/server/replication.go 中實現了此邏輯:

func (s *Server) handleSyncCommand(dbEngine *db.DB, client *command.Client, args [][]byte) resp.Value {
	log.Printf("收到來自從節點 %s 的 SYNC 請求", client.Conn.RemoteAddr().String())

	// 1. 同步生成 RDB 快照到檔案中
	rdbFilename := "dump_sync.rdb"
	_ = dbEngine.SaveRDB(rdbFilename)
	defer os.Remove(rdbFilename)

	// 2. 讀取 RDB 檔案內容
	rdbBytes, _ := os.ReadFile(rdbFilename)

	// 3. 將 client 的連線加入 replicas 列表,以便後續的寫命令廣播
	s.replicaMu.Lock()
	s.replicas[client.Conn] = struct{}{}
	s.replicaMu.Unlock()

	// 4. 回傳 RDB 快照二進位數據當作 Bulk String
	return resp.NewBulkString(rdbBytes)
}

2. 寫指令實時傳播(Propagate)

主節點如何將後續的寫指令同步給從節點?
這邊我原本想另外開一個 channel 把指令丟過去,後來發現可以直接沿用 Day 13 在 Dispatcher 寫的 AofWriteCallback,一石二鳥,真的是意外之喜。當 Dispatcher 成功執行了一個寫操作命令時,除了寫入 AOF,我們也會在此回呼中調用 s.PropagateToReplicas(val)

// 在 server.go 中初始化時:
	dispatcher.SetAofWriteCallback(func(val resp.Value) {
		// 1. 追加寫入 AOF
		if s.aofLogger != nil { _ = s.aofLogger.Write(val) }
		// 2. 同時將寫指令傳播給所有從節點
		s.PropagateToReplicas(val)
	})

PropagateToReplicas 會遍歷所有活躍的從節點連線,將寫指令以標準的 RESP 格式寫入其 socket:

func (s *Server) PropagateToReplicas(val resp.Value) {
	s.replicaMu.Lock()
	defer s.replicaMu.Unlock()

	if len(s.replicas) == 0 { return }

	bytesToSend := val.Marshal()
	for replica := range s.replicas {
		_, err := replica.Write(bytesToSend) // 發送增量寫指令
		if err != nil {
			// 連線中斷,將該從節點移除
			replica.Close()
			delete(s.replicas, replica)
		}
	}
}

3. 從節點接收並重播

全量同步完成後,從節點的 runReplica 會在背景持續讀主節點送來的資料。因為送來的是標準 RESP 寫指令(例如 SET),從節點只要讀出來,再呼叫 s.dispatcher.Dispatch(dummyClient, cmdVal) 重播即可。

	// 在 runReplica 背景 goroutine 中:
	for {
		cmdVal, err := reader.ReadValue()
		if err != nil { break }
		s.dispatcher.Dispatch(dummyClient, cmdVal) // 執行重播
	}

跑起來看看

寫了這麼久,來跑個效能測試看看 QPS 能不能看:

# 使用 redis-benchmark 壓測 10 萬次請求,50 個併發,安靜模式 (-q)
$ redis-benchmark -p 6379 -n 100000 -c 50 -q
# 預期回覆:
# PING_INLINE: 98230.12 requests per second, p50=0.250 msec
# SET: 85410.60 requests per second, p50=0.350 msec
# GET: 89120.45 requests per second, p50=0.320 msec

沒有做特別極限的優化,還能跑到接近 9 萬 QPS,這個結果比我預期好。


總結

今天把主從複製最後一塊補上了:主節點能發快照,也能把後續寫指令廣播給從節點。

明天要幫這個裸奔的資料庫加點防護,補 AUTH 密碼校驗和 CLIENT 管理。不然真拿去上線,大概很快就會被當成礦機。


上一篇
Day 27:實作主從複製 - 從節點背景同步 goroutine與 RDB 全量加載
下一篇
Day 29:AUTH 安全密碼校驗與 CLIENT 連線管理機制實作
系列文
手刻 Redis:用 Go 從零打造高效能高併發的記憶體資料庫30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言