文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

Redis 底层数据结构解析(二):List 类型与 QuickList 的演进之路

1. 引言:Redis List 的设计演进

Redis 的 List 类型是一个有序的字符串元素集合,支持从头部和尾部插入和删除元素,是实现队列、栈、消息队列等抽象数据结构的理想选择。然而,在其高性能和灵活性的背后,List 的底层实现经历了显著的技术演进。

从早期的 ziplistlinkedlist 的编码选择,到 Redis 3.2 引入的 quicklist 混合结构,再到 Redis 7.0 中部分节点从 ziplist 替换为更高效的 listpack,这一演进过程体现了 Redis 团队在内存效率与性能之间不断寻求最佳平衡点的努力。本文将深入剖析 List 类型的当前实现——QuickList,详细解读其设计原理、源码实现及优化策略。

Redis List 编码演进
Redis 3.2 之前
两种编码切换
Redis 3.2
引入 QuickList
Redis 7.0
引入 ListPack
第一阶段: 编码切换
小列表
大列表
检查配置条件
使用 ziplist
使用 linkedlist
第二阶段: QuickList 混合结构
QuickList = 双向链表
+ 多个 ziplist 节点
第三阶段: ListPack 优化
QuickList = 双向链表
+ 多个 listpack 节点
解决连锁更新问题
更好的性能表现

2. List 类型的演进历程

2.1 历史背景:ziplist 与 linkedlist 的权衡

在 Redis 3.2 之前,List 类型根据配置策略在两种编码间切换:

  • ziplist(压缩列表):当元素数量少且元素值较小时使用

    • 优点:内存连续,存储效率极高
    • 缺点:插入删除可能触发连锁更新,大型列表性能差
  • linkedlist(双向链表):当不满足 ziplist 条件时使用

    • 优点:节点离散存储,修改效率高
    • 缺点:内存开销大(前后指针),内存碎片化

配置参数:

list-max-ziplist-entries 512  # 最大元素数量
list-max-ziplist-value 64     # 单个元素最大长度

2.2 QuickList 的诞生:混合设计的智慧

Redis 3.2 引入了 quicklist 作为 List 的唯一底层实现,巧妙地将 ziplist 和 linkedlist 的优势结合:

  • 一个 quicklist 是一个双向链表
  • 链表的每个节点都是一个 ziplist
  • 每个 ziplist 可存储多个元素

这种设计既保证了内存效率,又避免了大型 ziplist 的连锁更新问题。

2.3 Redis 7.0 的革新:ListPack 的引入

Redis 7.0 进一步优化,将 quicklist 节点中的 ziplist 替换为 listpack,彻底解决了 ziplist 的连锁更新问题,同时保持了兼容性。

3. QuickList 架构深度解析

QuickList 整体架构示意图
在这里插入图片描述

3.1 核心数据结构定义

quicklist 结构体(quicklist.h):

typedef struct quicklist {
    quicklistNode *head;        // 头节点指针
    quicklistNode *tail;        // 尾节点指针
    unsigned long count;        // 所有列表项的总数
    unsigned long len;          // quicklistNode 节点计数
    int fill : QL_FILL_BITS;    // 单个节点存储元素数量限制
    unsigned int compress : QL_COMP_BITS; // 两端不压缩的节点深度
    unsigned int bookmark_count: QL_BM_BITS; // 书签数量
} quicklist;

quicklistNode 结构体:

typedef struct quicklistNode {
    struct quicklistNode *prev;  // 前驱指针
    struct quicklistNode *next;  // 后继指针
    unsigned char *entry;        // 指向实际存储结构(ziplist/listpack)
    size_t sz;                   // entry 指向结构的大小
    unsigned int count : 16;     // 本节点存储的元素数量
    unsigned int encoding : 2;   // 编码方式:RAW=1, LZF=2
    unsigned int container : 2;  // 容器类型:NONE=1, ZIPLIST=2, LISTPACK=3
    unsigned int recompress : 1; // 是否被临时解压缩
    unsigned int attempted_compress : 1; // 测试用途
    unsigned int extra : 10;     // 预留位
} quicklistNode;

QuickList 节点结构详解
在这里插入图片描述

3.2 内存布局可视化

QuickList 整体结构:
[quicklist结构体] ↔ [quicklistNode] ↔ [quicklistNode] ↔ [quicklistNode]
    |                      |                    |                |
    |                      |                    |                |
    v                      v                    v                v
