目录
在使用zset做一个临时消息列表需求时,有涉及在lua中操作zset的场景,有点担心zset操作会影响脚本性能,因此本文会对zset操作时间复杂度进行总结。
本文以5.1.x 版本为参考,从操作时间复杂度的角度进行梳理,总结关于zset一些常用操作使用时一些注意事项。
下载源码后,zset相关操作实现源码在t_zset.c
源码文件。
底层数据结构
底层实现结合了 压缩列表(ziplist)和 跳跃表 (skiplist)。
bash
# 查看编码类型,可以看到使用了哪种集合。
OBJECT ENCODING myzset
(1)压缩列表(ziplist):在满足下述条件阈值时使用压缩列表。
- 当元素数量 ≤
zset-max-ziplist-entries
(默认128)且每个元素的长度 ≤zset-max-ziplist-value
(默认64字节)时使用。 - 每个元素由 两个连续的压缩列表节点 存储:
- 第一个节点保存成员(member),第二个节点保存分值(score),例如,插入
(member1, score1)
时,ziplist
的布局如: [member1][score1][member2][score2]...
- 第一个节点保存成员(member),第二个节点保存分值(score),例如,插入
(2)跳跃表 (skiplist)和 字典(hash): 超过上述阈值时,则会转为跳跃表和哈希表的组合结构。
hash
用来存储value
到score
的映射,因此可以在O(1)
时间复杂度内找到value
对应的分数;skipList
每个元素的值都是[socre,value]
**对,按从小到大的顺序进行存储;skipList
可以保证增、删、查等操作时的时间复杂度为O(logN)
;
c
// server.h 文件,zset的结构定义:
typedef struct zset {
dict *dict; // 哈希表实现,维护 member->score 的映射(O(1) 查找)
zskiplist *zsl; // 按 score 排序的跳表(支持范围查询)
} zset;
// server.h 文件,跳表zskiplist 的结构定义:
typedef struct zskiplist {
// 指向跳表的头节点、尾节点
struct zskiplistNode *header, *tail;
// 记录跳表的长度
PORT_ULONG length;
// 记录当前跳表内,所有节点中层数最大的level
int level;
} zskiplist;
// server.h 文件,跳表节点的结构定义:
typedef struct zskiplistNode {
sds ele; // 元素
double score; // 分数
struct zskiplistNode *backward; // 指向当前节点的后退指针,用于从后往前遍历跳表
struct zskiplistLevel {
struct zskiplistNode *forward; // 指向同一层的下一个节点
PORT_ULONG span; // 跨度, 可用来计算当前节点在跳表中的一个排名,为zset提供了查看排名的方法
} level[]; // 数组存储每一层的向前指针和跨度信息,用于实现跳表的快速查找特性
} zskiplistNode;
t_zset.c
源码文件包含与有序集合(zset)相关的操作实现:
c
// 根据给定的层数、分数和元素创建一个新的跳表节点
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;
}
#define ZSKIPLIST_MAXLEVEL 64 /* Should be enough for 2^64 elements */
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
// 随机层数生成算法
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
...
Ziplist -> Skiplist 转换阈值可通过zset-max-ziplist-entries
和zset-max-ziplist-value
配置。(下文zadd源码部分涉及这两个参数)
常用操作时间复杂度
Zscore
时间复杂度 O(1)
核心查找函数:
(1)压缩列表(ziplist)查找 :需要遍历压缩列表(默认 ≤ 128 个元素)查找成员member,复杂度是O(N)
。但是此处 N 为常数且元素很少,因此也可以视为时间复杂度 O(1)
。
(2)跳跃表(skiplist)查找:使用字典dict作为成员member到分数score的映射,dictFind
时间复杂度 O(1)
。
c
int zsetScore(robj *zobj, sds member, double *score) {
if (!zobj || !member) return C_ERR;
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
// 压缩列表编码的情况
if (zzlFind(zobj->ptr, member, score) == NULL) return C_ERR;
} else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
// 跳跃表编码的情况
zset *zs = zobj->ptr;
dictEntry *de = dictFind(zs->dict, member);
if (de == NULL) return C_ERR;
*score = *(double*)dictGetVal(de);
} else {
serverPanic("Unknown sorted set encoding");
}
return C_OK;
}
Zadd
时间复杂度 O(log N)
主要处理流程:
(1)若集合不存在,先创建集合对象 压缩列表 ,若不满足压缩列表条件则创建 跳跃表;
(2)添加成员和分数(核心函数 zsetAdd
)。
c
void zaddCommand(client *c) {
zaddGenericCommand(c,ZADD_NONE);
}
/* This generic command implements both ZADD and ZINCRBY. */
void zaddGenericCommand(client *c, int flags) {
// 解析和处理参数...
// 获取或创建有序集合对象
zobj = lookupKeyWrite(c->db,key);
if (zobj == NULL) {
if (xx) goto reply_to_client; // +XX 选项但key不存在
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
// 创建跳跃表编码的集合
// (不满足 zset_max_ziplist_entries 和 zset_max_ziplist_value)
zobj = createZsetObject();
} else {
// 创建压缩列表编码的集合
zobj = createZsetZiplistObject();
}
dbAdd(c->db,key,zobj);
} else {
if (zobj->type != OBJ_ZSET) {
addReply(c,shared.wrongtypeerr);
goto cleanup;
}
}
// // 处理每个元素
for (j = 0; j < elements; j++) {
double newscore;
score = scores[j];
int retflags = flags;
ele = c->argv[scoreidx+1+j*2]->ptr;
// 尝试添加/更新元素
int retval = zsetAdd(zobj, score, ele, &retflags, &newscore);
// ...处理返回值
}
// ...返回响应
}
核心添加逻辑(zsetAdd
):
(1)压缩列表处理:查找/插入/更新(先删除后插入)的时间复杂度都为O(N)
;
(2)跳跃表处理:查找元素时间复杂度O(1)
(字典直接返回),插入/更新(先删除后插入)的时间复杂度都为O(log N)
,更新时若分数相同时间复杂度O(1)
;
c
int zsetAdd(robj *zobj, double score, sds ele, int *flags, double *newscore) {
// ...
/* (1)压缩列表ziplist处理 */
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *eptr;
// (1.1) 元素存在,处理更新分数, zzlFind查找时间复杂度 O(N)
if ((eptr = zzlFind(zobj->ptr,ele,&curscore)) != NULL) {
/* NX 参数处理,已存在直接返回 */
if (nx) {
*flags |= ZADD_NOP;
return 1;
}
if (incr) {
// incr 分数处理
}
/* 更新分数 先删除,后插入 */
if (score != curscore) {
zobj->ptr = zzlDelete(zobj->ptr,eptr);
zobj->ptr = zzlInsert(zobj->ptr,ele,score);
// ...
}
return 1;
}
// (1.2) 元素不存在,插入新元素
else if (!xx) {
// 检查是否超过压缩列表限制
// (不满足 zset_max_ziplist_entries 和 zset_max_ziplist_value)
if (zzlLength(zobj->ptr)+1 > server.zset_max_ziplist_entries ||
sdslen(ele) > server.zset_max_ziplist_value ||
!ziplistSafeToAdd(zobj->ptr, sdslen(ele)))
{
// 转换为跳跃表
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
} else {
// 插入新元素到压缩列表(保持有序)
zobj->ptr = zzlInsert(zobj->ptr,ele,score);
// ...
}
}
// ...
}
/* (2) 跳跃表skiplist处理 */
if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
// ...
// 查找元素是否已存在(通过字典O(1)查找)
de = dictFind(zs->dict,ele);
// (2.1) 元素存在,处理更新分数
if (de != NULL) {
/* NX 参数,已存在直接返回 */
if (nx) {
// ...
return 1;
}
curscore = *(double*)dictGetVal(de);
if (incr) {
// incr 分数处理
}
/* Remove and re-insert when score changes. */
if (score != curscore) {
// 先删除,再插入
znode = zslUpdateScore(zs->zsl,curscore,ele,score);
/* 更新字段分数score */
dictGetVal(de) = &znode->score; /* Update score ptr. */
// ...
}
// 分数相同,无需更新
return 1;
}
// (2.2)元素不存在,插入到跳跃表和字典
else if (!xx) {
ele = sdsdup(ele);
// 插入跳跃表
znode = zslInsert(zs->zsl,score,ele);
// 插入字典
serverAssert(dictAdd(zs->dict,ele,&znode->score) == DICT_OK);
// ...
return 1;
}
// ...
} else {
serverPanic("Unknown sorted set encoding");
}
return 0; /* Never reached. */
}
zskiplistNode *zslUpdateScore(zskiplist *zsl, double curscore, sds ele, double newscore) {
// ...
zslDeleteNode(zsl, x, update);
zskiplistNode *newnode = zslInsert(zsl,newscore,x->ele);
// ...
return newnode;
}
Zrem
时间复杂度 O(log N)
主要处理流程:
(1)若集合不存在,直接返回;
(2)删除成员(核心删除逻辑 zsetDel
);
(3)若集合为空,删除 key
;
c
void zremCommand(client *c) {
// ...
// 查找有序集合对象
if ((zobj = lookupKeyWriteOrReply(c,key,shared.czero)) == NULL ||
checkType(c,zobj,OBJ_ZSET)) return;
// 遍历所有要删除的元素
for (j = 2; j < c->argc; j++) {
// 移除元素
if (zsetDel(zobj,c->argv[j]->ptr)) deleted++;
// 元素已清空,把集合也移除
if (zsetLength(zobj) == 0) {
dbDelete(c->db,key);
// ...
}
}
// ...
}
核心删除逻辑(zsetDel
):
时间复杂度主要 = 查找 + 删除,
(1)压缩列表删除:需要移动后续元素填补被删除元素的空间,时间复杂度 O(N) + O(N) = O(N)
(2)跳跃表删除:O(1) + O(log N) = O(log N)
c
int zsetDel(robj *zobj, sds ele) {
// 压缩列表处理,时间复杂度 查找 O(N) + 删除重建 O(N)
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *eptr;
if ((eptr = zzlFind(zobj->ptr,ele,NULL)) != NULL) {
zobj->ptr = zzlDelete(zobj->ptr,eptr);
return 1;
}
}
// 跳跃表处理,时间复杂度 字典元素移除 O(1) + 删除 (O(log N))
else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
dictEntry *de;
double score;
de = dictUnlink(zs->dict,ele);
if (de != NULL) {
/* Get the score in order to delete from the skiplist later. */
score = *(double*)dictGetVal(de);
/* Delete from the hash table and later from the skiplist.
* Note that the order is important: deleting from the skiplist
* actually releases the SDS string representing the element,
* which is shared between the skiplist and the hash table, so
* we need to delete from the skiplist as the final step. */
dictFreeUnlinkedEntry(zs->dict,de);
// 从跳跃表删除 (O(log N))
int retval = zslDelete(zs->zsl,score,ele,NULL);
// ...
return 1;
}
} else {
serverPanic("Unknown sorted set encoding");
}
return 0; /* No such element found. */
}
Zrange / Zrevrange
时间复杂度 :返回 K个元素(K = end -start + 1)
(1) 压缩列表:O(start + K) / O(N)
(2) 跳跃表:O(log N + K)
(3) 获取头部/末尾节点元素:O(1)
命令:
c
void zrangeCommand(client *c) {
zrangeGenericCommand(c,0);
}
void zrevrangeCommand(client *c) {
zrangeGenericCommand(c,1);
}
通用范围命令实现 (zrangeGenericCommand
):
分为压缩列表和跳跃表处理,主要流程一致,
(1) 根据 start,end 参数,确定遍历范围;
(2) 根据 start 获取起始元素;
(3) 顺序访问遍历返回[start, end]范围内的所有元素;
c
void zrangeGenericCommand(client *c, int reverse) {
robj *key = c->argv[1];
robj *zobj;
int withscores = 0;
PORT_LONG start;
PORT_LONG end;
PORT_LONG llen;
PORT_LONG rangelen;
// 1. 解析校验 start 、end
if ((getLongFromObjectOrReply(c, c->argv[2], &start, NULL) != C_OK) ||
(getLongFromObjectOrReply(c, c->argv[3], &end, NULL) != C_OK)) return;
// 2. 检查 WITHSCORES 选项
if (c->argc == 5 && !strcasecmp(c->argv[4]->ptr,"withscores")) {
withscores = 1;
}
// ...
// 3. 查找有序集合对象,不存在直接返回
if ((zobj = lookupKeyReadOrReply(c,key,shared.emptymultibulk)) == NULL
|| checkType(c,zobj,OBJ_ZSET)) return;
// 4. 处理负数索引 :从尾部开始
llen = zsetLength(zobj);
if (start < 0) start = llen+start;
if (end < 0) end = llen+end;
if (start < 0) start = 0;
// 5. 校验范围 [start, end]
if (start > end || start >= llen) {
// ...
return;
}
if (end >= llen) end = llen-1;
// rangelen 为遍历范围长度,后续需要依次遍历
rangelen = (int)(end-start)+1;
// ...
// 6. 根据编码方式处理 ------ 压缩列表处理
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *zl = zobj->ptr;
unsigned char *eptr, *sptr;
unsigned char *vstr;
unsigned int vlen;
PORT_LONGLONG vlong;
// 定位起始元素,时间复杂度 O(start)
if (reverse)
eptr = ziplistIndex(zl,(int)(-2-(2*start)));
// ...
else
eptr = ziplistIndex(zl,(int)(2*start));
// ...
// 遍历返回[start, end]范围内的元素 ,时间复杂度 O(rangelen)
serverAssertWithInfo(c,zobj,eptr != NULL);
sptr = ziplistNext(zl,eptr);
while (rangelen--) {
serverAssertWithInfo(c,zobj,eptr != NULL && sptr != NULL);
// 获取元素值
serverAssertWithInfo(c,zobj,ziplistGet(eptr,&vstr,&vlen,&vlong));
if (vstr == NULL)
addReplyBulkLongLong(c,vlong);
else
addReplyBulkCBuffer(c,vstr,vlen);
// zzlGetScore 获取分数 (O(1))
if (withscores)
addReplyDouble(c,zzlGetScore(sptr));
// 移动到下一个元素, prev or next
if (reverse)
zzlPrev(zl,&eptr,&sptr);
else
zzlNext(zl,&eptr,&sptr);
}
}
// 6. 根据编码方式处理 ------ 跳跃表处理
else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
zskiplist *zsl = zs->zsl;
zskiplistNode *ln;
sds ele;
// 定位起始节点 , 时间复杂度 O(log N) ,若为头节点或尾节点则是 O(1)
if (reverse) {
// 获取尾节点
ln = zsl->tail;
// start > 0 表示非尾节点,需要继续查找
if (start > 0)
ln = zslGetElementByRank(zsl,llen-start);
} else {
// 获取尾头结点
ln = zsl->header->level[0].forward;
// start > 0 表示非头节点,需要继续查找
if (start > 0)
ln = zslGetElementByRank(zsl,start+1);
}
// 遍历返回[start, end]范围内的元素 ,时间复杂度 O(rangelen)
while(rangelen--) {
// ...
ele = ln->ele;
addReplyBulkCBuffer(c,ele,sdslen(ele));
// 获取分数 (O(1))
if (withscores)
addReplyDouble(c,ln->score);
// 移动到下一个元素, backward or forward
ln = reverse ? ln->backward : ln->level[0].forward;
}
} else {
serverPanic("Unknown sorted set encoding");
}
}
ZrangeByScore / ZrevrangeByScore
时间复杂度 :
(1) 压缩列表:O(N)
(2) 跳跃表:O(log N + K + M), K为遍历元素数 + LIMIT offset 跳过元素数
(3) 获取头部/末尾节点元素:O(1)
命令:
c
void zrangebyscoreCommand(client *c) {
genericZrangebyscoreCommand(c,0);
}
void zrevrangebyscoreCommand(client *c) {
genericZrangebyscoreCommand(c,1);
}
通用实现 (genericZrangebyscoreCommand
):
c
/* This command implements ZRANGEBYSCORE, ZREVRANGEBYSCORE. */
void genericZrangebyscoreCommand(client *c, int reverse) {
zrangespec range;
robj *key = c->argv[1];
robj *zobj;
PORT_LONG offset = 0, limit = -1;
int withscores = 0;
PORT_ULONG rangelen = 0;
void *replylen = NULL;
int minidx, maxidx;
/* Parse the range arguments. */
if (reverse) {
/* Range is given as [max,min] */
maxidx = 2; minidx = 3;
} else {
/* Range is given as [min,max] */
minidx = 2; maxidx = 3;
}
// 1. 解析分数范围 (O(1))
if (zslParseRange(c->argv[minidx],c->argv[maxidx],&range) != C_OK) {
addReplyError(c,"min or max is not a float");
return;
}
// 2. 解析可选参数 [WITHSCORES] [LIMIT offset count]
/* Parse optional extra arguments. Note that ZCOUNT will exactly have
* 4 arguments, so we'll never enter the following code path. */
if (c->argc > 4) {
int remaining = c->argc - 4;
int pos = 4;
while (remaining) {
// 解析可选参数 [WITHSCORES]
if (remaining >= 1 && !strcasecmp(c->argv[pos]->ptr,"withscores")) {
pos++; remaining--;
withscores = 1;
}
// 2. 解析可选参数 [LIMIT offset count]
else if (remaining >= 3 && !strcasecmp(c->argv[pos]->ptr,"limit")) {
// offset count
if ((getLongFromObjectOrReply(c, c->argv[pos+1], &offset, NULL)
!= C_OK) ||
(getLongFromObjectOrReply(c, c->argv[pos+2], &limit, NULL)
!= C_OK))
{
return;
}
pos += 3; remaining -= 3;
}
// ...
}
}
// 3. 查找有序集合对象
if ((zobj = lookupKeyReadOrReply(c,key,shared.emptymultibulk)) == NULL ||
checkType(c,zobj,OBJ_ZSET)) return;
// 4. 根据编码处理 - 压缩列表处理
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *zl = zobj->ptr;
unsigned char *eptr, *sptr;
unsigned char *vstr;
unsigned int vlen;
PORT_LONGLONG vlong;
double score;
// 根据分数范围,定位找到第一个符合条件的元素,时间复杂度 O(N)
if (reverse) {
eptr = zzlLastInRange(zl,&range);
} else {
eptr = zzlFirstInRange(zl,&range);
}
if (eptr == NULL) {
// ...
return;
}
/* Get score pointer for the first element. */
serverAssertWithInfo(c,zobj,eptr != NULL);
// 继续顺序遍历范围内的元素,时间复杂度 O(K)
sptr = ziplistNext(zl,eptr);
// 处理LIMIT - offset 逻辑, 跳过到下一个元素 prev or next
while (eptr && offset--) {
if (reverse) {
zzlPrev(zl,&eptr,&sptr);
} else {
zzlNext(zl,&eptr,&sptr);
}
}
// 处理LIMIT - count 逻辑, 返回limit个元素
while (eptr && limit--) {
// 分数
score = zzlGetScore(sptr);
/* 遍历到分数不在范围内,结束遍历 */
if (reverse) {
if (!zslValueGteMin(score,&range)) break;
} else {
if (!zslValueLteMax(score,&range)) break;
}
/* We know the element exists, so ziplistGet should always succeed */
serverAssertWithInfo(c,zobj,ziplistGet(eptr,&vstr,&vlen,&vlong));
rangelen++;
if (vstr == NULL) {
addReplyBulkLongLong(c,vlong);
} else {
addReplyBulkCBuffer(c,vstr,vlen);
}
// 处理 withscores
if (withscores) {
addReplyDouble(c,score);
}
// 移动到下一个节点
if (reverse) {
zzlPrev(zl,&eptr,&sptr);
} else {
zzlNext(zl,&eptr,&sptr);
}
}
}
// 4. 根据编码处理 - 跳跃表处理
else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
zset *zs = zobj->ptr;
zskiplist *zsl = zs->zsl;
zskiplistNode *ln;
// 1. 定位分数范围内的第一个节点,时间复杂度 O(log N)
if (reverse) {
ln = zslLastInRange(zsl,&range);
} else {
ln = zslFirstInRange(zsl,&range);
}
/* No "first" element in the specified interval. */
if (ln == NULL) {
addReply(c, shared.emptymultibulk);
return;
}
// ...
// 处理LIMIT - offset 逻辑, 跳过到下一个元素 prev or next
while (ln && offset--) {
if (reverse) {
ln = ln->backward;
} else {
ln = ln->level[0].forward;
}
}
// 遍历 limit 个元素
while (ln && limit--) {
/* 遍历到分数不在范围内,结束遍历 */
if (reverse) {
if (!zslValueGteMin(ln->score,&range)) break;
} else {
if (!zslValueLteMax(ln->score,&range)) break;
}
rangelen++;
addReplyBulkCBuffer(c,ln->ele,sdslen(ln->ele));
// 处理 withscores
if (withscores) {
addReplyDouble(c,ln->score);
}
/* 移动到下一个节点 backward or forward */
if (reverse) {
ln = ln->backward;
} else {
ln = ln->level[0].forward;
}
}
}
// ...
if (withscores) {
rangelen *= 2;
}
setDeferredMultiBulkLength(c, replylen, rangelen);
}
Zcard
时间复杂度 O(1)
命令:
c
void zcardCommand(client *c) {
robj *key = c->argv[1];
robj *zobj;
if ((zobj = lookupKeyReadOrReply(c,key,shared.czero)) == NULL ||
checkType(c,zobj,OBJ_ZSET)) return;
addReplyLongLong(c,zsetLength(zobj));
}
PORT_ULONG zsetLength(const robj *zobj) {
PORT_ULONG length = 0;
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
// 压缩列表编码
length = zzlLength(zobj->ptr);
} else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
// 跳跃表编码
length = (PORT_ULONG) ((const zset*)zobj->ptr)->zsl->length; WIN_PORT_FIX /* cast (PORT_ULONG) */
} else {
serverPanic("Unknown sorted set encoding");
}
return length;
}
(1) 压缩列表(ziplist):
- ziplist在头部维护了节点数量的字段
zlbytes
、zltail
和zllen
zllen
直接记录了当前包含的节点数量(元素member + 分数score 属于一对,会连续占用2个节点)- 获取长度只需读取固定位置的内存值;
c
unsigned int zzlLength(unsigned char *zl) {
// 在有序集合的 ziplist 编码中,每个元素存储为两个连续的 ziplist 节点,因此实际元素个数要 总节点数量/2
return ziplistLen(zl)/2;
}
// ziplist.h
/* Return the length of a ziplist, or UINT16_MAX if the length cannot be
* determined without scanning the whole ziplist. */
#define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
(2) 跳跃表(skiplist):从zskiplist
结构定义中看到,长度可以直接通过 zsl->length
获取;
c
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
// 维护当前节点总数
PORT_ULONG length;
int level;
} zskiplist;