深入源码分析Redis ZSet、ziplist、skiplist数据结构

本文深入探讨了Redis中的有序集合ZSet数据结构,包括ziplist和skiplist两种底层实现方式。详细介绍了这两种数据结构的选择标准、内存布局及增删查改等核心操作的实现原理。

Redis的几大数据结构之一的ZSet实现的就是Ordered Set有序集合,通常在实际业务开发中ZSet也是较为高频使用的数据结构,可以用来实现排行榜、有序队列等应用。ZSet本身根据以下2个变量控制底层数据结构的选用,底层有ziplist和dict+skiplist的实现方式。

为了尽量节省内存,zset在以下2个条件都成立时会使用ziplist数据结构实现
zset-max-ziplist-entries 128 // 当zset中存的元素数量 <= 128时
zset-max-ziplist-value 64 // 当每个元素的字节大小 <= 64时

在一开始调用zadd命令时如果val的大小<=64字节,肯定先用ziplist创建。在以后每次zadd命令都会检查这2个条件以便进行ziplist到skiplist的转化。那么skiplist会不会退化成ziplist? 也会的,在进行zinter和zunion命令的时候,因为对2个集合的元素做了合并或求交集,可能对集合的元素数量改变了就需要进行转化。

从zadd命令作为切入点看看zset是如何选择实例化数据结构的

/* This generic command implements both ZADD and ZINCRBY. */
void zaddGenericCommand(client *c, int flags) {
    
    // 省略了解析命令,提取输入参数,校验命令语法的代码..

    /* redis有16个库,每个库本身是一个dict的实现,这里根据输入key找redis obj对象 */
    zobj = lookupKeyWrite(c->db,key);
    if (zobj == NULL) {
        if (xx) goto reply_to_client; /* 无此key且操作参数为xx模式,直接响应 */
        // 如果redis.conf配置的max_ziplist_entries是0或者本次添加的元素大小超过了max_ziplist_value(默认64字节),就用skiplist+dict实现zset
        if (server.zset_max_ziplist_entries == 0 ||
            server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
        {
            // 创建skiplist+dict,每个redis obj对象有encoding字段标识它是什么数据类型
            zobj = createZsetObject();
        } else {
            // 创建ziplist,一般第一次zadd都会走这里,除非元素比较大
            zobj = createZsetZiplistObject();
        }
        // 加到redis库中
        dbAdd(c->db,key,zobj);
    } else {
        if (zobj->type != OBJ_ZSET) {
            addReply(c,shared.wrongtypeerr);
            goto cleanup;
        }
    }
    // 遍历元素,加入zset,elements在前面解析客户端输入参数时计算出
    for (j = 0; j < elements; j++) {
        double newscore;
        // ele对应的分数
        score = scores[j];
        int retflags = flags;
        // ele元素
        ele = c->argv[scoreidx+1+j*2]->ptr;
        // 执行zadd命令逻辑
        int retval = zsetAdd(zobj, score, ele, &retflags, &newscore);
        // 出错了直接响应
        if (retval == 0) {
            addReplyError(c,nanerr);
            goto cleanup;
        }
        // 统计加入、更新的元素
        if (retflags & ZADD_ADDED) added++;
        if (retflags & ZADD_UPDATED) updated++;
        if (!(retflags & ZADD_NOP)) processed++;
        // score用来回应incrby命令,告诉用户此元素最新的分数
        score = newscore;
    }
    // dirty统计一段时间内redis改变的key数量,用来rdb save做判断
    server.dirty += (added+updated);

    // 省略失败清理资源代码和响应客户端代码
}

 在上面可以看到实例化zset底层数据结构的逻辑,但是就添加元素来说是在zsetAdd函数实现。

int zsetAdd(
        // zset对象,可能是ziplist或skiplist
        robj *zobj,
        // 分数
        double score,
        // 元素值
        sds ele,
        // 命令参数
        int *flags,
        // newscore指针,计算新分数并响应
        double *newscore) {
    /* 提取命令参数 */
    int incr = (*flags & ZADD_INCR) != 0;
    int nx = (*flags & ZADD_NX) != 0;
    int xx = (*flags & ZADD_XX) != 0;
    *flags = 0; /* We'll return our response flags. */
    double curscore;

    /* score有效性检查 */
    if (isnan(score)) {
        *flags = ZADD_NAN;
        return 0;
    }

    /* 根据数据结构执行添加逻辑 */
    // ziplist
    if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
        unsigned char *eptr;
        // zzlFind,zl是ziplist缩写,ziplist查找ele
        if ((eptr = zzlFind(zobj->ptr, ele,&curscore)) != NULL) {
            // ziplist已存在ele值
            /* nx模式,啥都不做,统计个数据返回 */
            if (nx) {
                *flags |= ZADD_NOP;
                return 1;
            }

            /* 如果是zincrby命令,就算下加分 */
            if (incr) {
                score += curscore;
                if (isnan(score)) {
                    *flags |= ZADD_NAN;
                    r
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值