← 返回文章列表

Redis底层原理(三):核心数据结构实现

2020-02-01·7 分钟阅读

前言

在上一章中,我们分析了 Redis 的基础数据结构。本章将深入探索 Redis 的三种核心数据结构:跳跃表(Skip List)、整数集合(IntSet)和压缩列表(ZipList)。这些数据结构是 Redis 实现有序集合、小规模集合和列表的关键。

一、跳跃表(Skip List)

1.1 跳跃表简介

跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,实现快速访问。跳跃表支持平均 O(logN)、最坏 O(N) 的查找复杂度。

Redis 使用跳跃表作为有序集合(ZSET)的底层实现之一,原因如下:

┌──────────────────────────────────────────────────────────────┐
│                    为什么 Redis 选择跳跃表                    │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  对比维度         │  跳跃表          │  平衡树(如红黑树)   │
├───────────────────┼──────────────────┼───────────────────────┤
│ 实现复杂度        │ 简单             │ 复杂                  │
│ 范围查询          │ 高效             │ 需要中序遍历          │
│ 内存占用          │ 相对较高         │ 相对较低              │
│ 插入/删除复杂度   │ O(logN) 平均     │ O(logN)               │
│ 并发友好          │ 是               │ 需要复杂的锁机制      │
│                                                              │
│  Redis 选择跳跃表的原因:                                    │
│  1. 实现简单,易于理解和维护                                 │
│  2. 范围查询效率高(ZRANGE、ZRANK 等操作)                   │
│  3. 内存占用可接受(Redis 已优化)                           │
│                                                              │
└──────────────────────────────────────────────────────────────┘

1.2 跳跃表结构定义

// server.h - 跳跃表节点
typedef struct zskiplistNode {
    sds ele;                        // 成员对象(Redis 5.0 后改为 SDS)
    double score;                   // 分值
    struct zskiplistNode *backward; // 后退指针
    struct zskiplistLevel {
        struct zskiplistNode *forward;  // 前进指针
        unsigned long span;              // 跨度(用于计算排名)
    } level[];                      // 层
} zskiplistNode;

// 跳跃表
typedef struct zskiplist {
    struct zskiplistNode *header, *tail;  // 头尾节点
    unsigned long length;                   // 节点数量
    int level;                              // 最大层数
} zskiplist;

1.3 跳跃表结构示意图

┌──────────────────────────────────────────────────────────────┐
│                    跳跃表结构示意图                           │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  header(头节点,不存储实际数据)                            │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ L4 │─────────────────────────────────────────────────┼──►│ NULL
│  │ L3 │───────────────────────┬─────────────────────────┼──►│ NULL
│  │ L2 │──────────┬────────────┼───────────┬─────────────┼──►│ NULL
│  │ L1 │────┬─────┼────┬───────┼─────┬─────┼──────┬──────┼──►│ NULL
│  │ L0 │────┼─────┼────┼───────┼─────┼─────┼──────┼──────┼──►│ NULL
│  └────┴─────┴────┴────┴───────┴─────┴─────┴──────┴──────┘    │
│        │     │    │        │     │     │      │             │
│        ▼     ▼    ▼        ▼     ▼     ▼      ▼             │
│       ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐           │
│       │score=1 │ │score=3 │ │score=5 │ │score=7 │           │
│       │ele="a" │ │ele="b" │ │ele="c" │ │ele="d" │           │
│       │back───►│ │back───►│ │back───►│ │back───►│ NULL      │
│       └────────┘ └────────┘ └────────┘ └────────┘           │
│                                                              │
│  说明:                                                      │
│  • 每个节点随机生成 1-32 层                                  │
│  • 层越高,节点越稀疏                                        │
│  • span 记录跨度,用于计算排名                               │
│  • backward 指针用于从尾到头遍历                             │
│                                                              │
└──────────────────────────────────────────────────────────────┘

1.4 跳跃表核心操作

1.4.1 创建跳跃表

// t_zset.c
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;
    
    zsl = zmalloc(sizeof(*zsl));
    zsl->level = 1;
    zsl->length = 0;
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL, 0, NULL);
    
    // 初始化头节点的每一层
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
        zsl->header->level[j].forward = NULL;
        zsl->header->level[j].span = 0;
    }
    zsl->header->backward = NULL;
    zsl->tail = NULL;
    
    return zsl;
}

// 创建节点
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
    zskiplistNode *zn = zmalloc(sizeof(*zn) + level * sizeof(struct zskiplistLevel));
    zn->score = score;
    zn->ele = ele;
    return zn;
}

