Olivine:以 Go 建構與 Redis 相容的服務 · 章節目錄

第 7 章

Day 7:資料持久化

目前的 TCP server 已支援 RESP2,以及 PING、GET、SET 指令。

但資料存在 repo.mapStorage 的記憶體中,服務重新啟動後就會全部消失。

Redis 的資料持久化

Redis 有兩種持久化方式:

  • Append Only File(AOF):以 RESP 指令記錄所有會改變儲存內容的操作。
  • RDB:記錄某個時間點的儲存快照。

我選擇先實作較簡單的 AOF,未來也許會以附錄補上 RDB。

AOF 格式

AOF 的內容是經過 RESP marshal 的原始 bytes:

$ redis-cli -p 16879 SET foo bar
OK
$ cat database.aof
*3
$3
set
$3
foo
$3
bar
$ xxd database.aof
00000000: 2a33 0d0a 2433 0d0a 7365 740d 0a24 330d  *3..$3..set..$3.
00000010: 0a66 6f6f 0d0a 2433 0d0a 6261 720d 0a    .foo..$3..bar..

當改變資料的 Redis Command 成功執行後,就將它的 RESP bytes 附加到 AOF。

AOF Reader 與 Writer

package service

import (
	"fmt"
	"olivine/pkg/resp"
	"os"
	"sync"
)

type AOF interface {
	Read() (*resp.Command, error)
	Write(*resp.Command) error
	Close() error
}

type file struct {
	f  *os.File
	rd *resp.Reader
	mu sync.Mutex
}

func (aof *file) Read() (*resp.Command, error) {
	return resp.ReadCommand(aof.rd)
}

func (aof *file) Write(v *resp.Command) error {
	aof.mu.Lock()
	defer aof.mu.Unlock()

	if !v.Dirty() {
		return nil
	}

	marshaled := v.Marshal()
	n, err := aof.f.Write(marshaled)
	if err != nil {
		return err
	}
	if n != len(marshaled) {
		return fmt.Errorf("wrote length mismatch: got %d want %d", n, len(marshaled))
	}

	return nil
}

func (aof *file) Close() error {
	if aof.f == nil {
		return nil
	}

	aof.mu.Lock()
	defer aof.mu.Unlock()

	return aof.f.Close()
}

透過 Middleware 整合 AOF

Middleware 適合 HTTP server 這類 request-response 模型。

Go 的 middleware 通常以接收 handler 的函式表示。在 internal/server/handler.go 加入 HandlerFunc 與 Middleware:

type HandlerFunc func(context.Context, *resp.Command) (resp.Value, error)
type Middleware func(HandlerFunc) HandlerFunc

func NewHandler(cmds []cmd.Command, middlewares ...Middleware) Handler {
	h := simpleHandler{}
	h.factory = make(map[string]HandlerFunc)
	for _, cmd := range cmds {
		h.factory[cmd.Command()] = cmd.Exec
	}

	h.serve = h.exec
	for i := len(middlewares) - 1; i >= 0; i-- {
		h.serve = middlewares[i](h.serve)
	}

	return &h
}

加入 AOFMiddleware constructor

在 internal/server/middleware.go 加入 NewAOFMiddleware():

package server

import (
	"context"
	"fmt"
	"olivine/internal/service"
	"olivine/pkg/resp"
)

func NewAOFMiddleware(aof service.AOF) Middleware {
	return func(next HandlerFunc) HandlerFunc {
		return func(ctx context.Context, cmd *resp.Command) (resp.Value, error) {
			ret, err := next(ctx, cmd)
			if err != nil {
				return ret, err
			}
			if err := aof.Write(cmd); err != nil {
				return nil, fmt.Errorf("%w: failed to write AOF: %w", ErrServer, err)
			}

			return ret, nil
		}
	}
}

在 App 註冊 Middleware

func NewApp() (*App, error) {
	aof, err := service.NewAOF("database.aof")
	if err != nil {
		return nil, err
	}

	return &App{
		srv: server.NewServer(server.NewHandler(cmd.NewCommands(repo.NewStorage()), server.NewAOFMiddleware(aof))),
	}, nil
}

啟動時從 AOF 還原

Restorer interface 與 aofRestorer 實作

考量未來可能加入 RDB,先定義 Restorer interface,讓 aofRestorer 和未來的 rdbRestorer 共用介面。

package server

import (
	"context"
	"errors"
	"io"
	
	"olivine/internal/service"
)

type Restorer interface {
	LoadFromDisk() error
}

func NewRestorer(aof service.AOF, handler Handler) Restorer {
	return &aofRestorer{aof: aof, handler: handler}
}

type aofRestorer struct {
	aof     service.AOF
	handler Handler
}

func (r *aofRestorer) LoadFromDisk() error {
	for {
		cmd, err := r.aof.Read()
		if err != nil {
			if errors.Is(err, io.EOF) {
				return nil
			}
			return err
		}

		if _, err := r.handler.Exec(context.Background(), cmd); err != nil {
			return err
		}
	}
}

目前 NewRestorer 需要 service.AOF 參數。理想上,它應該根據檔案 header 判斷使用 AOF 或 RDB,但目前先接受這個限制。

使用 Restorer

在 Server 套用 Restorer:

type Server interface {
	ListenAndServe() error
	RestoreFromDisk() error
}

func NewServer(handler Handler, restorer Restorer) Server {
	return &simpleSrv{
		handler:  handler,
		restorer: restorer,
	}
}

type simpleSrv struct {
	handler  Handler
	restorer Restorer

	listener net.Listener
}

func (s *simpleSrv) RestoreFromDisk() error {
	return s.restorer.LoadFromDisk()
}

再於 App 建立 Server 時註冊:

func NewApp() (*App, error) {
	aof, err := service.NewAOF(AOFPath)
	if err != nil {
		return nil, err
	}

	handler := server.NewHandler(cmd.NewCommands(repo.NewStorage()), server.NewAOFMiddleware(aof))

	return &App{
		srv: server.NewServer(
			handler,
			server.NewRestorer(aof, handler),
		),
	}, nil
}

接下來呢?

恭喜,我們已實作能處理 RESP 與部分 Redis Commands 的 TCP server。

但仍有許多工作:

  • 使用 connection context。
  • Graceful shutdown。
  • 測試(Gemini 提醒了這件事,我打算下一週補上)。
  • RDB 與 AOF rewrite。
  • Sentinel 與 cluster mode。
  • 其他功能。

希望基礎篇對你我都有幫助,進階篇再見!