iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Software Development

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

Day 14:實作命令處理 - 基礎命令與 Key-Value String 操作

  • 分享至 

  • xImage
  •  

昨天把 Command Dispatcher 接起來後,今天就來補真正會被呼叫的 command handler。先從 PINGECHO 這種基礎命令,以及 String 相關的 SETGETDELEXISTSEXPIRETTL 開始。


基礎命令的實作

這些命令不太需要碰 DB 的複雜狀態,主要用來確認連線還活著,或回傳一些最基本的資訊。

1. PING

PING 命令不帶參數時回傳 +PONG\r\n,若帶有額外參數(如 PING hello),則回傳該參數作為 Bulk String:

func pingCommand(dbEngine *db.DB, client *Client, args [][]byte) resp.Value {
	if len(args) == 0 {
		return resp.NewSimpleString("PONG")
	}
	return resp.NewBulkString(args[0])
}

2. ECHO

ECHO 接收一個參數,並直接將其回傳:

func echoCommand(dbEngine *db.DB, client *Client, args [][]byte) resp.Value {
	if len(args) != 1 {
		return resp.NewError("ERR wrong number of arguments for 'echo' command")
	}
	return resp.NewBulkString(args[0])
}

KV String 命令的整合

這些命令會直接調用我們在第二階段(Day 6 - 8)實作於 db.DB 中的thread-safe方法。

1. 寫入指令:SET (含 EX 過期參數解析)

Redis 的 SET 可以帶有 EX(秒)或是 PX(毫秒)過期引數。寫這個解析參數的時候有點雷,原本以為可以直接拿 strconv 轉 int 就好,後來想到如果要支援 PX,時間單位的換算很容易搞混,這邊處理字串要特別小心。

這裡先支援 EX <seconds>。如果參數符合格式,就轉成 Go 的 time.Duration 再交給 DB:

func setCommand(dbEngine *db.DB, client *Client, args [][]byte) resp.Value {
	if len(args) < 2 {
		return resp.NewError("ERR wrong number of arguments for 'set' command")
	}
	key := string(args[0])
	val := args[1]

	var ttl time.Duration
	// 解析 SET key value EX seconds
	if len(args) >= 4 {
		opt := strings.ToUpper(string(args[2]))
		if opt == "EX" {
			sec, err := strconv.ParseInt(string(args[3]), 10, 64)
			if err != nil || sec <= 0 {
				return resp.NewError("ERR value is not an integer or out of range")
			}
			ttl = time.Duration(sec) * time.Second
		}
	}

	dbEngine.Set(key, val, ttl)
	return resp.NewSimpleString("OK")
}

2. 讀取指令:GET

讀取 String,若 Key 不存在或類型不符,依據 RESP 規範回傳特殊的 Null Bulk String ($-1\r\n):

func getCommand(dbEngine *db.DB, client *Client, args [][]byte) resp.Value {
	if len(args) != 1 {
		return resp.NewError("ERR wrong number of arguments for 'get' command")
	}
	key := string(args[0])
	val, exists := dbEngine.GetString(key)
	if !exists {
		return resp.NewNullBulkString() // 回傳 $-1\r\n
	}
	return resp.NewBulkString(val)
}

3. 生存時間查詢:TTL

TTL 用於獲取 Key 的剩餘時間,將底層傳回的 int64 封裝為 RESP Integer 回傳:

func ttlCommand(dbEngine *db.DB, client *Client, args [][]byte) resp.Value {
	if len(args) != 1 {
		return resp.NewError("ERR wrong number of arguments for 'ttl' command")
	}
	key := string(args[0])
	res := dbEngine.TTL(key)
	return resp.NewInteger(int(res))
}

寫入命令的標記

當我們在 Dispatcher.registerAll() 中註冊這些命令時,需要明確標記寫命令(isWrite = true),以便dispatcher通知 AOF 和 Replication 機制:

func (d *Dispatcher) registerAll() {
	d.Register("PING", pingCommand, false)
	d.Register("SET", setCommand, true)    // 標記為寫指令
	d.Register("GET", getCommand, false)
	d.Register("DEL", delCommand, true)    // 標記為寫指令
	d.Register("EXPIRE", expireCommand, true) // 標記為寫指令
	d.Register("TTL", ttlCommand, false)
	// ...
}

跑起來看看

先把 Server 跑起來:

go run ./code/main.go

接著我們開終端機,用 printf + nc 來玩看看 Transaction(交易)的 MULTI / EXEC 指令:

# 開啟交易 MULTI
printf "*1\r\n\$5\r\nMULTI\r\n" | nc localhost 6379
# 預期回覆:+OK

# 放入佇列:SET foo bar
printf "*3\r\n\$3\r\nSET\r\n\$3\r\nfoo\r\n\$3\r\nbar\r\n" | nc localhost 6379
# 預期回覆:+QUEUED

# 放入佇列:GET foo
printf "*2\r\n\$3\r\nGET\r\n\$3\r\nfoo\r\n" | nc localhost 6379
# 預期回覆:+QUEUED

# 執行交易 EXEC
printf "*1\r\n\$4\r\nEXEC\r\n" | nc localhost 6379
# 預期回覆:*2\r\n+OK\r\n$3\r\nbar\r\n (所有命令的批次回覆 Array)

總結

今天把 PING/ECHO 和 String 相關指令接起來,也順手整理了 transaction 的核心流程。老實說這邊佇列邏輯剛開始寫的時候忘記把 EXEC 的鎖定跟資料庫操作包好,結果中間被其他 client 偷塞指令,果然交易機制的併發控制不能隨便寫。

明天要處理 List、Hash、Set 和 ZSet 指令,參數解析應該會寫到懷疑人生,明天見啦!


上一篇
Day 13:整合Command Dispatcher與協定整合
下一篇
Day 15:實作命令處理 - 整合 List、Hash、Set 與 ZSet 命令
系列文
手刻 Redis:用 Go 從零打造高效能高併發的記憶體資料庫16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言