实现 List 结构
本文进度对应的代码仓库:List
在前面的章节中,我们从 0 开始,实现了一个具备集群功能、可以持久化的 Redis 服务器。但是之前我们只实现了字符串类型,并没有实现其他的数据类型。
从这一章节开始,我们将逐步实现其他的数据类型。
我们先删除 redis.conf
文件中的集群相关配置,然后重新启动服务器,以单例模式启动服务器。
介绍
List 是 Redis 中最基础的数据结构之一,它是一个有序的字符串集合。
它支持的操作有:
LPUSH
:将一个或多个值插入到列表的头部RPUSH
:将一个或多个值插入到列表的尾部LPOP
:移除并返回列表的第一个元素RPOP
:移除并返回列表的最后一个元素LRANGE
:返回列表中指定区间内的元素LLEN
:返回列表的长度LINDEX
:返回列表中指定索引的元素LSET
:设置列表中指定索引的元素的值
实现
我们对 List 结构的实现,我们使用 Go 语言的标准库中的 container/list
包。
这个包提供了一个双向链表,我们可以使用它来实现 List 结构。
我们在 database/list.go
文件中实现了 List 结构的相关操作。
database/list.go
package database
import (
// Use the go standard library's list package
"container/list"
"redigo/interface/database"
"redigo/interface/resp"
"redigo/lib/utils"
"redigo/resp/reply"
"strconv"
)
// getAsList retrieves the list stored at the given key, or creates a new one if it doesn't exist.
// It returns the list and a boolean indicating if the key existed.
func getAsList(db *DB, key string) (*list.List, bool) {
entity, ok := db.GetEntity(key)
if !ok {
// Key doesn't exist, create a new list
return list.New(), false
}
// Key exists, check if it's a list
lst, ok := entity.Data.(*list.List)
if !ok {
// Key exists but is not a list type
return nil, true // Indicate key exists but is wrong type
}
return lst, true
}
// execLPush implements the LPUSH command: Prepends one or multiple values to a list
// LPUSH key value [value ...]
func execLPush(db *DB, args [][]byte) resp.Reply {
key := string(args[0])
values := args[1:]
// Get or create list
lst, exists := getAsList(db, key)
if lst == nil && exists { // Key exists but is not a list
return reply.MakeWrongTypeErrReply()
}
// Prepend values
for _, value := range values {
lst.PushFront(value) // Add to the front (left)
}
// Store the updated list
db.PutEntity(key, &database.DataEntity{Data: lst})
db.addAof(utils.ToCmdLineWithName("LPUSH", args...))
// Return the new length of the list
return reply.MakeIntReply(int64(lst.Len()))
}
// execRPush implements the RPUSH command: Appends one or multiple values to a list
// RPUSH key value [value ...]
func execRPush(db *DB, args [][]byte) resp.Reply {
key := string(args[0])
values := args[1:]
// Get or create list
lst, exists := getAsList(db, key)
if lst == nil && exists { // Key exists but is not a list
return reply.MakeWrongTypeErrReply()
}
// Append values
for _, value := range values {
lst.PushBack(value) // Add to the back (right)
}
// Store the updated list
db.PutEntity(key, &database.DataEntity{Data: lst})
db.addAof(utils.ToCmdLineWithName("RPUSH", args...))
// Return the new length of the list
return reply.MakeIntReply(int64(lst.Len()))
}
// execLPop implements the LPOP command: Removes and returns the first element of the list stored at key
// LPOP key
func execLPop(db *DB, args [][]byte) resp.Reply {
key := string(args[0])
// Get list
lst, exists := getAsList(db, key)
if !exists {
return reply.MakeNullBulkReply()
}
if lst == nil { // Key exists but is not a list
return reply.MakeWrongTypeErrReply()
}
// Check if list is empty
if lst.Len() == 0 {
return reply.MakeNullBulkReply()
}
// Remove and get the first element
element := lst.Front()
lst.Remove(element)
value := element.Value.([]byte)
// If list becomes empty after pop, remove the key
if lst.Len() == 0 {
db.Remove(key)
} else {
// Otherwise update the list in database
db.PutEntity(key, &database.DataEntity{Data: lst})
}
db.addAof(utils.ToCmdLineWithName("LPOP", args...))
return reply.MakeBulkReply(value)
}
// execRPop implements the RPOP command: Removes and returns the last element of the list stored at key
// RPOP key
func execRPop(db *DB, args [][]byte) resp.Reply {
key := string(args[0])
// Get list
lst, exists := getAsList(db, key)
if !exists {
return reply.MakeNullBulkReply()
}
if lst == nil { // Key exists but is not a list
return reply.MakeWrongTypeErrReply()
}
// Check if list is empty
if lst.Len() == 0 {
return reply.MakeNullBulkReply()
}
// Remove and get the last element
element := lst.Back()
lst.Remove(element)
value := element.Value.([]byte)
// If list becomes empty after pop, remove the key
if lst.Len() == 0 {
db.Remove(key)
} else {
// Otherwise update the list in database
db.PutEntity(key, &database.DataEntity{Data: lst})
}
db.addAof(utils.ToCmdLineWithName("RPOP", args...))
return reply.MakeBulkReply(value)
}
// execLRange implements the LRANGE command: Returns the specified elements of the list stored at key
// LRANGE key start stop
func execLRange(db *DB, args [][]byte) resp.Reply {
// Parse arguments
key := string(args[0])
start, err := strconv.ParseInt(string(args[1]), 10, 64)
if err != nil {
return reply.MakeStandardErrorReply("value is not an integer or out of range")
}
stop, err := strconv.ParseInt(string(args[2]), 10, 64)
if err != nil {
return reply.MakeStandardErrorReply("value is not an integer or out of range")
}
// Get list
lst, exists := getAsList(db, key)
if !exists {
return reply.MakeEmptyMultiBulkReply()
}
if lst == nil { // Key exists but is not a list
return reply.MakeWrongTypeErrReply()
}
// Convert negative indices
size := int64(lst.Len())
if start < 0 {
start = size + start
}
if stop < 0 {
stop = size + stop
}
if start < 0 {
start = 0
}
if stop >= size {
stop = size - 1
}
if start > stop {
return reply.MakeEmptyMultiBulkReply()
}
// Collect elements
elements := make([][]byte, 0, stop-start+1)
index := int64(0)
for e := lst.Front(); e != nil; e = e.Next() {
if index >= start && index <= stop {
elements = append(elements, e.Value.([]byte))
} else if index > stop {
break
}
index++
}
return reply.MakeMultiBulkReply(elements)
}
// execLLen implements the LLEN command: Returns the length of the list stored at key
// LLEN key
func execLLen(db *DB, args [][]byte) resp.Reply {
key := string(args[0])
// Get list
lst, exists := getAsList(db, key)
if !exists {
return reply.MakeIntReply(0)
}
if lst == nil { // Key exists but is not a list
return reply.MakeWrongTypeErrReply()
}
return reply.MakeIntReply(int64(lst.Len()))
}
// execLIndex implements the LINDEX command: Returns the element at index in the list stored at key
// LINDEX key index
func execLIndex(db *DB, args [][]byte) resp.Reply {
key := string(args[0])
index, err := strconv.ParseInt(string(args[1]), 10, 64)
if err != nil {
return reply.MakeStandardErrorReply("value is not an integer or out of range")
}
// Get list
lst, exists := getAsList(db, key)
if !exists {
return reply.MakeNullBulkReply()
}
if lst == nil { // Key exists but is not a list
return reply.MakeWrongTypeErrReply()
}
size := int64(lst.Len())
if index < 0 {
index = size + index
}
if index < 0 || index >= size {
return reply.MakeNullBulkReply()
}
// Find the element at the specified index
var element *list.Element
if index < size/2 {
// If index is in the first half, iterate from front
element = lst.Front()
for i := int64(0); i < index; i++ {
element = element.Next()
}
} else {
// If index is in the second half, iterate from back
element = lst.Back()
for i := size - 1; i > index; i-- {
element = element.Prev()
}
}
return reply.MakeBulkReply(element.Value.([]byte))
}
// execLSet implements the LSET command: Sets the list element at index to value
// LSET key index value
func execLSet(db *DB, args [][]byte) resp.Reply {
key := string(args[0])
index, err := strconv.ParseInt(string(args[1]), 10, 64)
if err != nil {
return reply.MakeStandardErrorReply("value is not an integer or out of range")
}
value := args[2]
// Get list
lst, exists := getAsList(db, key)
if !exists {
return reply.MakeStandardErrorReply("no such key")
}
if lst == nil { // Key exists but is not a list
return reply.MakeWrongTypeErrReply()
}
size := int64(lst.Len())
if index < 0 {
index = size + index
}
if index < 0 || index >= size {
return reply.MakeStandardErrorReply("index out of range")
}
// Find and update the element at the specified index
var element *list.Element
if index < size/2 {
element = lst.Front()
for i := int64(0); i < index; i++ {
element = element.Next()
}
} else {
element = lst.Back()
for i := size - 1; i > index; i-- {
element = element.Prev()
}
}
element.Value = value
db.PutEntity(key, &database.DataEntity{Data: lst})
db.addAof(utils.ToCmdLineWithName("LSET", args...))
return reply.MakeOKReply()
}
func init() {
// Register list commands
// Arity is negative because the command takes a variable number of arguments (key + at least one value)
RegisterCommand("LPUSH", execLPush, -3) // key value [value ...] -> at least 3 args
RegisterCommand("RPUSH", execRPush, -3) // key value [value ...] -> at least 3 args
RegisterCommand("LPOP", execLPop, 2) // key
RegisterCommand("RPOP", execRPop, 2) // key
RegisterCommand("LRANGE", execLRange, 4) // key start stop
RegisterCommand("LLEN", execLLen, 2) // LLEN key -> exactly 2 args
RegisterCommand("LINDEX", execLIndex, 3) // LINDEX key index -> exactly 3 args
RegisterCommand("LSET", execLSet, 4) // LSET key index value -> exactly 4 args
}
但是这里我们还有一点没做,就是在 Cluster 模式下,我们的 List 结构还不能正常工作。因为我们还没注册 List 结构的相关命令。
我们只需要在 cluster/router.go
文件中注册 List 结构的相关命令即可,使用 defaultFunc
函数作为默认处理函数。因为每个指令都是对单独的单个 key 进行操作的。
cluster/router.go
func makeRouter() map[string]CmdFunc {
routerMap := make(map[string]CmdFunc)
routerMap["exists"] = defaultFunc // exists key
routerMap["type"] = defaultFunc // type key
routerMap["set"] = defaultFunc // set key
routerMap["get"] = defaultFunc // get key
routerMap["setnx"] = defaultFunc // setnx key
routerMap["getset"] = defaultFunc // getset key
routerMap["ping"] = pingFunc // ping command
routerMap["rename"] = renameFunc // rename key
routerMap["renamex"] = renameFunc
routerMap["flushdb"] = flushDBFunc // flushdb command
routerMap["del"] = delFunc // del key
routerMap["select"] = selectFunc // select database
routerMap["lpush"] = defaultFunc
routerMap["rpush"] = defaultFunc
routerMap["lpop"] = defaultFunc
routerMap["rpop"] = defaultFunc
routerMap["lrange"] = defaultFunc
routerMap["llen"] = defaultFunc
routerMap["lindex"] = defaultFunc
routerMap["lset"] = defaultFunc
return routerMap
}
测试
redis-cli -p 6380
127.0.0.1:6380> LPUSH mylist "first"
(integer) 1
127.0.0.1:6380> LPUSH mylist "second" "third"
(integer) 3
127.0.0.1:6380> RPUSH mylist "last"
(integer) 4
127.0.0.1:6380> LPOP mylist
"third"
127.0.0.1:6380> RPOP mylist
"last"
127.0.0.1:6380> LRANGE mylist 0 -1
1) "second"
2) "first"
127.0.0.1:6380> LRANGE mylist 0 0
1) "second"
127.0.0.1:6380> LRANGE mylist -2 -1
1) "second"
2) "first"
127.0.0.1:6380> DEL mylist
(integer) 1
127.0.0.1:6380> RPUSH mylist "one" "two" "three"
(integer) 3
127.0.0.1:6380> LLEN mylist
(integer) 3
127.0.0.1:6380> LINDEX mylist 0
"one"
127.0.0.1:6380> LINDEX mylist -1
"three"
127.0.0.1:6380> LINDEX mylist 5
(nil)
127.0.0.1:6380> LSET mylist 1 "new-two"
OK
127.0.0.1:6380> LRANGE mylist 0 -1
1) "one"
2) "new-two"
3) "three"
在这里我们测试了 List 结构的基本操作。如结果所示,这些操作都可以正常执行。
Last updated on