昨天把 Command Dispatcher 接起來後,今天就來補真正會被呼叫的 command handler。先從 PING、ECHO 這種基礎命令,以及 String 相關的 SET、GET、DEL、EXISTS、EXPIRE、TTL 開始。
這些命令不太需要碰 DB 的複雜狀態,主要用來確認連線還活著,或回傳一些最基本的資訊。
PINGPING 命令不帶參數時回傳 +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])
}
ECHOECHO 接收一個參數,並直接將其回傳:
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])
}
這些命令會直接調用我們在第二階段(Day 6 - 8)實作於 db.DB 中的thread-safe方法。
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")
}
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)
}
TTLTTL 用於獲取 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 指令,參數解析應該會寫到懷疑人生,明天見啦!