1.4.2 随机层数生成

Redis 使用幂次定律(power law)生成随机层数:

// t_zset.c
#define ZSKIPLIST_P 0.25      // 晋升概率
#define ZSKIPLIST_MAXLEVEL 32  // 最大层数

int zslRandomLevel(void) {
    int level = 1;
    // 随机数小于 ZSKIPLIST_P * 0xFFFF 时晋升
    while ((random() & 0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

层数概率分布:

┌──────────────────────────────────────────────────────────────┐
│                    跳跃表层数概率分布                         │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  层数  │ 概率              │ 说明                            │
├────────┼───────────────────┼─────────────────────────────────┤
│  1     │ 75%               │ 基础层,所有节点都有             │
│  2     │ 18.75%            │ 75% * 25%                       │
│  3     │ 4.69%             │ 75% * 25% * 25%                 │
│  4     │ 1.17%             │ ...                             │
│  5     │ 0.29%             │ ...                             │
│  ...   │ ...               │ 逐层递减                        │
│  32    │ 极低              │ 理论最大层数                    │
│                                                              │
│  晋升概率 P = 0.25 意味着:                                  │
│  • 约 1/4 的节点有第 2 层                                    │
│  • 约 1/16 的节点有第 3 层                                   │
│  • 平均每个节点有 1/(1-0.25) = 1.33 个指针                   │
│                                                              │
└──────────────────────────────────────────────────────────────┘

1.4.3 插入节点

// t_zset.c - 简化的插入逻辑
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL];  // 记录每层的前驱节点
    unsigned int rank[ZSKIPLIST_MAXLEVEL];       // 记录排名
    zskiplistNode *x;
    int i, level;
    
    x = zsl->header;
    
    // 从最高层向下查找插入位置
    for (i = zsl->level - 1; i >= 0; i--) {
        rank[i] = i == (zsl->level - 1) ? 0 : rank[i + 1];
        
        // 沿着当前层前进
        while (x->level[i].forward &&
               (x->level[i].forward->score < score ||
                (x->level[i].forward->score == score &&
                 sdscmp(x->level[i].forward->ele, ele) < 0))) {
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
        update[i] = x;  // 记录前驱节点
    }
    
    // 随机生成层数
    level = zslRandomLevel();
    
    // 如果新节点层数大于当前最大层数
    if (level > zsl->level) {
        for (i = zsl->level; i < level; i++) {
            rank[i] = 0;
            update[i] = zsl->header;
            update[i]->level[i].span = zsl->length;
        }
        zsl->level = level;
    }
    
    // 创建新节点
    x = zslCreateNode(level, score, ele);
    
    // 更新各层指针
    for (i = 0; i < level; i++) {
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;
        
        // 更新跨度
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }
    
    // 更新更高层的跨度
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }
    
    // 设置后退指针
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        zsl->tail = x;
    
    zsl->length++;
    return x;
}

1.5 跳跃表查询效率

┌──────────────────────────────────────────────────────────────┐
│                    跳跃表查询过程                             │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  查找 score=5 的节点                                        │
│                                                              │
│  Level 4: ────────────────────────────────────────────► NULL │
│                                                              │
│  Level 3: ──────────────────┬───────────────────────► NULL   │
│                            ▼                                 │
│  Level 2: ─────────┬───────┼───────┬───────────────► NULL    │
│                    ▼       │       ▼                          │
│  Level 1: ───┬─────┼───────┼───┬───┼───────┬───────► NULL    │
│              ▼     │       │   ▼   │       │                  │
│  Level 0: ───┼─────┼───────┼───┼───┼───────┼───► NULL        │
│              ▼     ▼       ▼   ▼   ▼       ▼                  │
│            [1]   [3]     [5] [5] [5]     [7]                  │
│                          ▲                                   │
│                          │                                   │
│                      找到目标                                │
│                                                              │
│  查找路径:从最高层开始,逐层下降                            │
│  时间复杂度:O(logN)                                         │
│                                                              │
└──────────────────────────────────────────────────────────────┘

二、整数集合(IntSet)

2.1 整数集合简介

整数集合是 Redis 用于保存整数值的集合抽象数据结构,可以保存 int16_t、int32_t、int64_t 类型的整数值,并且保证集合中不出现重复元素。

┌──────────────────────────────────────────────────────────────┐
│                    整数集合特点                               │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  ✅ 有序:元素按从小到大排列                                 │
│  ✅ 无重复:自动去重                                         │
│  ✅ 编码升级:根据元素类型自动升级                           │
│  ✅ 内存紧凑:连续内存,无指针开销                           │
│                                                              │
│  应用场景:                                                  │
│  • Set 类型的底层实现之一(元素都是整数且数量较少时)         │
│  • 节省内存                                                  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

2.2 整数集合结构定义

// intset.h
typedef struct intset {
    uint32_t encoding;  // 编码方式:int16、int32、int64
    uint32_t length;    // 元素数量
    int8_t contents[];  // 保存元素的数组(柔性数组)
} intset;

// 编码类型
#define INTSET_ENC_INT16 (sizeof(int16_t))  // 2 字节
#define INTSET_ENC_INT32 (sizeof(int32_t))  // 4 字节
#define INTSET_ENC_INT64 (sizeof(int64_t))  // 8 字节

2.3 整数集合内存布局

┌──────────────────────────────────────────────────────────────┐
│                    整数集合内存布局                           │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  intset 结构(encoding = INTSET_ENC_INT16)                  │
│  ┌──────────────┬──────────────┬───────────────────────────┐│
│  │ encoding=2   │ length=4     │ contents 数组              ││
│  └──────────────┴──────────────┴───────────────────────────┘│
│                                              │               │
│                                              ▼               │
│  contents 数组(每个元素 2 字节):                          │
│  ┌────────┬────────┬────────┬────────┐                      │
│  │   1    │   2    │   3    │   5    │                      │
│  │ 2bytes │ 2bytes │ 2bytes │ 2bytes │                      │
│  └────────┴────────┴────────┴────────┘                      │
│    [0]     [1]     [2]     [3]                              │
│                                                              │
│  总内存:4 + 4 + 4 * 2 = 16 字节                             │
│                                                              │
│  如果用传统数组 + 指针:                                     │
│  4 * 8 (指针) + 4 * 2 (值) = 40 字节                         │
│                                                              │
│  内存节省:约 60%                                            │
│                                                              │
└──────────────────────────────────────────────────────────────┘

2.4 编码升级

当添加的新元素类型比现有元素类型更大时,整数集合会进行升级:

// intset.c - 升级并添加元素
static intset *intsetUpgradeAndAdd(intset *is, int64_t value) {
    uint8_t curenc = intrev32ifbe(is->encoding);
    uint8_t newenc = _intsetValueEncoding(value);
    int length = intrev32ifbe(is->length);
    int prepend = value < 0 ? 1 : 0;  // 新元素是负数则插入头部
    
    // 设置新编码
    is->encoding = intrev32ifbe(newenc);
    
    // 重新分配内存
    is = intsetResize(is, intrev32ifbe(is->length) + 1);
    
    // 从后向前移动元素(避免覆盖)
    while (length--)
        _intsetSet(is, length + prepend, 
                   _intsetGetEncoded(is, length, curenc));
    
    // 添加新元素
    if (prepend)
        _intsetSet(is, 0, value);
    else
        _intsetSet(is, intrev32ifbe(is->length), value);
    
    is->length = intrev32ifbe(intrev32ifbe(is->length) + 1);
    return is;
}

升级过程示意图:

┌──────────────────────────────────────────────────────────────┐
│                    整数集合升级过程                           │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  初始状态:encoding = INT16, 元素 [1, 2, 3]                  │
│  ┌────┬────┬───────────────────────┐                        │
│  │ 16 │ 3  │  1  │  2  │  3  │                              │
│  └────┴────┴───────────────────────┘                        │
│   编码  长度  2字节 2字节 2字节                               │
│                                                              │
│  添加元素:65535 (超过 INT16 范围,需要升级到 INT32)         │
│                                                              │
│  Step 1: 扩容(每个元素从 2 字节变成 4 字节)                │
│  ┌────┬────┬─────────────────────────────────────────┐      │
│  │ 32 │ 4  │    │    │    │    │    │    │    │      │      │
│  └────┴────┴─────────────────────────────────────────┘      │
│                                                              │
│  Step 2: 从后往前移动元素                                    │
│  ┌────┬────┬──────────┬──────────┬──────────┬──────────┐    │
│  │ 32 │ 4  │    1     │    2     │    3     │  65535   │    │
│  └────┴────┴──────────┴──────────┴──────────┴──────────┘    │
│   编码  长度   4字节      4字节      4字节      4字节         │
│                                                              │
│  升级特点:                                                  │
│  • 升级后不会降级(节省内存,避免频繁调整)                  │
│  • 升级是 O(N) 操作,但实际元素数量较少,影响不大            │
│                                                              │
└──────────────────────────────────────────────────────────────┘

2.5 整数集合操作

2.5.1 添加元素

// intset.c
intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
    uint8_t valenc = _intsetValueEncoding(value);
    uint32_t pos;
    
    if (success) *success = 1;
    
    // 需要升级编码
    if (valenc > intrev32ifbe(is->encoding)) {
        return intsetUpgradeAndAdd(is, value);
    }
    
    // 查找插入位置(二分查找)
    if (intsetSearch(is, value, &pos)) {
        if (success) *success = 0;  // 元素已存在
        return is;
    }
    
    // 扩容并插入
    is = intsetResize(is, intrev32ifbe(is->length) + 1);
    if (pos < intrev32ifbe(is->length)) {
        // 移动元素腾出空间
        memmove(((int8_t*)is->contents) + (pos + 1) * intrev32ifbe(is->encoding),
                ((int8_t*)is->contents) + pos * intrev32ifbe(is->encoding),
                (intrev32ifbe(is->length) - pos) * intrev32ifbe(is->encoding));
    }
    
    _intsetSet(is, pos, value);
    is->length = intrev32ifbe(intrev32ifbe(is->length) + 1);
    return is;
}

