Redis系列-跳表源码分析


摘要:跳表是一种具有 O ( l o g n ) O(logn) O(logn)级别的查询时间复杂度的数据结构,redis的有序集合在数据比较多或者单个数据比较大的时候底层使用的数据结构就是跳表。跳表相比于其他查询结构如红黑树、B树、B+树等等,具有易于实现的特点。LeetCode上没有手写红黑树或者B树的题目,但是有题目要求手写跳表,可见其相对易于实现。本文将以redis里的跳表源码来分析跳表这种数据结构,所使用的redis版本为6.0.19。
关键词:Redis,源码分析,跳表,数据结构,leetcode

源码分析

结构体

首先跳表结构体定义在server.h文件中,代码如下:

// 跳表节点定义
typedef struct zskiplistNode {
    sds ele;   // 节点名称
    double score;   // 节点分数
    struct zskiplistNode *backward;   // 后向指针,跳表的最低层是一个双向链表
    struct zskiplistLevel {
        struct zskiplistNode *forward;  // 前向指针
        unsigned long span;
    } level[];  // 这里把不同level的节点聚合在一起,减少内存
} zskiplistNode;

// 跳表结构体定义
typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

typedef struct zset {
    dict *dict;
    zskiplist *zsl;   // 指向跳表的指针
} zset;

接下来从高层的zset的角度来看一下跳表的创建、查询和插入等操作。

跳表创建和销毁

创建一个跳表的代码如下:

// 创建一个跳表节点
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
    // 按照level数量分配内存
    zskiplistNode *zn =
        zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
    zn->score = score;
    zn->ele = ele;
    return zn;
}

/* Create a new skiplist. */
// 创建一个跳表
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;

    zsl = zmalloc(sizeof(*zsl));   // 分配空间
    zsl->level = 1;
    zsl->length = 0;
    // 这里相当于是创建一个伪头部,其高度为32
    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;
}

这里使用了一个伪头部便于后续的操作,减少一些条件的判断。再来看下跳表的销毁,销毁链表是遍历最底层的链表,因为所有的节点都至少出现在最低层。

void zslFreeNode(zskiplistNode *node) {
    sdsfree(node->ele);
    zfree(node);
}

/* Free a whole skiplist. */
void zslFree(zskiplist *zsl) {
    zskiplistNode *node = zsl->header->level[0].forward, *next;

    zfree(zsl->header);
    while(node) {   // 遍历最底层的双向链表
        next = node->level[0].forward;
        zslFreeNode(node);
        node = next;
    }
    zfree(zsl);
}

下图为redis 一个跳表的示意图。
在这里插入图片描述

zslInsert 跳表插入

// 返回一个随机的level,表示要插入的节点多少level高
int zslRandomLevel(void) {
    int level = 1;
    // ZSKIPLiST_P等于0.25,相当于每一个节点有四分之一的概率往上一层
    // 这里的实现相当于是random() < ZSKIPLIST_P, 这里只是利用位运算来提高浮点数比较的效率
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    // 最高只能是32层
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

/* Insert a new node in the skiplist. Assumes the element does not already
 * exist (up to the caller to enforce that). The skiplist takes ownership
 * of the passed SDS string 'ele'. */
// 往跳表中插入一个节点,需要由调用方保证待插入的元素不存在于跳表中
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    serverAssert(!isnan(score));  // 参数校验
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {   // 从最高层次开始
        /* store rank that is crossed to reach the insert position */
        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;   // rank[i]记录每一层经过了多少元素找到的位置,后续用于更新跨度span
            x = x->level[i].forward;
        }
        update[i] = x;   // update保存的是每一层的更新节点,即待插入节点如果在该层的上一个节点
    }
    /* we assume the element is not already inside, since we allow duplicated
     * scores, reinserting the same element should never happen since the
     * caller of zslInsert() should test in the hash table if the element is
     * already inside or not. */
    // 到这里已经找到待插入节点在最底层要插入的位置了
    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++) {
        // 将当前节点插入到update[i]节点后面
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;

        /* update span covered by update[i] as x is inserted here */
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

    /* increment span for untouched levels */
    for (i = level; i < zsl->level; i++) {   // 高度不够,未设计到的层次的对应节点的跨度也要加一
        update[i]->level[i].span++;
    }
    // 后向指针,相当于双向链表中的prev,不会指向伪头部
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    // 使后一个节点的后向指针指向自己
    if (x->level[0].forward)
        x->level[0].forward->backward = x;  
    else
        zsl->tail = x;  // 插入的节点在末尾,更新tail
    zsl->length++;
    return x;
}

