Redis 作為一個資料庫,其最重要的特性就是_ Data Persistence(資料持久化)_:當用戶儲存了一個值之後,在下次重新啟動服務這個值也應該是有效的。因此,我們不能僅僅將資料存在記憶體中,它應該要被寫入硬碟。
Redis 有兩種資料持久化策略:
我們在這邊先實現 AOF 的方案,因為它比較容易實作。
註:RDB 的部份其實滿困難的,因為 RDB 有很多特性是其於 Redis 內部的記憶體排佈方式,這與 Go 的記憶體排佈方式有著巨大差異,雖然可以硬性支援但在效能上與易用性上會與官方版本存在巨大落差。
其實 AOF 就是由一大堆的 RESP Value 經過 Marshal() 後的產物:
$ redis-cli SET foo bar
OK
$ cat database.aof
*3
$3
set
$3
foo
$3
bar
因此,實作上這個邏輯很簡單:只要 storage 成功儲存資料,就把 RESP 原封不動地塞進 AOF 即可。
可以在 ./internal/service 底下建立 AOF 相關的介面,這邊一樣使用介面是因為未來也可以抽換成其它的 AOF 實作,例如像是 ledisdb/ledisdb 中就整合了許多不同的存儲後端。
// file: ./internal/service/aof.go
type AOF interface {
Read() (*resp.Command, error)
Write(*resp.Command) error
Close() error
}
並且用 os.(*File) 實現:
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()
}
為了判斷指令是不是應該被寫入 AOF(只有會造成資源變動的指令才應該被寫入 AOF,例如像 SET),因此我在 resp.(*Command) 加入 IsDirty() 來判斷它是不是 SET 指令。