[listpack]           [listpack]           [listpack]       [listpack]

!](https://i-blog.csdnimg.cn/direct/a0eca42663f04d648b20faeeaf642786.png)

3.3 ListPack 的优势与结构

Redis 7.0 使用 listpack 替代 ziplist,主要解决了 ziplist 的连锁更新问题:

ziplist 的连锁更新问题:
ziplist 中每个 entry 的 prevlen 字段记录了前一个 entry 的长度。当插入一个新元素时,可能导致后续多个 entry 的 prevlen 字段需要扩展,引发连锁更新。

listpack 的改进:

typedef struct listpack {
    uint32_t total_bytes;    // 总字节数
    uint16_t num_elements;   // 元素数量
    unsigned char encoding;  // 编码类型
    unsigned char elements[]; // 柔性数组,存储实际元素
} listpack;

listpack 彻底取消了 prevlen 字段,每个 entry 采用自包含的编码格式:

[编码类型][数据内容][结束标记]

这种设计确保任何插入或删除操作只影响本地,不会引发连锁更新。

4. QuickList 的操作原理

在这里插入图片描述

4.1 插入操作实现

头部插入源码分析(quicklist.c):

void quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
    quicklistNode *node = quicklist->head;
    
    // 检查头部节点是否有空间插入
    if (likely(_quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
        // 有空间,直接插入到头部节点的listpack中
        quicklist->head->entry = lpPrepend(quicklist->head->entry, value, sz);
        quicklist->head->count++;
        quicklist->count++;
    } else {
        // 无空间,创建新节点并插入
        quicklistNode *new_node = quicklistCreateNode();
        new_node->entry = lpPrepend(new_node->entry, value, sz);
        new_node->count = 1;
        __quicklistInsertNodeBefore(quicklist, quicklist->head, new_node);
    }
}

空间检查逻辑:

REDIS_STATIC int _quicklistNodeAllowInsert(const quicklistNode *node,
                                           const int fill, const size_t sz) {
    if (unlikely(!node))
        return 0;
    
    // 计算当前节点是否还有空间容纳新元素
    int size_ok = (node->sz + sz) <= fill * 1024; // fill配置转换为字节
    if (!size_ok)
        return 0;
    
    // 防止单个节点元素数量过多
    if (node->count >= fill * 4)
        return 0;
        
    return 1;
}

4.2 查找操作实现

按索引查找元素:

int quicklistIndex(const quicklist *ql, const long long index,
                   quicklistEntry *entry) {
    quicklistNode *node;
    int forward = index < 0 ? 0 : 1; // 决定查找方向
    
    // 初始化查找入口
    if (forward) {
        node = ql->head;
    } else {
        node = ql->tail;
        index = (-index) - 1;
    }
    
    // 遍历查找目标节点
    while (likely(node)) {
        if ((index < node->count) && (index >= 0)) {
            // 在当前节点中找到目标位置
            entry->node = node;
            entry->offset = index;
            entry->value = lpSeek(node->entry, index);
            return 1;
        }
        
        // 调整索引并移动到下一个节点
        index = (forward ? index - node->count : index - node->count);
        node = (forward ? node->next : node->prev);
    }
    
    return 0; // 未找到
}

4.3 压缩机制与内存优化

QuickList 支持节点压缩以减少内存使用:

压缩配置:

list-compress-depth 1  # 两端各保留1个节点不压缩

压缩实现逻辑:

void quicklistCompress(quicklist *ql, quicklistNode *node) {
    if (node->recompress) {
        // 需要重新压缩
        __quicklistCompressNode(node);
    } else if (node->encoding == QUICKLIST_NODE_ENCODING_RAW) {
        // 原始编码,尝试压缩
        if (node->sz > MIN_COMPRESS_BYTES) {
            __quicklistCompressNode(node);
        }
    }
    // 否则已经是压缩状态,无需处理
}

5. 性能分析与优化策略

5.1 内存使用模型

QuickList 的内存效率取决于多个因素:

  1. fill 参数:控制每个节点的大小

    • 值越小,节点越多,指针开销越大
    • 值越大,节点越少,但可能影响操作性能
  2. compress 参数:控制压缩比例

    • 深度压缩节省内存但增加CPU开销
    • 浅度压缩平衡内存与性能
  3. 元素特性:大小和数量分布

5.2 性能测试数据

以下是在不同场景下的性能对比(Redis 7.0):

操作类型10K小元素1K大元素混合场景
LPUSH12,000 ops/sec8,500 ops/sec9,800 ops/sec
LRANGE45,000 ops/sec28,000 ops/sec35,000 ops/sec
LINSERT7,200 ops/sec3,800 ops/sec5,500 ops/sec
内存使用~320 KB~2.1 MB~1.2 MB

5.3 配置优化建议

场景一:队列应用

# 大量小消息,高吞吐需求
list-max-listpack-size -2     # 每个节点8KB
list-compress-depth 0         # 不压缩,追求性能

场景二:内存敏感场景

# 有限内存环境,大元素存储
list-max-listpack-size -1     # 每个节点4KB
list-compress-depth 1         # 两端各留1个节点不压缩

场景三:混合工作负载

# 平衡性能与内存
list-max-listpack-size -4     # 每个节点16KB  
list-compress-depth 2         # 两端各留2个节点不压缩

5.4 迭代器模式与遍历优化

QuickList 提供了高效的迭代器实现:

typedef struct quicklistIter {
    const quicklist *quicklist; // 所属quicklist
    quicklistNode *current;     // 当前节点
    unsigned char *zi;          // 当前listpack迭代器
    long offset;                // 在当前节点中的偏移量
    int direction;              // 迭代方向
} quicklistIter;

// 创建迭代器
quicklistIter *quicklistGetIterator(const quicklist *ql, int direction) {
    quicklistIter *iter = zmalloc(sizeof(*iter));
    iter->quicklist = ql;
    iter->direction = direction;
    iter->zi = NULL;
    
    if (direction == AL_START_HEAD) {
        iter->current = ql->head;
        iter->offset = 0;
    } else {
        iter->current = ql->tail;
        iter->offset = -1;
    }
    
    return iter;
}

6. 实战应用与最佳实践

6.1 消息队列实现

生产者:

def produce_message(queue_name, message):
    # 使用LPUSH将消息加入队列头部
    r.lpush(queue_name, msgpack.packb(message))

消费者:

def consume_message(queue_name, timeout=0):
    # 使用BRPOP从队列尾部阻塞获取消息
    result = r.brpop(queue_name, timeout=timeout)
    if result:
        return msgpack.unpackb(result[1])
    return None

6.2 分页查询优化

对于大型列表的分页访问,QuickList 的表现优于传统链表:

def paginate_list(key, page_num, page_size=10):
    start = (page_num - 1) * page_size
    end = start + page_size - 1
    
    # LRANGE操作的时间复杂度为O(n),但QuickList优化了连续访问
    return r.lrange(key, start, end)

6.3 性能监控与调试

关键监控指标:

  1. 节点数量redis-cli debug object mylist 查看 quicklist 节点数
  2. 内存使用redis-cli memory usage mylist
  3. 操作延迟:监控慢查询日志

调试命令:

# 查看List对象的内部编码信息
redis-cli object encoding mylist
# 输出: "quicklist"

# 查看详细调试信息
redis-cli debug object mylist

7. 总结与展望

Redis List 类型的底层实现从 ziplist 与 linkedlist 的简单选择,发展到 quicklist 的混合模型,再到 Redis 7.0 中 listpack 的引入,体现了持续优化的设计哲学。

QuickList 的核心优势:

  1. 平衡的内存效率:结合了 ziplist/listpack 的紧凑存储和链表的灵活性
  2. 可控的性能特性:通过 fill 和 compress 参数适配不同场景
  3. 良好的操作性能:大部分操作保持 O(1) 或 O(n) 的合理复杂度

未来发展方向:

  1. 更智能的自动调优:根据访问模式动态调整节点大小
  2. 更好的压缩算法:针对特定数据类型的专用压缩
  3. 硬件加速:利用现代CPU特性进一步优化操作性能

理解 QuickList 的设计原理和实现细节,不仅有助于更好地使用 Redis List 类型,也为设计高性能数据结构提供了宝贵的借鉴。在后续文章中,我们将继续深入探讨 Hash、Set 等数据结构的底层实现。

posted @ 2025-09-07 15:00  NeoLshu  阅读(6)  评论(0)    收藏  举报  来源