总结一下一个节点插入到跳表的过程:

  1. 找到待插入的位置。从最高层次的伪头部开始,如果前向指针不为空且前向指针指向的节点的分数小于待插入节点的分数(或者等于,但是名称要小于),则在当前层次中往后遍历。如果不满足,则下落到下一层次,继续进行以上过程,直到最底层,在这一过程中记录每一层次下落前的节点,新插入节点插入在这些节点的后面
  2. 创建节点,并且根据概率决定该节点的层次数,大概是该节点具有n层次的概率为 1 / 4 n 1 / 4^n 1/4n
  3. 然后根据这个层次将该节点插入到第一步收集的节点的后面
  4. 更新最底层的后向指针,因为某些操作的需要,最底层维护成一个双向链表

以下图为例,在之前所说的那个跳表中,如果要插入一个score为7的节点,那么节点的遍历过程如下图的蓝色箭头,而蓝色圆圈所标识的位置就是新节点要插入的位置。
在这里插入图片描述

zslDelete 跳表删除

// 具体完成一个节点的删除,且更新所在层次的链表
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    int i;
    for (i = 0; i < zsl->level; i++) {
        if (update[i]->level[i].forward == x) {  // 说明待删除节点在这一个层次
            update[i]->level[i].span += x->level[i].span - 1;  // 更新跨度
            update[i]->level[i].forward = x->level[i].forward;  // 更新前一个节点的前向指针
        } else {
            update[i]->level[i].span -= 1;   // 待删除节点没有到这一个层次,只需要将跨度减一即可
        }
    }
    // 更新最底层的双向链表
    if (x->level[0].forward) {  // 不是最后一个节点
        x->level[0].forward->backward = x->backward;
    } else {   // 删除了最后一个节点,需要更新tail
        zsl->tail = x->backward;
    }
    while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)  // 最高层次更新
        zsl->level--;
    zsl->length--;  // 跳表中的元素数量更新
}

/* Delete an element with matching score/element from the skiplist.
 * The function returns 1 if the node was found and deleted, otherwise
 * 0 is returned.
 *
 * If 'node' is NULL the deleted node is freed by zslFreeNode(), otherwise
 * it is not freed (but just unlinked) and *node is set to the node pointer,
 * so that it is possible for the caller to reuse the node (including the
 * referenced SDS string at node->ele). */
// 删除节点,返回1表示删除成功,返回0表示跳表中没有这个节点
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    int i;

    x = zsl->header;
    // 还是和插入节点一样的查找过程
    for (i = zsl->level-1; i >= 0; i--) {
        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)))
        {
            x = x->level[i].forward;
        }
        update[i] = x;
    }
    /* We may have multiple elements with the same score, what we need
     * is to find the element with both the right score and object. */
    // 此时的x及往后的部分节点可能是待删除的节点
    x = x->level[0].forward;
    if (x && score == x->score && sdscmp(x->ele,ele) == 0) {
        zslDeleteNode(zsl, x, update);
        // 根据调用方传入的二级指针,如果为空,说明调用方不关心这个节点,直接释放掉,否则要让这个二级指针去执行被删除的节点
        if (!node)
            zslFreeNode(x);
        else
            *node = x;
        return 1;
    }
    return 0; /* not found */
}

具体的过程和插入一个节点的过程类似,也是先最高层次开始往下,每一层链表查找。最后落在最底层的某个节点,判断该节点的下一个节点的score和ele是否和待删除节点的信息一致,如果一致,删除该节点,并且根据查找过程中维护的下落前的各个层次的节点来更新各个层次的单向链表,最后更新最底层的双向链表。如果不一致,则说明跳表中没有待删除节点。

