在 Redis 中,有序集合(Sorted Set)中的分数是用来对元素进行排序的关键,而其底层实现主要依赖于跳表(Skip List)和哈希表的结合。下面是关于其底层存储与实现的详细解释:
底层数据结构
-
跳表(Skip List)
- 跳表是一种概率性数据结构,支持快速的插入、删除、查找操作,平均时间复杂度为 O(log N)。
- 在有序集合中,跳表用于维护所有元素的排序信息。每个节点包含一个成员和它对应的分数。
-
哈希表(Hash Table)
- 另一个数据结构是哈希表,用于从成员快速获取其对应的分数。
- 这样可以实现快速的分数查找和更新。
分数的存储
-
双重索引:每个有序集合中的元素在这两个数据结构中都有各自的表示:
- 在跳表中,根据分数进行排序,以支持范围查询等操作。
- 在哈希表中,通过成员名作为键,分数作为值,支持快速的分数读取和更新。
举例说明
假设我们有一个空的有序集合,我们将依次插入一些元素,并观察底层数据结构的变化。
初始状态
开始时,有序集合是空的,没有任何元素。
插入第一个元素
ZADD leaderboard 100 "player1"
-
跳表:
- 创建一个新的节点
player1,其分数为 100。 - 由于这是第一个元素,所以直接插入到跳表中。
- 创建一个新的节点
-
哈希表:
- 在哈希表中添加键值对
"player1"->100。
- 在哈希表中添加键值对
插入第二个元素
ZADD leaderboard 150 "player2"
-
跳表:
- 创建一个新节点
player2,其分数为 150。 - 将
player2插入到跳表中,在player1之后,因为 150 > 100。
- 创建一个新节点
-
哈希表:
- 添加键值对
"player2"->150。
- 添加键值对
插入第三个元素
ZADD leaderboard 120 "player3"
-
跳表:
- 创建一个新节点
player3,其分数为 120。 - 将
player3插入到跳表中,在player1和player2之间,即player1<player3<player2。
- 创建一个新节点
-
哈希表:
- 添加键值对
"player3"->120。
- 添加键值对
ZADD 操作源码分析
-
关键流程
-
查找或创建节点:
- 使用哈希表判断成员是否已经存在。
- 如果存在,则根据
xx/nx参数决定是更新分数还是直接返回。 - 如果不存在,则创建一个新的跳表节点和哈希表条目。
-
更新跳表:
- 新元素或分数变化导致位置变化时,需要在跳表中找到正确的插入位置。
- 跳表通过多级索引快速定位插入点,随后插入新节点。
-
更新哈希表:
- 同步更新哈希表中的成员分数映射。
-
-
源码片段探讨
在 Redis 的源代码文件
t_zset.c中,实现了有序集合的各种操作。zaddGenericCommand函数负责处理 ZADD 命令:void zaddGenericCommand(client *c, int flags) { ... // 解析参数,初始化变量 zobj = lookupKeyWrite(c->db,c->argv[1]); if (zobj == NULL) { zobj = createZsetObject(); dbAdd(c->db,c->argv[1],zobj); } else { if (checkType(c,zobj,OBJ_ZSET)) return; } zsetConvert(zobj,ZSKIPLIST); for (j = 0; j < elements; j++) { ele = c->argv[scoreidx+1+j*2]; score = scores[j]; /* 更新或插入跳表 */ if (zzlInsert(zobj,ele,score,&incr,flags,&newscore)) updated++; } if (updated) server.dirty++; addReplyLongLong(c,updated); }在这段代码中:
lookupKeyWrite:查找集合对象,如果不存在则创建一个新的 ZSET 对象。zsetConvert:确保当前对象为跳表实现。zzlInsert:核心插入逻辑,在跳表中插入或更新节点,并根据需要调整位置。
补充:xx/nx 参数
在 Redis 的 ZADD 命令中,如果不指定 NX 或 XX 选项,默认的行为是既插入新成员又更新已有成员。也就是说,默认情况下:
如果成员已经存在,则更新其分数。- 如果成员不存在,则添加新的成员及其分数。
在 Redis 的 ZADD 命令中,XX 和 NX 是用来控制元素插入和更新行为的选项:
-
NX(Not eXists) :
- 仅在指定的成员不存在时才添加。
如果成员已经存在,则不执行任何操作,也不更新分数。- 常用于
确保不会意外覆盖已有的成员及其分数。
-
XX(eXists) :
- 仅在指定的成员已经存在时才更新分数。
- 如果成员不存在,则不执行任何操作。
- 常用于需要更新特定已有成员的分数,而不希望插入新成员的场景。
使用示例
假设我们有一个有序集合 leaderboard:
-
使用 NX:仅在成员不存在时才添加
ZADD leaderboard NX 100 "player1"如果
"player1"已经存在,则这条命令不会改变有序集合。 -
使用 XX:仅在成员存在时才更新分数
ZADD leaderboard XX 200 "player1"如果
"player1"不存在,则这条命令不会对有序集合产生影响。