前幾天我弄好了 AOF 追加日誌。AOF 雖然資料安全,但缺點就是檔案有夠大,資料一多,重啟時 replay 指令會跑到讓人想睡覺。
所以 Redis 還有另一招:RDB (Redis Database) 快照。
今天來拆 RDB 的原理,順便設計一套自己的二進位快照格式。
RDB 持久化可以想成幫記憶體資料庫拍一張快照,把某個時間點的資料狀態寫成二進位檔案(例如 dump.rdb)。
AOF 日誌 (像指令重播記錄): "SET a 1", "DEL a", "SET a 2"
RDB 快照 (像全景照片): "a = 2"
| 特性 | RDB 快照 | AOF 日誌 |
|---|---|---|
| 檔案體積 | 通常較小 (二進位格式,適合備份與傳輸) | 較大 (純文字指令追加) |
| 恢復速度 | 極快 (直接加載二進位結構到記憶體) | 慢 (需要依序重跑指令) |
| 資料安全性 | 較低 (若當機,會丟失最後一次快照之後的資料) | 高 (可配置為每秒 fsync) |
實務上通常會把兩者搭在一起用:
在 Go 裡要先做一版二進位快照,不一定要自己手排 struct bytes。標準庫的 encoding/gob (Go Binary) 已經能處理不少序列化工作。
encoding/gob?由於我們的 KV 引擎底層的 entry.val 儲存的是 interface{}(可以是 List 雙向鏈結、Set 的空 map、跳躍表等),Gob 預設無法序列化複雜的指針鏈結。
為此,我們在寫入前,先將其轉換為平面化的二進位安全結構(如 List 轉為 [][]byte,ZSet 轉為 []ZSetMember),然後再使用 Gob 加密:
老實說,gob 序列化剛開始讓我很頭痛,interface{} 裡塞 map 不先 register 就直接 panic,查了半天才知道要先處理這塊。
type rdbEntry struct {
DataType DataType
Val interface{}
ExpireAt time.Time
}
為了讓 Gob 識別這些 interface{} 中包裹的具體型態,我們在 code/db/rdb.go 中註冊了它們:
func init() {
gob.Register([]byte{})
gob.Register([][]byte{})
gob.Register(map[string][]byte{})
gob.Register([]string{})
gob.Register([]ZSetMember{})
}
這層轉換很重要,因為雙向鏈結和跳躍表這種指標結構不適合直接序列化。先攤平成 slice 或 map,再交給 Gob,會安全很多。
來測一下我們的 RDB 快照能不能正常產出:
# 在 redis-cli 塞點資料並手動觸發 BGSAVE
$ redis-cli
> SET hero "batman"
# 預期回覆:OK
> BGSAVE
# 預期回覆:Background saving started
這時候回終端機看目錄,應該會多出一個 dump.rdb:
$ ls -lh dump.rdb
-rw-r--r-- 1 sky staff 84B Aug 13 22:45 dump.rdb
# 用 xxd 看一下二進位長怎樣,gob 序列化的痕跡都在這
$ xxd dump.rdb | head -n 3
00000000: 476f 620a 1f23 0401 02ff 8200 0102 0110 Gob..#..........
00000010: 0000 37ff 8103 0101 0852 6462 456e 7472 ..7......RdbEntr
00000020: 7901 02ff 8200 0104 0108 4461 7461 5479 y.........DataTy
今天先把 RDB 和 AOF 的差異釐清,也決定用 encoding/gob 做第一版 RDB 快照模型。
明天來處理 RDB 的 Save/Load,還要補啟動時自動判斷 AOF/RDB 的流程,這塊寫起來應該滿好玩的。