zslUpdateScore 更新节点

zskiplistNode *zslUpdateScore(zskiplist *zsl, double curscore, sds ele, double newscore) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    int i;

    /* We need to seek to element to update to start: this is useful anyway,
     * we'll have to update or remove it. */
    // 一样的查找过程,在这一过程中记录每一层下落前的节点,方便后续修改
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        while (x->level[i].forward &&
                (x->level[i].forward->score < curscore ||
                    (x->level[i].forward->score == curscore &&
                     sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            x = x->level[i].forward;
        }
        update[i] = x;
    }

    /* Jump to our element: note that this function assumes that the
     * element with the matching score exists. */
    x = x->level[0].forward;   // 保证该节点的分数和名称都和参数一致
    serverAssert(x && curscore == x->score && sdscmp(x->ele,ele) == 0);

    /* If the node, after the score update, would be still exactly
     * at the same position, we can just update the score without
     * actually removing and re-inserting the element in the skiplist. */
    // 通过最底层的双向链表判断,更新后的分数是否还能留在原位置,直接更新,然后返回
    if ((x->backward == NULL || x->backward->score < newscore) &&
        (x->level[0].forward == NULL || x->level[0].forward->score > newscore))
    {
        x->score = newscore;
        return x;
    }

    /* No way to reuse the old node: we need to remove and insert a new
     * one at a different place. */
    // 否则只能删除该节点,然后重新插入一个新节点
    zslDeleteNode(zsl, x, update);
    zskiplistNode *newnode = zslInsert(zsl,newscore,x->ele);
    /* We reused the old node x->ele SDS string, free the node now
     * since zslInsert created a new one. */
    // 复用用来保存节点名称的sds,所以将节点指向的ele置为null,然后清理节点
    x->ele = NULL;
    zslFreeNode(x);
    return newnode;
}

更新一个节点的分数大致过程如下:

  1. 找到待插入的位置。从最高层次的伪头部开始,如果前向指针不为空且前向指针指向的节点的分数小于待插入节点的分数(或者等于,但是名称要小于),则在当前层次中往后遍历。如果不满足,则下落到下一层次,继续进行以上过程,直到最底层,在这一过程中记录每一层次下落前的节点,新插入节点插入在这些节点的后面
  2. 取当前节点的前向指针指向的节点作为待更新节点,检查该节点的名称和分数是否和传入的名称、旧分数一致,如果不一致,直接返回,否则进行更新
  3. 在最底层,检查后向指针指向的节点是否为空或者节点的分数小于新分数,以及前向指针指向的节点是否为空或者该节点的分数大于新分数,如果满足,说明新分数的节点的位置不需要调整,直接返回
  4. 否则,需要删除旧节点,然后重新插入一个新节点

跳表实现

实现代码如下:

import (
	"math/rand"
	"time"
)

const (
	MAX_HEIGHT=32
	Rand_LEVEL_P = 0.25
)

type SkiplistNodeLevel struct {
	level int
	next *SkiplistNode   // 指向下一个节点
}


type SkiplistNode struct {
	val int
	prev *SkiplistNode   // 指向前一个节点
	level int
	levels []*SkiplistNodeLevel
}


func NewSkiplistNode(val int, level int) *SkiplistNode {
	node := &SkiplistNode{val: val, levels: make([]*SkiplistNodeLevel, level), level: level}
	for i := 0; i < level; i++ {
		node.levels[i] = &SkiplistNodeLevel{level: i}
	}
	return node
}


func NewSkiplistNodeNoVal(level int) *SkiplistNode {
	node := &SkiplistNode{levels: make([]*SkiplistNodeLevel, level), level: level}
	for i := 0; i < level; i++ {
		node.levels[i] = &SkiplistNodeLevel{level: i}
	}
	return node
}


type Skiplist struct {
	level int   // 除伪头部外最高层次
	length int // 元素数量
	head, tail *SkiplistNode
}


