list是Redis中重要的数据结构之一,它的应用场景非常多,比如常用应用的关注列表、粉丝列表等都可以用Redis的list结构来实现,list可以按照插入顺序进行排序,同时它的另一个主要应用是消息队列。
它可以为我们提供以下操作:
| 命令 | 操作 |
|---|---|
| LLEN key | 获取列表长度 |
| LPUSH key value1 [value2] | 将一个或多个值插入到列表头部 |
| RPUSH key value1 [value2] | 在列表中添加一个或多个值到列表尾部 |
| LPOP key | 移出并获取列表的第一个元素 |
| RPOP key | 移除列表的最后一个元素,返回值为移除的元素 |
| LINDEX key index | 通过索引获取列表中的元素 |
| LRANGE key start stop | 获取列表指定范围内的元素 |
| BLPOP key1 [key2 ] timeout | 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 |
| BRPOPLPUSH source destination timeout | 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 |
| ... | ... |
例如: lpush在list的左边添加元素,rpush在list的右边添加元素
最终结果是 e d c b a x y z
可以正向读取,也可以逆向读取,依次,我们可以选择用双向链表进行实现,它的优势在于:
- 链表节点的结构拥有pre和next指针,所以获取某个节点的前置节点和后置节点的时间复杂度都是O(1)
- list结构提供表头指针head和表尾指针tail,所以获取的表头和表尾节点的时间复杂度都是O(1)
- 记录链表节点个数,所以获取链表中的节点数量的时间复杂度是O(1)
以下是我使用golang实现Redis列表的具体代码
首先,定义ListNode和List的结构
pkg\datastruct\list\list.go
// 双向链表的每一个节点
type ListNode struct {
pre *ListNode
next *ListNode
value string
}
// 双向链表本身
type List struct {
head *ListNode
tail *ListNode
len int
}
然后进行list的主要操作的实现
- Len()
- RPush(value)
- LPush(value)
- LPop()
- GetByIndex(index)
- Range(start, stop)
主要说一下RPush的实现,首先创建一个新的链表节点,
如果要插入的链表为空:则头尾指针均指向该节点
如果不为空:则把该节点加入末尾,把尾指针指向该节点
另外,不要忘记把长度++
func (list *List) RPush(value string) {
node := NewListNode(value)
if list.len == 0 {
// 链表为空
list.head = node
list.tail = node
} else {
// 链表不为空,加入到末尾
list.tail.next = node
node.pre = list.tail
list.tail = node
}
list.len++
}
其他方法依此类推,具体实现可以参考slava项目
同时可以编写list_test.go文件进行测试
至此,完成了数据结构的实现,但仍然不能与数据库交互
需要在database文件夹中支持该结构
pkg\database\list.go
对数据库对象进行获取list或者初始化一个list
func (db *DB) getAsList(key string) (*list.List, protocol.ErrorReply) {
entity, exists := db.GetEntity(key)
if !exists {
return nil, nil
}
list, ok := entity.Data.(*list.List)
if !ok {
return nil, &protocol.WrongTypeErrReply{}
}
return list, nil
}
// 首先获取getList,如果list不为空,则返回;如果为空,则初始化一个list
func (db *DB) getOrInitList(key string) (*list.List, bool, protocol.ErrorReply) {
getList, errReply := db.getAsList(key)
if errReply != nil {
return nil, false, errReply
}
isNew := false
if getList == nil {
getList = list.NewList()
db.PutEntity(key, &database.DataEntity{
Data: getList,
})
}
return getList, isNew, nil
}
之后就是对各种操作执行的实现
注意:redis通过AOF写后日志的方式保证数据的持久化,具体可以看我上一篇博客,Redis持久化之AOF ,所以记得在执行操作后调用db.AddAof(...)来进行日志的记录
以execListRPush为例,首先获取参数key,然后获取需要添加的参数
通过db.getOrInitList(key)得到要操作的list,之后针对每一个需要添加的元素执行RPush的操作
最后通过db.AddAof()进行日志的记录
// 执行LPush,并进行aof操作
func execListRPush(db *DB, args [][]byte) slava.Reply {
key := string(args[0])
values := args[1:]
list, _, errReply := db.getOrInitList(key)
if errReply != nil {
return errReply
}
for _, value := range values {
list.RPush(string(value))
}
db.AddAof(utils.ToCmdLine3("rpush", args...))
return protocol.MakeIntReply(int64(list.Len()))
}
虽然双向链表有一些优点,但是也存在很多缺点:
- pre 指针 8byte
- next 指针 8byte
- value string aaaaa
链表对于redis内存存取,消耗很大,当数据量多的时候,容易造成内存碎片
redis的list的另一种实现是通过ziplist,它拥有连续的内存空间,可以根据数据大小和类型进行不同的空间大小分配,可以节省内存
但同时也有些缺点:
扩容问题,如果超过一定量的大小,再添加数据,需要重新开辟一块新的内存空间
那么后续采用了quicklist来实现,后续争取逐步实现