2.5.2 二分查找

// intset.c
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
    int min = 0, max = intrev32ifbe(is->length) - 1, mid = -1;
    int64_t cur = -1;
    
    // 空集合
    if (intrev32ifbe(is->length) == 0) {
        if (pos) *pos = 0;
        return 0;
    }
    
    // 检查边界
    if (value > _intsetGet(is, max)) {
        if (pos) *pos = intrev32ifbe(is->length);
        return 0;
    }
    if (value < _intsetGet(is, 0)) {
        if (pos) *pos = 0;
        return 0;
    }
    
    // 二分查找
    while (max >= min) {
        mid = ((unsigned int)min + (unsigned int)max) >> 1;
        cur = _intsetGet(is, mid);
        if (value > cur) {
            min = mid + 1;
        } else if (value < cur) {
            max = mid - 1;
        } else {
            break;
        }
    }
    
    if (value == cur) {
        if (pos) *pos = mid;
        return 1;
    } else {
        if (pos) *pos = min;
        return 0;
    }
}

2.6 整数集合操作时间复杂度

操作时间复杂度说明
查找O(logN)二分查找
插入O(N)可能需要移动元素或升级
删除O(N)需要移动元素

三、压缩列表(ZipList)

3.1 压缩列表简介

压缩列表是 Redis 为了节约内存而开发的,由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或一个整数值。