func NewSkiplist() *Skiplist {
	sl := &Skiplist{level: 1, head: NewSkiplistNodeNoVal(MAX_HEIGHT)}
	return sl
}

func (s *Skiplist) Search(target int) bool {
	var cur *SkiplistNode = s.head
	for i := s.level - 1; i >= 0; i-- {
		for cur.levels[i].next != nil && cur.levels[i].next.val < target {
			cur = cur.levels[i].next
		}
	}
	if cur.levels[0].next != nil && cur.levels[0].next.val == target {
		return true
	}
	return false
}

func (s *Skiplist) randomLevel() int {
	rand.Seed(time.Now().UnixNano())
	level := 1
	for (level < MAX_HEIGHT && 0x7fff * rand.Float64() < 0x7fff * Rand_LEVEL_P) {
		level++
	}
	return min(level, MAX_HEIGHT)
}

func (s *Skiplist) Insert(val int) {
	var cur *SkiplistNode = s.head
	prevNodes := make([]*SkiplistNode, MAX_HEIGHT)
	for i := s.level - 1; i >= 0; i-- {
		for cur.levels[i].next != nil && cur.levels[i].next.val < val {
			cur = cur.levels[i].next
		}
		prevNodes[i] = cur
	}

	level := s.randomLevel()
	node := NewSkiplistNode(val, level)

	for i := min(level - 1, s.level - 1); i >= 0; i-- {
		node.levels[i].next = prevNodes[i].levels[i].next
		prevNodes[i].levels[i].next = node
	}
	if level > s.level {
		for i := s.level; i < level; i++ {
			s.head.levels[i].next = node
		}
		s.level = level
	}
	node.prev = prevNodes[0]
	if node.levels[0].next != nil {
		node.levels[0].next.prev = node
	} else {
		s.tail = node
	}
}


func (s *Skiplist) Delete(val int) bool {
	var cur *SkiplistNode = s.head
	prevNodes := make([]*SkiplistNode, MAX_HEIGHT)
	for i := s.level - 1; i >= 0; i-- {
		for cur.levels[i].next != nil && cur.levels[i].next.val < val {
			cur = cur.levels[i].next
		}
		prevNodes[i] = cur
	}
	if cur.levels[0].next == nil || cur.levels[0].next.val != val {
		return false
	}

	deletingNode := cur.levels[0].next

	for i := deletingNode.level - 1; i >= 0; i-- {
		prevNodes[i].levels[i].next = deletingNode.levels[i].next
	}

	if deletingNode.levels[0].next != nil {
		deletingNode.levels[0].next.prev = prevNodes[0]
	} else {
		s.tail = prevNodes[0]
	}
	return true
}

部分函数调整后(leetCode函数名笔者不认可),可以通过LeetCode 1206的所有测试用例,运行截图如下:
在这里插入图片描述

扩展

工业界其他使用跳表的场景还有,基于LSM-Tree的数据库比如LevelDB和RocksDB等等,都有使用跳表。

参考

