1. 什么是链表
链表作为一种常用数据结构,链表内置在很多高级的编程语言里面,以为 Redis 使用的 C语言并没有内置这种数据结构,所以 Redis 构建了自己的链表实现。
在Redis 中,链表的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis 就会使用链表作为列表建的底层实现。
2. 链表的数据结构
2.1 链表节点
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
}listNode;
多个链表节点就可以组成双向链表了,如图所示:
2.2 链表
Redis 在 listNode
基础上,加多了一个 list
。使用 list
来操作链表的话,会更方便。具体的好处可以参考后面 list
& listNode
API。
typedef struct list {
// 表头节点
listNode *head;
// 表尾节点
listNode *tail;
// 链表所包含的节点数量
unsigned long len;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
void (*match)(void *ptr, void *key);
}list;
list 结构为链表提供了表头指针 head
、表尾指针 tail
、以及链表长度计数器 len
,这些都不难理解。而 dup
、free
和 match
成员变量则是用于实现多态链表所需的类型特定函数:
dup
函数:用于复制链表节点所保存的值;free
函数:用于释放链表节点所保存的值;match
函数:用于对比链表节点所保存的值和另一个输入值是否相等。
list
结构如图所示:
3. 链表基本使用
3.1 LLEN
Redis Llen 命令用于返回列表的长度。 如果列表 key 不存在,则 key 被解释为一个空列表,返回 0 。 如果 key 不是列表类型,返回一个错误。
3.1.1 语法
redis 127.0.0.1:6379> LLEN KEY_NAME
3.1.2 示例
# 读取不存在的key的链表长度
127.0.0.1:6379> EXISTS myList
(integer) 0
127.0.0.1:6379> LLEN myList
(integer) 0
# 读取存在的key的链表长度
127.0.0.1:6379> LPUSH myList value1
(integer) 1
127.0.0.1:6379> LLEN myList
(integer) 1
# 读取非链表key
127.0.0.1:6379> EXISTS myKey
(integer) 1
127.0.0.1:6379> LLEN myKey
(error) WRONGTYPE Operation against a key holding the wrong kind of value
3.2 LPUSH & RPUSH
两个命令都是往链表里面插入值,如果 key 不存在,一个空列表会被创建并执行插入操作。 当 key 存在但不是列表类型时,返回一个错误。
不同的是,LPUSH
是往头部插入值,RPUSH
是往尾部插入值。
3.2.1 语法
redis 127.0.0.1:6379> LPUSH KEY_NAME VALUE1.. VALUEN
redis 127.0.0.1:6379> RPUSH KEY_NAME VALUE1..VALUEN
3.2.2 示例
# myList 是个空链表
127.0.0.1:6379> LLEN myList
(integer) 0
# 从头部分别插入:5 4 3 2 1
127.0.0.1:6379> LPUSH myList 5 4 3 2 1
(integer) 5
127.0.0.1:6379> LLEN myList
(integer) 5
127.0.0.1:6379> LRANGE myList 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
# 从头部分别插入:6 7 8 9 10
127.0.0.1:6379> RPUSH myList 6 7 8 9 10
(integer) 10
# 得到的结果
127.0.0.1:6379> LRANGE myList 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"
10) "10"
3.3 LPUSHX & RPUSHX
具体作用和 LPUSH
& RPUSH
类似,唯一区别是,如果目标链表不存在,则操作无效。
3.3.1 语法
redis 127.0.0.1:6379> LPUSHX KEY_NAME VALUE1.. VALUEN
redis 127.0.0.1:6379> RPUSHX KEY_NAME VALUE1..VALUEN
3.3.2 示例
127.0.0.1:6379> EXISTS myList2
(integer) 0
127.0.0.1:6379> LPUSHX myList2 value1
(integer) 0
127.0.0.1:6379> LLEN myList2
(integer) 0
3.4 LRANGE
Redis Lrange 返回列表中指定区间内的元素,区间以偏移量 START 和 END 指定。 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
3.4.1 语法
redis 127.0.0.1:6379> LRANGE KEY_NAME START END
3.4.2 示例
# 查询已存在的链表
127.0.0.1:6379> LRANGE myList 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"
10) "10"
# 查询不存在的链表
127.0.0.1:6379> LRANGE myList3 0 -1
(empty array)
# 查询非链表
127.0.0.1:6379> LRANGE myKey 0 -1
(error) WRONGTYPE Operation against a key holding the wrong kind of value
3.5 LINDEX
Redis Lindex 命令用于通过索引获取列表中的元素。你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
3.5.1 语法
redis 127.0.0.1:6379> LINDEX KEY_NAME INDEX_POSITION
3.5.2 示例
127.0.0.1:6379> EXISTS myList
(integer) 1
127.0.0.1:6379> LRANGE myList 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"
10) "10"
# 获取链表范围内的下标值
127.0.0.1:6379> LINDEX myList 5
"6"
# 获取链表范围外的下标值
127.0.0.1:6379> LINDEX myList 11
(nil)
3.5 LPOP & RPOP
返回列表中的元素。 当列表 key 不存在时,返回 nil 。
LPOP
返回列表中的头部元素
RPOP
返回列表中的尾部元素
3.5.1 语法
redis 127.0.0.1:6379> Lpop KEY_NAME
redis 127.0.0.1:6379> Rpop KEY_NAME
3.5.2 示例
127.0.0.1:6379> LRANGE myList 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"
10) "10"
127.0.0.1:6379> LPOP myList
"1"
127.0.0.1:6379> RPOP myList
"10"
127.0.0.1:6379> LRANGE myList 0 -1
1) "2"
2) "3"
3) "4"
4) "5"
5) "6"
6) "7"
7) "8"
8) "9"
3.6 LREM
Redis Lrem 根据参数 COUNT
的值,移除列表中与参数 VALUE
相等的元素。
COUNT 的值可以是以下几种:
count > 0
: 从表头开始向表尾搜索,移除与 VALUE 相等的元素,数量为 COUNT 。count < 0
: 从表尾开始向表头搜索,移除与 VALUE 相等的元素,数量为 COUNT 的绝对值。count = 0
: 移除表中所有与 VALUE 相等的值。
3.6.1 语法
redis 127.0.0.1:6379> LREM key count VALUE
3.6.2 示例
127.0.0.1:6379> RPUSH list2 "hello" "hello" "hello" "redis" "hello" "hello" "hello"
(integer) 7
127.0.0.1:6379> LRANGE list2 0 -1
1) "hello"
2) "hello"
3) "hello"
4) "redis"
5) "hello"
6) "hello"
7) "hello"
# count > 0,从头部开始删除一个"hello"值
127.0.0.1:6379> LREM list2 1 "hello"
(integer) 1
127.0.0.1:6379> LRANGE list2 0 -1
1) "hello"
2) "hello"
3) "redis"
4) "hello"
5) "hello"
6) "hello"
# count < 0,从尾部开始删除一个"hello"值
127.0.0.1:6379> LREM list2 -1 "hello"
(integer) 1
127.0.0.1:6379> LRANGE list2 0 -1
1) "hello"
2) "hello"
3) "redis"
4) "hello"
5) "hello"
# count = 0,删除所有"hello"值
127.0.0.1:6379> LREM list2 0 "hello"
(integer) 4
127.0.0.1:6379> LRANGE list2 0 -1
1) "redis"
3.7 BLPOP & BRPOP
BLPOP
& BRPOP
的作用和 LPOP
& RPOP
类似,区别在于,如果链表为空时,BLPOP
& BRPOP
会阻塞列表直到等待超时或发现可弹出元素为止。而 LPOP
& RPOP
是返回 nil
3.7.1 语法
redis 127.0.0.1:6379> BLPOP LIST1 LIST2 .. LISTN TIMEOUT
redis 127.0.0.1:6379> BRPOP LIST1 LIST2 .. LISTN TIMEOUT
3.7.2 示例
127.0.0.1:6379> RPUSH list1 a b c
(integer) 3
127.0.0.1:6379> BRPOP list1 0
1) "list1"
2) "c"
127.0.0.1:6379> BRPOP list1 0
1) "list1"
2) "b"
127.0.0.1:6379> BRPOP list1 0
1) "list1"
2) "a"
127.0.0.1:6379> BRPOP list1 5
(nil)
(5.06s)
4. 链表 API
函数 | 作用 | 时间复杂度 |
---|---|---|
listSearchKey | 查找并返回链表中包含给定值的节点 | O(N),N 为链表长度 |
listIndex | 返回链表在给定索引上的节点 | O(N),N 为链表长度 |
listDelNode | 从链表中删除给定节点 | O(N),N 为链表长度 |
listDup | 复制一个给定链表的副本 | O(N),N 为链表长度 |
listRelease | 释放给定链表,以及链表中的所有节点 | O(N),N 为链表长度 |
这里只给出了时间复杂度为:O(N) 的 API,其余的操作时间复杂度皆为:O(1)。在日常操作中,应该更关注链表中那些操作的时间复杂度较高,可能会影响整体性能。
5. 参考
- 《Redis设计与实现》
- Redis 链表