iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
Software Development

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

Day 15:實作命令處理 - 整合 List、Hash、Set 與 ZSet 命令

  • 分享至 

  • xImage
  •  

String 指令跑通後,今天換把 List、Hash、Set 和 ZSet 這四個比較麻煩的結構接到 code/command/commands.go

因為這些指令通常帶很多參數,回覆格式也多是陣列,照著 RESP 規範封裝起來會比較費工。


1. List 命令實作

List 命令包含推入(LPUSH, RPUSH)、彈出(LPOP, RPOP)與範圍讀取(LRANGE)。

推入與彈出

推入方法支援多個 value 同時推入,回傳操作後 List 的長度。彈出若為空回傳 Null Bulk String:

func lpushCommand(dbEngine *db.DB, client *Client, args [][]byte) resp.Value {
	if len(args) < 2 {
		return resp.NewError("ERR wrong number of arguments for 'lpush' command")
	}
	key := string(args[0])
	vals := args[1:]
	newLen, err := dbEngine.LPush(key, vals...)
	if err != nil {
		return resp.NewError(err.Error())
	}
	return resp.NewInteger(newLen)
}

範圍查詢:LRANGE

LRANGE 回傳的是 RESP Array,所以底層拿到的 [][]byte 要一個一個包成 Value

func lrangeCommand(dbEngine *db.DB, client *Client, args [][]byte) resp.Value {
	// ... 參數個數與 strconv 轉換 ...
	res, err := dbEngine.LRange(key, start, stop)
	if err != nil {
		return resp.NewError(err.Error())
	}
	
	respVals := make([]resp.Value, len(res))
	for i, val := range res {
		respVals[i] = resp.NewBulkString(val)
	}
	return resp.NewArray(respVals) // 封裝為 RESP Array 回傳
}

2. Hash 命令實作

Hash 包含欄位的設定、讀取與刪除。

  • HSET:新欄位回傳 1,更新已存在欄位回傳 0。
  • HGET:回傳欄位內容,若不存在回傳 Null Bulk String。
  • HDEL:傳入一或多個 field 進行刪除,回傳成功刪除的個數。
func hsetCommand(dbEngine *db.DB, client *Client, args [][]byte) resp.Value {
	if len(args) != 3 {
		return resp.NewError("ERR wrong number of arguments for 'hset' command")
	}
	key := string(args[0])
	field := string(args[1])
	val := args[2]

	res, err := dbEngine.HSet(key, field, val)
	if err != nil { return resp.NewError(err.Error()) }
	return resp.NewInteger(res)
}

3. Set 命令實作

Set 包含無序集合的添加(SADD)、刪除(SREM)與獲取所有成員(SMEMBERS):

func smembersCommand(dbEngine *db.DB, client *Client, args [][]byte) resp.Value {
	if len(args) != 1 {
		return resp.NewError("ERR wrong number of arguments for 'smembers' command")
	}
	key := string(args[0])
	members, err := dbEngine.SMembers(key)
	if err != nil { return resp.NewError(err.Error()) }

	respVals := make([]resp.Value, len(members))
	for i, m := range members {
		respVals[i] = resp.NewBulkString([]byte(m))
	}
	return resp.NewArray(respVals)
}

4. Sorted Set (ZSet) 命令實作

ZSet 主要實作 ZADDZRANGE

區間查詢:ZRANGE (支援 WITHSCORES 參數)

實作 ZRANGEWITHSCORES 時超級卡,因為 RESP 規範裡要把 Member 跟 Score 平攤在同一個 Array 裡交替出現。我一開始直接傳 map 回去,結果 redis-cli 根本看不懂,只好老老實實把它打平。

如果客戶端發送 ZRANGE key start stop WITHSCORES,我要交替排列回傳成員分數(以字串 Bulk 表示)

func zrangeCommand(dbEngine *db.DB, client *Client, args [][]byte) resp.Value {
	// ... 參數解析 ...
	withScores := false
	if len(args) >= 4 && strings.ToUpper(string(args[3])) == "WITHSCORES" {
		withScores = true
	}

	res, err := dbEngine.ZRange(key, start, stop)
	if err != nil { return resp.NewError(err.Error()) }

	var respVals []resp.Value
	if withScores {
		respVals = make([]resp.Value, 0, len(res)*2)
		for _, member := range res {
			respVals = append(respVals, resp.NewBulkString([]byte(member.Member)))
			respVals = append(respVals, resp.NewBulkString([]byte(strconv.FormatFloat(member.Score, 'f', -1, 64))))
		}
	} else {
		respVals = make([]resp.Value, len(res))
		for i, member := range res {
			respVals[i] = resp.NewBulkString([]byte(member.Member))
		}
	}
	return resp.NewArray(respVals)
}

跑起來看看

先把 Server 跑起來:

go run ./code/main.go

順便測一下 Pipeline (管線化)。做法是一次用 printf 送多個 RESP 命令,讓它們一起進 TCP 連線,減少一問一答的網路等待時間:

# 一次送出三個命令:
# PING
# SET pipe_key 123
# GET pipe_key
printf "*1\r\n\$4\r\nPING\r\n*3\r\n\$3\r\nSET\r\n\$8\r\npipe_key\r\n\$3\r\n123\r\n*2\r\n\$3\r\nGET\r\n\$8\r\npipe_key\r\n" | nc localhost 6379

# 預期回覆:
# +PONG
# +OK
# $3
# 123

Server 會照順序處理這些命令,再把結果依序回來。批量操作時,這個差異會很明顯。


總結

今天把幾個複雜資料結構的指令路由都串好了,Redis Clone 開始有比較完整的資料結構支援。說實在的,處理 RESP 陣列的巢狀結構時,一開始沒考慮好邊界條件,還不小心 panic 過幾次,還好有靠單元測試抓出來。

明天要寫 Redis 很有趣的特性:事務機制(MULTI, EXEC, DISCARD)。又要開始處理排隊邏輯了,大家明天見!


上一篇
Day 14:實作命令處理 - 基礎命令與 Key-Value String 操作
下一篇
Day 16:Redis 事務(Transaction)原理與 MULTI, EXEC, DISCARD 實作
系列文
手刻 Redis:用 Go 從零打造高效能高併發的記憶體資料庫16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言