6、Redis系统-数据结构-02-链表

时间:2024-07-09 07:24:13

二、List(列表)

1、List 数据结构的必要性

List 是一种有序的数据结构,可以按顺序存储多个字符串。它的主要特点如下:

  1. 有序性:List 中的元素是有序的,可以通过索引访问。
  2. 双向操作:List 支持从两端进行操作(如插入和删除),非常灵活。
  3. 多用途:List 可以用作队列(FIFO)和栈(LIFO),适用于多种场景。

在实际应用中,List 非常适合用于需要保持顺序的数据存储,例如任务队列、聊天记录、时间线等。由于 Redis 的高性能特点,List 还可以处理大量的数据读写操作。

2、List 的底层实现

在 Redis 中,List 有两种底层实现方式:

  1. 压缩列表(ziplist)
  2. 快速列表(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 的性能和功能。