┌──────────────────────────────────────────────────────────────┐
│                    压缩列表特点                               │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  ✅ 内存紧凑:连续内存,无指针开销                           │
│  ✅ 双向遍历:每个节点记录前一节点长度                       │
│  ✅ 多种编码:根据数据类型和大小选择最优编码                 │
│  ❌ 连锁更新:中间节点长度变化可能引发连续更新               │
│                                                              │
│  应用场景:                                                  │
│  • List 类型的底层实现之一(元素较少时)                     │
│  • Hash 类型的底层实现(字段和值都较小时)                   │
│  • ZSET 的底层实现(元素较少时)                             │
│                                                              │
└──────────────────────────────────────────────────────────────┘

3.2 压缩列表结构定义

// 压缩列表没有显式的结构体定义,而是通过字节序列来组织
// 整体结构:
// zlbytes | zltail | zllen | entries | zlend

/*
 * zlbytes: 4 字节,整个压缩列表占用的字节数
 * zltail:  4 字节,尾节点偏移量
 * zllen:   2 字节,节点数量(超过 65535 需要遍历统计)
 * entries: 节点列表
 * zlend:   1 字节,0xFF,结束标记
 */

3.3 压缩列表内存布局

┌──────────────────────────────────────────────────────────────┐
│                    压缩列表内存布局                           │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌─────────┬─────────┬─────────┬──────────┬─────────┐       │
│  │ zlbytes │ zltail  │ zllen   │ entries  │  zlend  │       │
│  │ 4 bytes │ 4 bytes │ 2 bytes │  ...     │ 1 byte  │       │
│  └─────────┴─────────┴─────────┴──────────┴─────────┘       │
│                                        │                     │
│                                        ▼                     │
│  entries 区域(节点列表):                                  │
│  ┌──────────────────┬──────────────────┬──────────────────┐ │
│  │    entry 1       │    entry 2       │    entry 3       │ │
│  │ "hello"          │   100            │   "world"        │ │
│  └──────────────────┴──────────────────┴──────────────────┘ │
│                                                              │
│  示例:存储 ["hello", 100, "world"]                         │
│  总内存:4 + 4 + 2 + (1+1+5) + (1+2) + (1+1+5) + 1 = 23 字节 │
│                                                              │
└──────────────────────────────────────────────────────────────┘

