今天这篇文章中,我们主要是来聊聊关于Redis的zset数据结构。
聊这款数据结构之前,我想先讲讲自己为什么要学习这款数据结构的背后知识体系。
说实话,这个数据结构之前我可能只是单纯地知道它该怎么调用,然后在某些场景下其底层的实现原理是采用了ziplist,在某些场景下会使用到skiplist,但是具体的原理和细节,了解的并不深入。
但是最近的好几次项目需求开发中,发现在工作中开始渐渐使用到了这类数据结构,例如在落地推荐系统的时候,会将用户的信息匹配度使用zset结构来进行存储。在某些特殊的业务场景下使用了zset来存储心跳信息等等。
zset的使用介绍
redis 有序集合zset和集合set一样也是string类型元素的集合,且不允许重复的成员。
不同的是 zset 的每个元素都会关联一个分数(分数可以重复),redis 通过分数来为集合中 的成员进行从小到大的排序。
这是一种有序的集合,可以进行排序,但是内部的元素都是唯一的。它和普通的set不一样之处在于,该集合会给每个元素添加一个分数,排序的时候按照这个分数来进行排序。
插入元素
zadd key score member
> zadd test-zset 19 key-19
(integer) 1
获取相关元素的value值
zscore key member
> zscore test-zset key-19
19.0
获取某个元素在对应大小区间段内的排序信息
zrange key start stop [withscores]
> zrange test-zset 0 10 withscores
1) "key-19"
2) 19.0
将某个zset内部的集合按照得分大小进行排序
zrevrange key start end
> zrange test-zset 0 10
1) "key-19"
移除某个zset内部的一些指定key
zrem key members
> zrem test-zset key-19
1
zset这类数据结构在底层实现的时候我们可以通过使用object encodeing和type指令观察一些基础属性。
来看接下来的这段代码案例:
127.0.0.1:6379> zadd zset-1 10 key1
(integer) 1
127.0.0.1:6379> zadd zset-2 11 key2
(integer) 1
127.0.0.1:6379> zadd zset-3 12 key3
(integer) 1
127.0.0.1:6379> zadd zset 12 key1
(integer) 1
127.0.0.1:6379> zadd zset 13 key2
(integer) 1
127.0.0.1:6379> zadd zset 14 key3
(integer) 1
127.0.0.1:6379> type zset
zset
127.0.0.1:6379> object encoding zset
"ziplist"
127.0.0.1:6379> zadd zset 15 ddddddddddddddddddddddddddddddddddddddddddd
ddddddddddddddddddddddddddddd
(integer) 1
127.0.0.1:6379> object encoding zset
"skiplist"
127.0.0.1:6379>
通过这段案例之后你会发现,当往zset中插入过长的元素之后,对应的zset编码格式会从ziplist转变为skiplist类型。
那么什么是skiplist呢?
SkipList的基本结构
直接通过一张图我们来了解下skipList数据结构长什么样子吧:
从上边的图中我们可以看到,跳表在进行设计的时候额外用了一部分的存储空间来存储索引信息数据。通过索引信息来查询对应的具体数据。
假设程序需要查询图中的40这个key,那么此时只需要通过匹配,查询的时候定位到23,30,34三个数字,然后便可定位到具体的区间段内,
跳表的核心思想是通过了空间换时间的思路来减少无谓的查询。
我们也可以归纳一下:
假设存储了n个元素的情况下,
第一层索引的节点个数:n/2
第二层索引的节点个数:n/2 ^ 2
第三层索引的节点个数:n/2 ^ 3
第k层索引的节点个数:n/2 ^ k
所以如果在使用skiplist的时候,一昧地给它新增索引层数,有可能会导致空间开销越来越大,因此索引的层数必须要有一个数字大小的上限控制。
Redis在底层是如何实现SkipList的?
zadd命令出处 server.c 代码文件中可以查找到:
{"zadd",zaddCommand,-4,
"write use-memory fast @sortedset",
0,NULL,1,1,1,0,0,0},
zaddCommand点击进去深入了解其中的实现:
void zaddCommand(client *c) {
zaddGenericCommand(c,ZADD_IN_NONE);
}
继续深入定位zaddGenericCommand函数:
/* This generic command implements both ZADD and ZINCRBY. */
void zaddGenericCommand(client *c, int flags) {
static char *nanerr = "resulting score is not a number (NaN)";
robj *key = c->argv[1];
robj *zobj;
sds ele;
double score = 0, *scores = NULL;
int j, elements, ch = 0;
int scoreidx = 0;
/* The following vars are used in order to track what the command actually
* did during the execution, to reply to the client and to trigger the
* notification of keyspace change. */
int added = 0; /* Number of new elements added. */
int updated = 0; /* Number of elements with updated score. */
int processed = 0; /* Number of elements processed, may remain zero with
options like XX. */
/* Parse options. At the end 'scoreidx' is set to the argument position
* of the score of the first score-element pair. */
scoreidx = 2;
while(scoreidx < c->argc) {
char *opt = c->argv[scoreidx]->ptr;
if (!strcasecmp(opt,"nx")) flags |= ZADD_IN_NX;
else if (!strcasecmp(opt,"xx")) flags |= ZADD_IN_XX;
else if (!strcasecmp(opt,"ch")) ch = 1; /* Return num of elements added or updated. */
else if (!strcasecmp(opt,"incr")) flags |= ZADD_IN_INCR;
else if (!strcasecmp(opt,"gt")) flags |= ZADD_IN_GT;
else if (!strcasecmp(opt,"lt")) flags |= ZADD_IN_LT;
else break;
scoreidx++;
}
/* Turn options into simple to check vars. */
int incr = (flags & ZADD_IN_INCR) != 0;
int nx = (flags & ZADD_IN_NX) != 0;
int xx = (flags & ZADD_IN_XX) != 0;
int gt = (flags & ZADD_IN_GT) != 0;
int lt = (flags & ZADD_IN_LT) != 0;
/* After the options, we expect to have an even number of args, since
* we expect any number of score-element pairs. */
elements = c->argc-scoreidx;
if (elements % 2 || !elements) {
addReplyErrorObject(c,shared.syntaxerr);
return;
}
elements /= 2; /* Now this holds the number of score-element pairs. */
/* Check for incompatible options. */
if (nx && xx) {
addReplyError(c,
"XX and NX options at the same time are not compatible");
return;
}
if ((gt && nx) || (lt && nx) || (gt && lt)) {
addReplyError(c,
"GT, LT, and/or NX options at the same time are not compatible");
return;
}
/* Note that XX is compatible with either GT or LT */
if (incr && elements > 1) {
addReplyError(c,
"INCR option supports a single increment-element pair");
return;
}
/* Start parsing all the scores, we need to emit any syntax error
* before executing additions to the sorted set, as the command should
* either execute fully or nothing at all. */
scores = zmalloc(sizeof(double)*elements);
for (j = 0; j < elements; j++) {
if (getDoubleFromObjectOrReply(c,c->argv[scoreidx+j*2],&scores[j],NULL)
!= C_OK) goto cleanup;
}
/* Lookup the key and create the sorted set if does not exist. */
zobj = lookupKeyWrite(c->db,key);
if (checkType(c,zobj,OBJ_ZSET)) goto cleanup;
if (zobj == NULL) {
if (xx) goto reply_to_client; /* No key + XX option: nothing to do. */
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
zobj = createZsetObject();
} else {
// 这里可以看到当某个元素的value值大于一定阈值的时候,就会采用skiplist数据结构
zobj = createZsetZiplistObject();
}
//将该数据结构添加到字典里面去
dbAdd(c->db,key,zobj);
}
for (j = 0; j < elements; j++) {
double newscore;
score = scores[j];
int retflags = 0;
ele = c->argv[scoreidx+1+j*2]->ptr;
//追加元素
int retval = zsetAdd(zobj, score, ele, flags, &retflags, &newscore);
if (retval == 0) {
addReplyError(c,nanerr);
goto cleanup;
}
if (retflags & ZADD_OUT_ADDED) added++;
if (retflags & ZADD_OUT_UPDATED) updated++;
if (!(retflags & ZADD_OUT_NOP)) processed++;
score = newscore;
}
server.dirty += (added+updated);
reply_to_client:
if (incr) { /* ZINCRBY or INCR option. */
if (processed)
addReplyDouble(c,score);
else
addReplyNull(c);
} else { /* ZADD. */
addReplyLongLong(c,ch ? added+updated : added);
}
cleanup:
zfree(scores);
if (added || updated) {
signalModifiedKey(c,c->db,key);
notifyKeyspaceEvent(NOTIFY_ZSET,
incr ? "zincr" : "zadd", key, c->db->id);
}
}
通过上述的代码阅读,主要关注的是一个叫做zsetAdd函数,
int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, double *newscore)
这个函数的底层内部涉及到了插入数据的判断,内部有一个核心模块叫做zzlInset
int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, double *newscore) {
/* Turn options into simple to check vars. */
int incr = (in_flags & ZADD_IN_INCR) != 0;
int nx = (in_flags & ZADD_IN_NX) != 0;
int xx = (in_flags & ZADD_IN_XX) != 0;
int gt = (in_flags & ZADD_IN_GT) != 0;
int lt = (in_flags & ZADD_IN_LT) != 0;
*out_flags = 0; /* We'll return our response flags. */
double curscore;
/* NaN as input is an error regardless of all the other parameters. */
if (isnan(score)) {
*out_flags = ZADD_OUT_NAN;
return 0;
}
/* Update the sorted set according to its encoding. */
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *eptr;
if ((eptr = zzlFind(zobj->ptr,ele,&curscore)) != NULL) {
/* NX? Return, same element already exists. */
if (nx) {
*out_flags |= ZADD_OUT_NOP;
return 1;
}
/* Prepare the score for the increment if needed. */
if (incr) {
score += curscore;
if (isnan(score)) {
*out_flags |= ZADD_OUT_NAN;
return 0;
}
}
/* GT/LT? Only update if score is greater/less than current. */
if ((lt && score >= curscore) || (gt && score <= curscore)) {
*out_flags |= ZADD_OUT_NOP;
return 1;
}
if (newscore) *newscore = score;
/* Remove and re-insert when score changed. */
if (score != curscore) {
zobj->ptr = zzlDelete(zobj->ptr,eptr);
zobj->ptr = zzlInsert(zobj->ptr,ele,score);
*out_flags |= ZADD_OUT_UPDATED;
}
return 1;
} else if (!xx) {
/* Optimize: check if the element is too large or the list
* becomes too long *before* executing zzlInsert. */
zobj->ptr = zzlInsert(zobj->ptr,ele,score);
if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries ||
sdslen(ele) > server.zset_max_ziplist_value)
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
if (newscore) *newscore = score;
*out_flags |= ZADD_OUT_ADDED;
return 1;
} else {
*out_flags |= ZADD_OUT_NOP;
return 1;
}
}
//跳表类型数据结构的插入
else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
zskiplistNode *znode;
dictEntry *de;
//查找是否存在该类数据结构
de = dictFind(zs->dict,ele);
if (de != NULL) {
/* NX? Return, same element already exists. */
if (nx) {
*out_flags |= ZADD_OUT_NOP;
return 1;
}
//查询对应元素的得分
curscore = *(double*)dictGetVal(de);
/* Prepare the score for the increment if needed. */
if (incr) {
score += curscore;
if (isnan(score)) {
*out_flags |= ZADD_OUT_NAN;
return 0;
}
}
/* GT/LT? Only update if score is greater/less than current. */
if ((lt && score >= curscore) || (gt && score <= curscore)) {
*out_flags |= ZADD_OUT_NOP;
return 1;
}
if (newscore) *newscore = score;
/* Remove and re-insert when score changes. */
if (score != curscore) {
//如果元素相同,但是得分不同,则需要更新该元素的得分 先删除再插入
znode = zslUpdateScore(zs->zsl,curscore,ele,score);
/* Note that we did not removed the original element from
* the hash table representing the sorted set, so we just
* update the score. */
dictGetVal(de) = &znode->score; /* Update score ptr. */
*out_flags |= ZADD_OUT_UPDATED;
}
return 1;
} else if (!xx) {
ele = sdsdup(ele);
znode = zslInsert(zs->zsl,score,ele);
serverAssert(dictAdd(zs->dict,ele,&znode->score) == DICT_OK);
*out_flags |= ZADD_OUT_ADDED;
if (newscore) *newscore = score;
return 1;
} else {
*out_flags |= ZADD_OUT_NOP;
return 1;
}
} else {
serverPanic("Unknown sorted set encoding");
}
return 0; /* Never reached. */
}
从源代码可以看到zset的底层编码会随着元素的数目和member的长度大小发生变化:
这段函数的内部具体逻辑也是比较复杂的,内部有一个if判断,其中当当前采用了SkipList的编码格式,则会判断所需要插入的元素是否已存在,如果是已经存在的元素,则只需要更新对应的score得分数值即可,这部分更新操作被作者封装在了这么一个函数当中:
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. */
x->ele = NULL;
zslFreeNode(x);
return newnode;
}
通过阅读了这段代码之后,我才明白,当频繁对一个zset进行相通key的score进行更新的时候,其底层是会对该元素进行先删除再更新的操作。
哦,那么是否说这样的删除插入操作会对整个结构的性能造成影响呢?(因为工作中确实有在项目里面这么使用过,客户端频繁发送请求去更新zset内部的一个score数值)后来查阅了相关资料发现:
在 Redis 5.0 版本中,Redis 的作者 Antirez 优化了这个更新的过程,目前的更新过程是如果判断这个 value是否存在,如果存在的话就直接更新,然后再调整整个跳跃表的 score 排序,这样就不需要两次的搜索过程。
如果存在相同scope的元素时候,该如何排序?
在比较时,不仅比较分数(相当于skiplist的key),还比较数据本身。在Redis的skiplist实现中,数据本身的内容唯一标识这份数据,而不是由key来唯一标识。另外,当多个元素分数相同的时候,还需要根据数据内容来进字典排序。
接下来我们继续深入学习。
在skiplist数据结构的底层当中,有一个叫做跳表节点的概念,核心源代码部分为以下模块:
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span; //每个索引层之间的索引节点之间的数据间隔
} level[]; //索引层
} zskiplistNode;
score 是对应的得分,
ele 是对应的key,
level[] 是对应的索引层集合,
forward 这个指针用于指向下一个元素,
backward 这个指针用于指向上一个元素,
span 是两个不同索引节点之间的间隔数目
可以看出在底层的链表当中,元素之间的链接是通过指针的方式进行引用,从这里可以看出zset底层存储的数据是存储在了一个链式结构中,而并非是数组结构中,这种结构的好处在于频繁插入或者更新的时候只需要做简单的指针调整即可,相比于数组而言开销要低很多。
深入看看zskiplist这个结构体当中,结构体内部有一个头结点指针,还有一个尾节点指针,设计两个不同类型的指针有个好处就是在对队列元素按照得分排序查询的时候,不论是倒序还是顺序排序都能够很快就计算出来。
typedef struct zskiplist {
//zrange和zrerange的遍历顺序关键是根据这两个指针去处理的
struct zskiplistNode *header, *tail;
unsigned long length; // 索引的个数
int level //层高度
} zskiplist;
关键信息在源代码当中的一些XXXRange函数中可以看到相关的影子。
最后我们再看看zSet集合的结构体设计:
typedef struct zset {
dict *dict; //为了快速索引到对应记录
zskiplist *zsl; //存储有序的数据
} zse
从这个结构体中可以看出zset结构中有对应的zskiplist类型。现在我们再去使用zset结构之后可能就会更加了解内部的机制了。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/5XJbxMmIk7uWnWtDqAus3Q
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。