iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0

灌水了 11 天的文,我們終於可以進到整個 Redis 最核心的功能:鍵值存儲。

資料儲存

在實際設計 GET 與 SET 時,我們應該要先決定資料如何被儲存。

repo.(Storage) 介面

建立一個 repo.(Storage) 介面,並提供所需的函式:

// file: ./internal/repo/storage.go
type Storage interface {
	Set(ctx context.Context, k, v string) error
	Get(ctx context.Context, k string) (string, error)
}

雖然目前 context.Context 還沒有作用,但是我們可以預期這兩個函式將會被 goroutine 多次調用,在這個地方留下 context.Context 是個良好的開發習慣。

repo.(*mapStorage) 實作

在不考慮資料持久化的前提之下,其實 Go 早就內建了一個鍵值資料型態:map

註:事實上,Go 在 1.24 之後 map 底層用了 Swiss Table,這是一個經過 SIMD 及分支預測最佳化的資料結構,並且有著非常優異的效能足以應付絕大多數場景,可以參閱 Faster Go maps with Swiss Tables 這篇文章。

建立 repo.(*mapStorage)

// file: ./internal/repo/storage.go
type mapStorage struct {
	storage map[string]string
	mu      sync.RWMutex
}

func (s *mapStorage) Set(_ context.Context, k, v string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	s.storage[k] = v
	return nil
}

func (s *mapStorage) Get(_ context.Context, k string) (v string, err error) {
	s.mu.RLock()
	defer s.mu.RUnlock()

	var ok bool
	if v, ok = s.storage[k]; !ok {
		return "", ErrNotFound
	}

	return
}

Go 的 map 並不是 Concurrency Safe(並發安全) 的資料結構,如果有兩個 goroutine 同時對 map 進行操作會觸發 panic,因此我們必須使用 sync.RWMutex 在存取時鎖定資源。

套用 repo.(*mapStorage)

建立 repo.NewStorage(),它可以初始化相關資源:

// file: ./internal/repo/storage.go
func NewStorage() Storage {
	s := mapStorage{}
	s.storage = make(map[string]string)

	return &s
}

然後將 repo.Storage 作為 cmd.NewCommands() 的參數:

// file: ./internal/service/cmd/cmd.go
func NewCommands(storage repo.Storage) []Command {
	return []Command{
		&PING{},
	}
}

加入指令

GET 指令

file: ./internal/service/cmd/get.go
package cmd

import (
	"context"
	"errors"
	"fmt"
	
	"olivine/internal/repo"
	"olivine/pkg/resp"
)

type Get struct {
	storage repo.Storage
}

func NewGet(storage repo.Storage) *Get {
	return &Get{
		storage: storage,
	}
}

func (c *Get) Command() string {
	return "GET"
}

func (c *Get) Exec(ctx context.Context, cmd *resp.Command) (resp.Value, error) {
	args := cmd.Args()
	if len(args) != 1 {
		return nil, fmt.Errorf("%w: argument count mismatch: expect '%d' got '%d'", ErrValidation, len(args), 1)
	}

	k := args[0]
	v, err := c.storage.Get(ctx, k.String())
	if err != nil {
		if errors.Is(err, repo.ErrNotFound) {
			return resp.NewNullBulkString(), nil
		}
		return nil, fmt.Errorf("%w: %w", ErrStorage, err)
	}

	return resp.NewBulkString(v), nil
}

SET 指令

原始的 Redis SET 指令其實非常繁瑣,它具有各種不同的判斷功能。我們此處先不引入這些功能,避免過高的複雜度,單純專注在功能本身。

file: ./internal/service/cmd/set.go
package cmd

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

type Set struct {
	storage repo.Storage
}

func NewSet(storage repo.Storage) *Set {
	return &Set{storage: storage}
}

func (c *Set) Command() string {
	return "SET"
}

func (c *Set) Exec(ctx context.Context, cmd *resp.Command) (resp.Value, error) {
	args := cmd.Args()
	if len(args) != 2 {
		return nil, fmt.Errorf("%w: argument count mismatch: expect '%d' got '%d'", ErrValidation, len(args), 2)
	}

	k, v := args[0], args[1]
	if err := c.storage.Set(ctx, k.String(), v.String()); err != nil {
		return nil, fmt.Errorf("%w: %w", ErrStorage, err)
	}

	return resp.SimpleString("OK"), nil
}

加入指令集

最後將這兩個指令都注入 cmd.NewCommands() 中即可

// file: ./internal/service/cmd/cmd.go
func NewCommands(storage repo.Storage) []Command {
	return []Command{
		&Ping{},

		NewSet(storage),
		NewGet(storage),
	}
}

上一篇
[Day 11] Redis 指令與 PING 實作
下一篇
[Day 13] 資料持久化(上):AOF 的實現
系列文
從零開始打造 Redis:以 Go 建立 Production Ready 應用程式 15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言