3.4 压缩列表节点结构

每个压缩列表节点由三部分组成:

/*
 * 节点结构:
 * +----------------+----------------+----------------+
 * | previous_entry |   encoding     |     content    |
 * |     length     |                |                |
 * +----------------+----------------+----------------+
 */

// previous_entry_length: 记录前一节点的长度,用于从后向前遍历
//    - 如果前一节点长度 < 254 字节,占用 1 字节
//    - 如果前一节点长度 >= 254 字节,占用 5 字节(第一字节为 0xFE)

// encoding: 记录当前节点的数据类型和长度
//    - 字节数组编码:
//      00xxxxxx: 长度 < 64 字节,6 位存储长度
//      01xxxxxx xxxxxxxx: 长度 < 16384 字节,14 位存储长度
//      10xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx: 更长数组
//    - 整数编码:
//      11000000: int16_t (2 字节)
//      11010000: int32_t (4 字节)
//      11100000: int64_t (8 字节)
//      11110000: 24 位有符号整数
//      11111110: 8 位有符号整数
//      1111xxxx: 4 位无符号整数(0-12),xxxx 存储值

节点编码示意图:

┌──────────────────────────────────────────────────────────────┐
│                    压缩列表节点编码                           │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  字节数组编码:                                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ 00xxxxxx    │ 长度 0-63 的字节数组                       ││
│  └─────────────────────────────────────────────────────────┘│
│  ┌─────────────────────────────────────────────────────────┐│
│  │ 01xxxxxx xxxxxxxx │ 长度 0-16383 的字节数组             ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
│  整数编码:                                                  │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ 11000000 │ int16_t (2 字节整数)                         ││
│  │ 11010000 │ int32_t (4 字节整数)                         ││
│  │ 11100000 │ int64_t (8 字节整数)                         ││
│  │ 11110000 │ 24 位有符号整数                               ││
│  │ 11111110 │ 8 位有符号整数                                ││
│  │ 1111xxxx │ 4 位无符号整数 (0-12),xxxx 直接存储值        ││
│  └─────────────────────────────────────────────────────────┘│
│                                                              │
└──────────────────────────────────────────────────────────────┘

3.5 连锁更新问题

压缩列表的 previous_entry_length 字段可能导致连锁更新:

┌──────────────────────────────────────────────────────────────┐
│                    连锁更新问题                               │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  初始状态:多个连续节点,每个长度约 250 字节                 │
│                                                              │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐            │
│  │ e1      │ │ e2      │ │ e3      │ │ e4      │            │
│  │ 250字节 │ │ 250字节 │ │ 250字节 │ │ 250字节 │            │
│  │ prev=1  │ │ prev=1  │ │ prev=1  │ │ prev=1  │            │
│  └─────────┘ └─────────┘ └─────────┘ └─────────┘            │
│                                                              │
│  在 e1 前插入一个大节点(长度 300 字节):                   │
│                                                              │
│  Step 1: e1 的 prev 从 1 字节变成 5 字节                     │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐            │
│  │ new     │ │ e1      │ │ e2      │ │ e3      │            │
│  │ 300字节 │ │ 250+4   │ │ 250字节 │ │ 250字节 │            │
│  │ prev=1  │ │ prev=5! │ │ prev=1  │ │ prev=1  │            │
│  └─────────┘ └─────────┘ └─────────┘ └─────────┘            │
│                         │                                    │
│                         ▼                                    │
│  Step 2: e1 长度变长,e2 的 prev 也要从 1 变成 5 字节        │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐            │
│  │ new     │ │ e1      │ │ e2      │ │ e3      │            │
│  │ 300字节 │ │ 254字节 │ │ 250+4   │ │ 250字节 │            │
│  │ prev=1  │ │ prev=5  │ │ prev=5! │ │ prev=1  │            │
│  └─────────┘ └─────────┘ └─────────┘ └─────────┘            │
│                                   │                          │
│                                   ▼                          │
│  Step 3: 连锁更新继续...                                     │
│                                                              │
│  最坏情况:O(N²) 的时间复杂度                                │
│  实际情况:概率很低,Redis 认为可接受                        │
│                                                              │
└──────────────────────────────────────────────────────────────┘

