二、List(列表)
1、List 数据结构的必要性
List 是一种有序的数据结构,可以按顺序存储多个字符串。它的主要特点如下:
- 有序性:List 中的元素是有序的,可以通过索引访问。
- 双向操作:List 支持从两端进行操作(如插入和删除),非常灵活。
- 多用途:List 可以用作队列(FIFO)和栈(LIFO),适用于多种场景。
在实际应用中,List 非常适合用于需要保持顺序的数据存储,例如任务队列、聊天记录、时间线等。由于 Redis 的高性能特点,List 还可以处理大量的数据读写操作。
2、List 的底层实现
在 Redis 中,List 有两种底层实现方式:
- 压缩列表(ziplist)
- 快速列表(quicklist)
2.1 压缩列表(ziplist)
压缩列表是一种为节省内存而设计的紧凑型双向链表。它通过将多个元素压缩存储在一个连续的内存块中,减少了内存碎片,提高了内存利用率。
结构:
typedef struct {
uint32_t zlbytes; // ziplist 占用的总字节数
uint32_t zltail; // ziplist 表尾节点的偏移量
uint16_t zlen; // ziplist 中节点的数量
unsigned char entries[]; // 数据项
} ziplist;
每个数据项(entry)包含:
-
prevrawlen
:前一个数据项的长度 -
len
:当前数据项的长度 -
data
:实际存储的数据
压缩列表通过紧凑的内存布局来节省空间,非常适合存储少量数据或小数据量的场景。然而,由于压缩列表的连续内存结构,在插入和删除操作时需要移动大量数据,时间复杂度较高。
2.2 快速列表(quicklist)
为了弥补压缩列表在插入和删除操作上的不足,Redis 3.2 引入了快速列表(quicklist)。快速列表结合了压缩列表和双向链表的优点,是一种用于实现列表(List)类型的高效数据结构。每个快速列表节点(quicklistNode)包含一个压缩列表,实现了数据的紧凑存储和高效操作。
结构:
typedef struct quicklist {
quicklistNode *head; // 快速列表头节点
quicklistNode *tail; // 快速列表尾节点
unsigned long count; // 快速列表中的节点数量
unsigned long len; // 快速列表中的元素数量
int fill : QL_FILL_BITS; // 填充因子
unsigned int compress : QL_COMP_BITS; // 压缩深度
quicklistBookmark bookmarks[]; // 书签
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev; // 前一个节点
struct quicklistNode *next; // 后一个节点
unsigned char *zl; // 压缩列表
unsigned int sz; // 压缩列表大小
unsigned int count : 16; // 节点中元素数量
unsigned int encoding : 2; // 编码类型
unsigned int container : 2; // 容器类型
unsigned int recompress : 1; // 重新压缩标志
unsigned int attempted_compress : 1; // 尝试压缩标志
unsigned int extra : 10; // 额外空间
} quicklistNode;
快速列表通过将多个压缩列表节点(quicklistNode)连接成一个双向链表,使得在两端进行插入和删除操作非常高效。同时,压缩列表的紧凑存储方式也能保证较高的内存利用率。
3、List 的常用操作
Redis 提供了丰富的 List 操作命令,包括插入、删除、获取和修剪等操作。
插入操作
-
LPUSH key value [value ...]
:在列表的左端插入一个或多个值。该命令会将一个或多个值插入到列表的左端(头部)。 -
RPUSH key value [value ...]
:在列表的右端插入一个或多个值。该命令会将一个或多个值插入到列表的右端(尾部)。
删除操作
-
LPOP key
:移除并返回列表的左端元素。该命令会移除并返回列表头部的元素。 -
RPOP key
:移除并返回列表的右端元素。该命令会移除并返回列表尾部的元素。
获取操作
-
LINDEX key index
:获取列表中指定索引的元素。该命令会返回列表中指定索引的元素。 -
LRANGE key start stop
:获取列表中指定范围的元素。该命令会返回指定范围内的元素,start 和 stop 为索引。
修剪操作
-
LTRIM key start stop
:对列表进行修剪,只保留指定范围内的元素。该命令会将列表修剪到指定范围之外的元素都会被删除。
4、List 的实现细节
创建 List 对象
robj *createQuicklistObject(void) {
quicklist *l = quicklistCreate();
robj *o = createObject(OBJ_LIST, l);
o->encoding = OBJ_ENCODING_QUICKLIST;
return o;
}
创建 quicklist 对象
quicklist *quicklistCreate(void) {
struct quicklist *quicklist;
quicklist = zmalloc(sizeof(*quicklist));
quicklist->head = quicklist->tail = NULL;
quicklist->len = 0;
quicklist->count = 0;
quicklist->compress = 0;
quicklist->fill = -2; // 默认填充因子
quicklist->bookmark_count = 0;
return quicklist;
}
创建 quicklistNode 对象
quicklistNode *quicklistCreateNode(void) {
quicklistNode *node;
node = zmalloc(sizeof(*node));
node->prev = node->next = NULL;
node->zl = NULL;
node->sz = 0;
node->count = 0;
node->encoding = 0;
node->container = 0;
node->recompress = 0;
node->attempted_compress = 0;
node->extra = 0;
return node;
}
压缩列表与快速列表的结合
// 创建一个新的 quicklist 对象
quicklist *quicklist = quicklistCreate();
// 创建一个新的 quicklistNode 对象
quicklistNode *node = quicklistCreateNode();
// 为 quicklistNode 分配一个新的压缩列表
node->zl = ziplistNew();
node->sz = ziplistBlobLen(node->zl);
// 将 node 插入到 quicklist 中
quicklist->head = node;
quicklist->tail = node;
quicklist->len = 1;
quicklist->count = ziplistLen(node->zl);
5、使用示例
以下是一些使用 Redis List 的示例,展示了如何利用 List 进行数据的存储和操作。
插入数据
LPUSH mylist "world"
# (integer) 1
LPUSH mylist "hello"
# (integer) 2
获取数据
LRANGE mylist 0 -1
# 1) "hello"
# 2) "world"
删除数据
LPOP mylist
# "hello"
LRANGE mylist 0 -1
# 1) "world"
结论
通过上述解析,我们可以更好地理解 List 的设计思想和实现原理,从而在实际开发中更好地利用 List 提供的优势。在 Redis 中,List 通过压缩列表和快速列表的结合,实现了高效的存储和操作,适用于多种场景如队列、栈和消息列表等。了解这些优化策略,可以帮助我们在实际应用中更好地利用 Redis 的性能和功能。