数据结构
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
两种实现方式
1、ziplist 第一个节点保存元素的成员,而第二个节点则保存元素的分值。压缩列表内的集合元素按分支从小到大排序,分值小的元素被放置在靠近表头的方向,分值较大的被放置在靠近表尾的方向。
2、 skiplist实际上,使用 zset 结构对字典和跳跃表进行封装。zset 结构中的 zsl 跳跃表分支按照从小到大保存了所有元素。通过这个跳跃表,程序可以对有序集合进行范围操作,比如 zrank , zrange 等命令都是基于跳跃表来实现的。
ziplist 插入数据
/* Insert (element,score) pair in listpack. This function assumes the element is
* not yet present in the list. */
unsigned char *zzlInsert(unsigned char *zl, sds ele, double score) {
unsigned char *eptr = lpSeek(zl,0), *sptr;
double s;
// 排序的过程
while (eptr != NULL) {
sptr = lpNext(zl,eptr);
serverAssert(sptr != NULL);
s = zzlGetScore(sptr);
if (s > score) {
/* First element with score larger than score for element to be
* inserted. This means we should take its spot in the list to
* maintain ordering. */
// 元素,score
zl = zzlInsertAt(zl,eptr,ele,score);
break;
} else if (s == score) {
/* Ensure lexicographical ordering for elements. */
if (zzlCompareElements(eptr,(unsigned char*)ele,sdslen(ele)) > 0) {
zl = zzlInsertAt(zl,eptr,ele,score);
break;
}
}
/* Move to next element. */
eptr = lpNext(zl,sptr);
}
/* Push on tail of list when it was not yet inserted. */
if (eptr == NULL)
zl = zzlInsertAt(zl,NULL,ele,score);
return zl;
}
ziplist 压缩列表切换为跳跃表
/* Convert the sorted set object into a listpack if it is not already a listpack
* and if the number of elements and the maximum element size and total elements size
* are within the expected ranges. */
void zsetConvertToListpackIfNeeded(robj *zobj, size_t maxelelen, size_t totelelen) {
if (zobj->encoding == OBJ_ENCODING_LISTPACK) return;
zset *zset = zobj->ptr;
if (zset->zsl->length <= server.zset_max_listpack_entries &&
maxelelen <= server.zset_max_listpack_value &&
lpSafeToAdd(NULL, totelelen))
{
zsetConvert(zobj,OBJ_ENCODING_LISTPACK);
}
}
注意点
在 skiplist 的基础上,还需要创建 dict 的原因是当需要获取某个元素的 score 时,skiplist 的时候复杂度为 O(logN),而 dict 时间复杂度为 O(1) , 见(zsetAdd)。需要特别主要的是底层为 ziplist 时,该操作时间复杂度为 O(n)
int zsetScore(robj *zobj, sds member, double *score) {
if (!zobj || !member) return C_ERR;
if (zobj->encoding == OBJ_ENCODING_LISTPACK) {
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;
}
?
unsigned char *zzlFind(unsigned char *lp, sds ele, double *score) {
unsigned char *eptr, *sptr;
?
if ((eptr = lpFirst(lp)) == NULL) return NULL;
eptr = lpFind(lp, eptr, (unsigned char*)ele, sdslen(ele), 1);
if (eptr) {
sptr = lpNext(lp,eptr);
serverAssert(sptr != NULL);
?
/* Matching element, pull out score. */
if (score != NULL) *score = zzlGetScore(sptr);
return eptr;
}
?
return NULL;
}
- zkiplist 和 dict 共享元素和分值(指针复制)。
- 由 zkiplist 转 skiplist 的操作是不可逆的
- 两个参数 【zset-max-ziplist-entries】和 【zset-max-ziplist-value】可以在配置文件中修改
- zset 也不允许重复
使用场景
- 优先队列
- 排行榜系统:主要是视频网站需要对用户上传的视频做排行榜,榜单的维度坑是多个方面的:按照时间、按照播放量、按照获取的赞排序
- 添加用户赞数
- zadd user:ranking:2022_02_20 3 mike
- 增加点赞
- zincrby user:ranking:2020_02_20 1 mike
- 取消点赞
- zrem user:ranking:2020_02_20 mike
- 暂时获取点赞数最多的10 个用户
- zrevrang user:rangking:2020_02_20 0 9
- 显示用户信息以及用户分数和排名
- hgetall user:info:tom zscore user:rangking:2020_02_20 mike zrank user:rangking:2020_02_20 mike
常见操作
Redis 对象总结
数据类型 | 使用场景 | 备注 |
字符串(string) | 缓存;计数器;分布式锁 | 简单型的,比如 set strnum studentinfo. 计数器如限流 |
列表(list) | lpush + lpop = Stack (栈) lpush + rpop = Queue (队列) lpush + ltrim = Capped Collection (有限集合) lpush + brpop = Message Queue (消息队列) | 如阻塞队列,关注列表 |
哈希(hash) | 对象属性(尤其不定长) | 如缓存 studentinfo hmset setnum setnum 1 stuname dinghaijun age 33 |
集合(set) | 适用于社交场景/推荐场景 | 点赞、粉丝、共同爱好/喜好、推送 |
有序集合(zset) | 排行榜;优先队列;缓存相关的元数据(比如按照排序的挑战) |
作者:心城以北
链接:https://juejin.cn/post/7068678429834477605