<think>我们被要求对Redis数据类型的源码进行详细分析,并且需要参考提供的引用内容。根据引用内容,我们知道Redis的数据类型包括String、Hash、List、Set、Sorted Set等,并且这些数据类型是通过不同的底层数据结构组合实现的。主要的底层数据结构有SDS动态字符串、双向链表、压缩列表(ziplist)、哈希表(hashtable)、跳表(skiplist)、整数集合(intset)、快速列表(quicklist)和紧凑列表(listpack)等。 接下来,我们将按照数据类型逐一分析其源码实现,并参考引用中的信息。 1. String(字符串) - 引用[4]提到字符串的编码格式有raw、embstr和int三种。 - 在源码中,字符串类型是通过SDS(Simple Dynamic String)实现的。SDS的结构定义在`sds.h`文件中。 - 三种编码格式: - int编码:当存储的值是整数并且可以用long表示时,会直接存储整数在redisObject的ptr位置。 - embstr编码:当字符串长度小于等于44字节(Redis5.0版本)时,使用embstr编码,该编码将redisObject和SDS结构连续存储,减少内存分配次数。 - raw编码:当字符串长度大于44字节时,使用raw编码,此时redisObject和SDS是分开存储的。 2. List(列表) - 引用[2]提到List的底层数据结构是快速列表(quicklist)。quicklist是ziplist和双向链表的结合,每个节点是一个ziplist。 -Redis3.2之后,List的底层实现统一使用quicklist。源码在`quicklist.h`和`quicklist.c`中。 - 结构定义:quicklist包含指向头尾节点的指针,以及节点数量等信息。每个节点(quicklistNode)包含一个ziplist。 3. Hash(哈希) - 引用[4]提到Hash的底层实现是ziplist或hashtable。 - 当哈希中的元素个数较少且元素长度较短时,使用ziplist存储;否则使用hashtable。 - ziplist存储方式是连续的键值对(key1, value1, key2, value2, ...)。 - hashtable则使用字典(dict)实现,字典的键和值都是SDS字符串。 4. Set(集合) - 引用[4]提到Set的底层实现是intset或hashtable。 - 当集合中元素都是整数且元素数量较少时,使用intset(整数集合)存储。 - 否则使用hashtable,此时字典的键为集合元素,值为NULL。 5. Sorted Set(有序集合) - 引用[4]和引用[5]提到有序集合的底层实现是压缩列表(ziplist)或跳表(skiplist)和字典(dict)的组合(zset结构)。 - 当元素数量较少且元素长度较小时,使用ziplist存储(按分值排序,元素和分值依次存储)。 - 否则使用zset结构,该结构包含一个跳表(zskiplist)和一个字典(dict)。跳表用于支持范围操作(如ZRANGE),字典用于支持按成员快速查找分值(O(1))。 - 跳表结构定义如引用[5]所示,其中zskiplistNode包含元素(sds)、分值(double)、后退指针和层级数组(每个层级有前进指针和跨度)。zskiplist包含头尾指针、长度和最大层级。zset结构则包含一个dict和一个zskiplist。 6. 其他类型:如HyperLogLog、Geo和Streams等,由于问题集中在常用数据类型,这里暂不展开。 对于源码分析,我们需要定位到Redis源码中的具体文件。以下是一些关键文件: - sds.h, sds.c: 字符串实现 - t_hash.c: 哈希表实现 - t_list.c: 列表实现(使用quicklist) - t_set.c: 集合实现 - t_zset.c: 有序集合实现 - 以及底层数据结构的实现:ziplist.c, dict.h, dict.c, intset.c, skiplist.c(实际在server.h中定义结构,在zset操作中使用), quicklist.c, listpack.c(新版本中可能替代ziplist) 由于引用内容较多,我们尽量参考引用中的信息,并结合Redis源码(以Redis 6.0版本为例)进行分析。 下面我们分别详细分析每个数据类型的源码实现,包括其底层数据结构、编码转换条件等。由于篇幅限制,我们将重点放在核心结构和关键函数上。 </think> ### Redis 数据类型源码详细分析 以下从源码层面对 Redis 核心数据类型进行深度解析,结合 Redis 6.0 源码结构,重点分析底层数据结构实现机制。 #### 1. String(字符串) **核心结构**: 在 `sds.h` 中定义 SDS (Simple Dynamic String) 动态字符串结构: ```c struct sdshdr { uint64_t len; // 字符串长度 uint64_t alloc; // 分配的内存空间 unsigned char flags; // 类型标志(SDS_TYPE_8/16/32/64) char buf[]; // 柔性数组存储实际数据 }; ``` **编码方式**(源码 `object.c`): - **INT 编码**:当值为整型时,直接存储在 `redisObject.ptr` 中(指针复用) - **EMBSTR 编码**:字符串 ≤44 字节时,`redisObject` 与 SDS 内存连续分配(减少内存碎片) - **RAW 编码**:字符串 >44 字节时,独立分配 SDS 内存空间 **关键特性**: 预分配策略(减少内存重分配)、二进制安全(`len` 记录长度而非 `\0` 终止)[^2][^4] --- #### 2. Hash(哈希表) **底层结构**(源码 `dict.h`): ```c typedef struct dict { dictType *type; // 类型特定函数 dictht ht[2]; // 双哈希表(用于渐进式rehash) long rehashidx; // rehash进度 } dict; typedef struct dictht { dictEntry **table; // 桶数组 unsigned long size; // 桶数量 unsigned long sizemask; // 掩码(size-1) unsigned long used; // 已用节点数 } dictht; ``` **编码转换**(源码 `t_hash.c`): - **ZIPLIST 编码**:元素数量 ≤512 且值长度 ≤64 字节时,使用连续内存存储键值对 - **HASHTABLE 编码**:超过阈值时转换为哈希表 **优化机制**:渐进式 rehash(操作时分步迁移数据,避免阻塞)[^2] --- #### 3. List(列表) **核心结构**(源码 `quicklist.h`): ```c typedef struct quicklist { quicklistNode *head; quicklistNode *tail; unsigned long count; // 元素总数 unsigned long len; // quicklistNode数量 int fill : 16; // ziplist大小限制 unsigned int compress : 16; // LZF压缩深度 } quicklist; typedef struct quicklistNode { struct quicklistNode *prev; struct quicklistNode *next; unsigned char *zl; // 指向ziplist size_t sz; // ziplist字节大小 unsigned int count : 16; // ziplist元素计数 unsigned int encoding : 2; // 编码类型(RAW/LZF) } quicklistNode; ``` **设计优势**: - 结合双向链表与 ziplist:降低小元素内存开销 - 支持节点压缩(LZF算法)[^2][^5] --- #### 4. Set(集合) **底层实现**(源码 `setpro.c`): - **INTSET 编码**:当元素均为整数时,使用整数数组: ```c typedef struct intset { uint32_t encoding; // 编码类型(INTSET_ENC_INT16/32/64) uint32_t length; // 元素数量 int8_t contents[]; // 柔性数组存储数据 } intset; ``` - **HASHTABLE 编码**:非整型元素或元素数量 >512 时转为哈希表(值设为 NULL) **自动升级**:插入新整数导致类型溢出时,自动扩展编码位数(如 INT16 → INT32) --- #### 5. Sorted Set(有序集合) **核心结构**(源码 `server.h`): ```c typedef struct zset { dict *dict; // 哈希表(维护 member->score 映射) zskiplist *zsl; // 跳表(维护有序性) } zset; typedef struct zskiplistNode { sds ele; // 元素值 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; ``` **协同机制**: - 跳表提供 $O(\log N)$ 范围查询(ZRANGE) - 哈希表提供 $O(1)$ 的单元素分值查询(ZSCORE)[^5] --- #### 6. 类型统一管理 **redisObject 结构**(源码 `redis.h`): ```c typedef struct redisObject { unsigned type:4; // 数据类型(STRING/LIST/HASH等) unsigned encoding:4; // 编码方式(RAW/INT/HASHTABLE等) unsigned lru:24; // LRU时间戳 int refcount; // 引用计数 void *ptr; // 指向实际数据结构 } robj; ``` **关键操作**: - 类型检查:通过 `type` 字段实现命令多态 - 内存回收:引用计数+LRU 淘汰策略 --- ### 编码转换触发条件 | 数据类型 | 编码1 | 阈值条件 | 编码2 | |---------|-------|----------|-------| | Hash | ziplist | field数>512 或 值>64字节 | hashtable | | List | ziplist | 元素>8KB 或 节点压缩深度>2 | quicklist | | Set | intset | 元素>512 或 非整数 | hashtable | | ZSet | ziplist | 元素>128 或 值>64字节 | skiplist+dict | > 阈值可通过 `redis.conf` 中 `hash-max-ziplist-entries` 等参数调整 --- ### 性能优化关键点 1. **小数据压缩**:ziplist 减少内存碎片 2. **写时复制**:robj.refcount 实现共享对象(如 0-9999 的整数字符串) 3. **惰性删除**:异步释放大对象内存 4. **渐进式扩容**:dict.rehashidx 避免大规模 rehash 阻塞[^3]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值