3.6 压缩列表操作

3.6.1 创建压缩列表

// ziplist.c
unsigned char *ziplistNew(void) {
    unsigned int bytes = ZIPLIST_HEADER_SIZE + ZIPLIST_END_SIZE;
    unsigned char *zl = zmalloc(bytes);
    
    // 设置各字段
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    ZIPLIST_LENGTH(zl) = 0;
    
    // 设置结束标记
    zl[bytes - 1] = ZIP_END;
    
    return zl;
}

3.6.2 插入元素

// ziplist.c - 简化的插入逻辑
unsigned char *ziplistInsert(unsigned char *zl, unsigned char *p, 
                             unsigned char *s, unsigned int slen) {
    size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl));
    unsigned int prevlensize, prevlen;
    unsigned int reqlen;
    unsigned char encoding = 0;
    
    // 获取前一节点长度
    zipPrevLenByteDiff(p, &prevlensize, &prevlen);
    
    // 计算需要的空间
    if (slen == 0 || s == NULL) {
        // 整数
        reqlen = zipTryEncoding(s, slen, &encoding);
    } else {
        // 字节数组
        reqlen = slen;
    }
    reqlen += zipStorePrevEntryLength(NULL, prevlen);
    reqlen += zipStoreEntryEncoding(NULL, encoding, slen);
    
    // 检查是否需要连锁更新
    int forcelarge = 0;
    unsigned int newprevlen = reqlen;
    unsigned char *next = p + reqlen;
    
    // 重新分配内存并插入
    zl = ziplistResize(zl, curlen + reqlen);
    memmove(p + reqlen, p, curlen - (p - zl));
    
    // 写入新节点
    p += zipStorePrevEntryLength(p, prevlen);
    p += zipStoreEntryEncoding(p, encoding, slen);
    if (s != NULL) {
        memcpy(p, s, slen);
    }
    
    // 更新后继节点的 prevlen
    // ... 连锁更新处理
    
    ZIPLIST_INCR_LENGTH(zl, 1);
    return zl;
}

3.7 ListPack(压缩列表的改进版)

Redis 7.0 引入了 ListPack 来替代 ZipList,解决了连锁更新问题:

┌──────────────────────────────────────────────────────────────┐
│                    ListPack vs ZipList                       │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  ZipList 节点结构:                                          │
│  ┌────────────────┬──────────┬───────────┐                  │
│  │ prev_node_len  │ encoding │  content  │                  │
│  └────────────────┴──────────┴───────────┘                  │
│         ↑                                                    │
│   记录前一节点长度 → 连锁更新的根源                          │
│                                                              │
│  ListPack 节点结构:                                         │
│  ┌──────────┬───────────┬───────────────┐                   │
│  │ encoding │  content  │ backlen       │                   │
│  └──────────┴───────────┴───────────────┘                   │
│                              ↑                               │
│                   记录当前节点长度(用于回溯)               │
│                                                              │
│  优势:                                                      │
│  • 消除了连锁更新问题                                        │
│  • 节点独立,修改不影响其他节点                              │
│  • 内存效率更高                                              │
│                                                              │
└──────────────────────────────────────────────────────────────┘

四、数据结构对比总结

┌──────────────────────────────────────────────────────────────┐
│                    Redis 核心数据结构对比                     │
├───────────────┬──────────────┬───────────────┬───────────────┤
│ 数据结构      │ 时间复杂度   │ 内存效率      │ 应用场景      │
├───────────────┼──────────────┼───────────────┼───────────────┤
│ 跳跃表        │ O(logN)      │ 中等          │ ZSET          │
│ 整数集合      │ O(logN)~O(N) │ 高            │ SET(小整数) │
│ 压缩列表      │ O(N)         │ 极高          │ 小规模数据    │
│ ListPack      │ O(N)         │ 极高          │ 替代压缩列表  │
└───────────────┴──────────────┴───────────────┴───────────────┘

五、总结

本章深入分析了 Redis 三种核心数据结构:

数据结构核心特点时间复杂度主要用途
跳跃表多层索引、随机层数O(logN)有序集合
整数集合编码升级、二分查找O(logN)~O(N)小整数集合
压缩列表内存紧凑、连锁更新O(N)小规模数据存储

这些数据结构体现了 Redis 在内存效率和查询性能之间的精妙平衡。下一章将深入分析 Redis 的对象系统。

参考资料

分享: