Redis Scan命令源码解析

本文深入探讨了Redis中的SCAN命令,包括其背后的HSCAN、SSCAN和ZSCAN。文章分析了SCAN命令如何处理不同数据结构,特别是当数据量不大时使用连续内存存储的情况。通过源码分析,揭示了dictScan函数在遍历过程中的关键步骤,特别是如何处理dict的rehash影响,以及采用二进制逆序技巧来确保遍历完整性的原因。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • 背景

最近团队自研的redis需要在集群版本上实现SCAN类命令,便于用户粗略的遍历全量key。相比较分析RDB文件对用户更友好,故深入的学习Redis相关源码。

  • 类似命令

SCAN遍历全量的key,除此之外还包括HSCAN、SSCAN、ZSCAN针对hash、set、zset的一个key进行遍历,详见官网说明SCAN

  • 代码架构

官网SCAN命令介绍中有提到,每次返回key的数量不一定,可能是全部、可能会比制定的COUNT多一些、可能少一些、甚至是0。只要返回的cursor不为0,就表示迭代器还未遍历完全部的key,那么返回的key数量到底受什么影响?什么时候会返回全部的key?

在redis实现中,zset、set、hash如果数据量不大时会用一段连续的内存进行存储,并不会存在全局的dict中。zset、hash用压缩列表存储、set用整数集合存储,多大的数据算大呢?在redis中通过配置可以指定包括元素的个数和大小,这里就不详细介绍了。

如果SCAN类命令(指HSCAN、SSCAN、ZSCAN)扫描的数据用以上连续内存的数据结构进行存储时,会将全部的key返回,这也是好理解的,用连续内存存储,在遍历时直接全量读取key效率更好,记录idx每次重头遍历反而降低效率。

SACN类命令的入口函数scanGenericCommand,其中设计到分类遍历和影响返回key数目的代码如下:

if (ht) {
        void *privdata[2];
        /*
         * 最大遍历次数,是count的10倍
         * count默认值是10,可用通过scan命令count参数指定
         */
        long maxiterations = count*10;

        /*
         * keys是收集遍历结果的list
         * o是遍历的对象,对于dict为NULL 
         * scanCallback 是遍历之后的回调函数,将遍历结果解析然后装入keys
         */
        privdata[0] = keys;
        privdata[1] = o;
        do {
            cursor = dictScan(ht, cursor, scanCallback, NULL, privdata);

         /* 遍历结束条件(有一个满足则退出循环):
          * 1. curosr == 0,遍历结束
          * 2. maxiterations == 0,超过最大遍历次数
          * 3. 遍历完成指定数量的key
          */
        } while (cursor &&
              maxiterations-- &&
              listLength(keys) < (unsigned long)count);
    } else if (o->type == OBJ_SET) {
        int pos = 0;
        int64_t ll;

        while(intsetGet(o->ptr,pos++,&ll))
            listAddNodeTail(keys,createStringObjectFromLongLong(ll));
        cursor = 0;
    } else if (o->type == OBJ_HASH || o->type == OBJ_ZSET) {
        unsigned char *p = ziplistIndex(o->ptr,0);
        unsigned char *vstr;
        unsigned int vlen;
        long long vll;

        while(p) {
            ziplistGet(p,&vstr,&vlen,&vll);
            listAddNodeTail(keys,
                (vstr != NULL) ? createStringObject((char*)vstr,vlen) :
                                 createStringObjectFromLongLong(vll));
            p = ziplistNext(o->ptr,p);
        }
        cursor = 0;
    } else {
        serverPanic("Not handled encoding in SCAN.");
    }

通过上述入口函数核心代码的分析,可知dictScan是遍历的核心代码,下面我们重点分析它,其他类型的遍历如果不是连续内存存储,底层均是由dict存储,处理流程相同。

  • 遍历代码详解

代码不多,话不多说直接上代码:

unsigned long dictScan(dict *d,
                       unsigned long v,
                       dictScanFunction *fn,
                       dictScanBucketFunction* bucketfn,
      
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值