由于 bitmap、zset、list、set、map、stream 用的底层数据结构已经在之前介绍了,而这些数据结构又不如 quicklist 复杂,所以将会在此一口气全部介绍完。

确定一个命令调用了什么函数

这个问题,举个例子说明。假如你在 redis-cli 中输入 setbit mykey 100 1,那么 redis 实际上调用的函数是什么。本来我是想在代码中找的,后面用 grep 一查才发现,代码中根本没有,它实际是在一个 commands.def 文件中定义的。以下是 commands.c。里面有一个 MAKE_CMD,这就是定义一个命令的宏函数。

#include "commands.h"
#include "server.h"

#define MAKE_CMD(name,summary,complexity,since,doc_flags,replaced,deprecated,group,group_enum,history,num_history,tips,num_tips,function,arity,flags,acl,key_specs,key_specs_num,get_keys,numargs) name,summary,complexity,since,doc_flags,replaced,deprecated,group_enum,history,num_history,tips,num_tips,function,arity,flags,acl,key_specs,key_specs_num,get_keys,numargs
#define MAKE_ARG(name,type,key_spec_index,token,summary,since,flags,numsubargs,deprecated_since) name,type,key_spec_index,token,summary,since,flags,deprecated_since,numsubargs
#define COMMAND_STRUCT redisCommand
#define COMMAND_ARG redisCommandArg

#ifdef LOG_REQ_RES
#include "commands_with_reply_schema.def"
#else
#include "commands.def"
#endif

打开 commands.def,我们就可以发现它是由 utils/generate-command-code.py 生成的。后面又发现了 utils/generate-commands-json.pysrc/commands/*.json。比如 src/commands/setbit.json

{
    "SETBIT": {
        "summary": "Sets or clears the bit at offset of the string value. Creates the key if it doesn't exist.",
        "complexity": "O(1)",
        "group": "bitmap",
        "since": "2.2.0",
        "arity": 4,
        "function": "setbitCommand",
        "command_flags": [
            "WRITE",
            "DENYOOM"
        ],
        "acl_categories": [
            "BITMAP"
        ],
...

里面的 function 字段就是执行 setbit 所调用的函数了。

Overview

Redis 中数据结构的关系

image

bitmap

bitmap,用一连串二进制的 0 和 1 来表示一个数据。内部实现是 redis 的 sds。剩下的内容基本上和你想的大差不差了,值得提的就是:

  1. setbit key offset 0/1:如果操作的 key 不存在,会创建一个大小为 offset / 8 + 1 的 sds。如果已经存在但是 sds 不够长,会自动扩容。以下是 setbitCommand 调用的 lookupStringFromBitCommand。
robj *lookupStringForBitCommand(client *c, uint64_t maxbit, int *dirty) {
    size_t byte = maxbit >> 3;
    robj *o = lookupKeyWrite(c->db,c->argv[1]);
    if (checkType(c,o,OBJ_STRING)) return NULL;
    if (dirty) *dirty = 0;

    if (o == NULL) {
        o = createObject(OBJ_STRING,sdsnewlen(NULL, byte+1));
        dbAdd(c->db,c->argv[1],o);
        if (dirty) *dirty = 1;
    } else {
        o = dbUnshareStringValue(c->db,c->argv[1],o);
        size_t oldlen = sdslen(o->ptr);
        o->ptr = sdsgrowzero(o->ptr,byte+1);
        if (dirty && oldlen != sdslen(o->ptr)) *dirty = 1;
    }
    return o;
}
  1. getbit 等读操作则不会自动扩容

  2. bitpos 寻找第一个设置为 0 或 1 的方法是,先处理 start 和 end(因为可能是负数),还需要根据当前 sds 长度处理 start 和 end,最后才去找。找的时候会先处理左边界的字节,中间部分会和 0 或者 ULONG_MAX 比较,再处理右边界

zset

zset 混合了 listpack、哈希表和 skiplist,但不是同时用这两个数据结构体。数据量小的时候会用 listpack,否则是 skiplist + 哈希表。

listpack 会调用 zzlFind 确认该元素是否已经存在。zzlFind 调用 lpFind,skip 这个参数设置为 1 —— 这说明此时的 listpack 的用法是一个当作 score,另一个当作 element。然后根据参数选择操作的方式。有以下几种:

#define ZADD_IN_NONE 0
#define ZADD_IN_INCR (1<<0)    /* Increment the score instead of setting it. */
#define ZADD_IN_NX (1<<1)      /* Don't touch elements not already existing. */
#define ZADD_IN_XX (1<<2)      /* Only touch elements already existing. */
#define ZADD_IN_GT (1<<3)      /* Only update existing when new scores are higher. */
#define ZADD_IN_LT (1<<4)      /* Only update existing when new scores are lower. */

它们和 ZADD 的用法是对应的,CH 则是 Element changed 的意思。

ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member...]

listpack 转换为 skiplist 的条件是:

            if (zzlLength(zobj->ptr)+1 > server.zset_max_listpack_entries ||
                sdslen(ele) > server.zset_max_listpack_value ||
                !lpSafeToAdd(zobj->ptr, sdslen(ele)))
            {
                zsetConvertAndExpand(zobj, OBJ_ENCODING_SKIPLIST, zsetLength(zobj) + 1);
            }

所以是比对 zset_max_listpack_entrieszset_max_listpack_value,以及新值能否正常插入到 listpack 中。

也可能转换回 listpack,只在 GEO 和 zunion 中看到这函数调用。

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);
    }
}

除此之外值得提的:

  1. ZCARD 不会处理重复的 score,所以就是直接返回 listpack 和 skiplist 中包含的元素总数量。ZCARD 会调用 zsetLength。(zset 是不可能有重复元素的,但是可能有重复的 score)。
unsigned long zsetLength(const robj *zobj) {
    unsigned long length = 0;
    if (zobj->encoding == OBJ_ENCODING_LISTPACK) {
        length = zzlLength(zobj->ptr);
    } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
        length = ((const zset*)zobj->ptr)->zsl->length;
    } else {
        serverPanic("Unknown sorted set encoding");
    }
    return length;
}
  1. ZDIFF 会进行算法选择
ZDIFF numkeys key1 [key2 ...] [WITHSCORES]

ZDIFF 将 key1 中有,在 key2, key3... 中没有的元素取出来。

两个算法的流程分别是:

算法1:

对 set 进行从元素多到元素少排序,然后类似于下面的伪代码:

for each key in key1 对应的 set:
  existed = false
  for each set in key2, key3, key4... 对应的 set:
    if key in set:
      existed = true
  if not existed:
    add to result

所以时间复杂度确实是O(key1对应的set的大小*一共有多少个要处理的set)源码注释中给的O(N*M+K*log(K))是对它更加精确的描述

算法2:

复制 key1 对应的 set/zset,挨个遍历其他所有 set/zset 中的所有元素,边遍历,边在复制的 set/zset 中删除。所以时间复杂度就是O(sum(所有 set/zset 中的元素数量)),源码注释中给的O(L+(N-K)log(N))也一样是一个更加精确的描述。

下面的函数用于选择用哪个算法。简单说就是比较O(N*M)O(L)哪个大,哪个小。

static int zsetChooseDiffAlgorithm(zsetopsrc *src, long setnum) {
    int j;

    long long algo_one_work = 0;
    long long algo_two_work = 0;

    for (j = 0; j < setnum; j++) {
        /* If any other set is equal to the first set, there is nothing to be
         * done, since we would remove all elements anyway. */
        if (j > 0 && src[0].subject == src[j].subject) {
            return 0;
        }

        algo_one_work += zuiLength(&src[0]);
        algo_two_work += zuiLength(&src[j]);
    }

    /* Algorithm 1 has better constant times and performs less operations
     * if there are elements in common. Give it some advantage. */
    algo_one_work /= 2;
    return (algo_one_work <= algo_two_work) ? 1 : 2;
}
  1. 可以混合 zset 和 set 使用

不仅仅可以直接用 redis-cli 试,还可以在源代码中找到对应的迭代器,可以轻松地看出来确实包含了多种迭代器。

下图展示了从 zset 取出所有不在 set 中的元素。

image

typedef struct {
    robj *subject;
    int type; /* Set, sorted set */
    int encoding;
    double weight;

    union {
        /* Set iterators. */
        union _iterset {
            struct {
                intset *is;
                int ii;
            } is;
            struct {
                dict *dict;
                dictIterator *di;
                dictEntry *de;
            } ht;
            struct {
                unsigned char *lp;
                unsigned char *p;
            } lp;
        } set;

        /* Sorted set iterators. */
        union _iterzset {
            struct {
                unsigned char *zl;
                unsigned char *eptr, *sptr;
            } zl;
            struct {
                zset *zs;
                zskiplistNode *node;
            } sl;
        } zset;
    } iter;
} zsetopsrc;
  1. 混合使用 set、zset,并使用 withscores

set 是没有 score 的,但是却可以获得下面的结果。

image

一看代码才知道,原来默认为 1.0(个人认为用一个 123456789 可能更加合适,这么奇怪的数字可以提醒开发者哪里出问题了,这种技巧也在 listpack 中出现过)

int zuiNext(zsetopsrc *op, zsetopval *val) {
...
    if (op->type == OBJ_SET) {
        iterset *it = &op->iter.set;
        if (op->encoding == OBJ_ENCODING_INTSET) {
            int64_t ell;

            if (!intsetGet(it->is.is,it->is.ii,&ell))
                return 0;
            val->ell = ell;
            val->score = 1.0;

            /* Move to next element. */
            it->is.ii++;
        } else if (op->encoding == OBJ_ENCODING_HT) {
            if (it->ht.de == NULL)
                return 0;
            val->ele = dictGetKey(it->ht.de);
            val->score = 1.0;

            /* Move to next element. */
            it->ht.de = dictNext(it->ht.di);
        } else if (op->encoding == OBJ_ENCODING_LISTPACK) {
            if (it->lp.p == NULL)
                return 0;
            val->estr = lpGetValue(it->lp.p, &val->elen, &val->ell);
            val->score = 1.0;

            /* Move to next element. */
            it->lp.p = lpNext(it->lp.lp, it->lp.p);
        } else {
            serverPanic("Unknown set encoding");
        }
...
  1. 集合合并(union)、求交集(inter)

由于可能要操作都个 zset,所以每次都要考虑取哪个 score。

ZUNION numkeys key [key ...] [WEIGHTS weight [weight ...]]
  [AGGREGATE <SUM | MIN | MAX>] [WITHSCORES]
ZINTER numkeys key [key ...] [WEIGHTS weight [weight ...]]
  [AGGREGATE <SUM | MIN | MAX>] [WITHSCORES]

这也是代码中我们看到zunionInterAggregate(&score,value,aggregate);的原因。

  1. 所有的集合操作都需要检查是否在对同一个集合合并、求交集等

之所以这样,是因为所有的实现都会用迭代器,对同一个集合只能用一个 unsafe iter。

list

list 混合了 listpack、quicklist,但不是同时用这两个数据结构体。数据量小的时候会用 listpack,否则是 quicklist。

数据量大小的界限与 list_max_listpack_size相关,如果是整数那就是元素数量,负数就是分级判断元素大小(另一个转换函数是listTypeTryConvertListpack,与之类似):

static void listTypeTryConvertQuicklist(robj *o, int shrinking, beforeConvertCB fn, void *data) {
...
    /* Check the length or size of the quicklist is below the limit. */
    quicklistNodeLimit(server.list_max_listpack_size, &sz_limit, &count_limit);
    if (shrinking) {
        sz_limit /= 2;
        count_limit /= 2;
    }
    if (ql->head->sz > sz_limit || ql->count > count_limit) return;
    /* Invoke callback before conversion. */
    if (fn) fn(data);

    /* Extract the listpack from the unique quicklist node,
     * then reset it and release the quicklist. */
    o->ptr = ql->head->entry;
    ql->head->entry = NULL;
    quicklistRelease(ql);
    o->encoding = OBJ_ENCODING_LISTPACK;
}

void quicklistNodeLimit(int fill, size_t *size, unsigned int *count) {
    *size = SIZE_MAX;
    *count = UINT_MAX;

    if (fill >= 0) {
        /* Ensure that one node have at least one entry */
        *count = (fill == 0) ? 1 : fill;
    } else {
        *size = quicklistNodeNegFillLimit(fill);
    }
}

static const size_t optimization_level[] = {4096, 8192, 16384, 32768, 65536};

static size_t quicklistNodeNegFillLimit(int fill) {
    assert(fill < 0);
    size_t offset = (-fill) - 1;
    // 就是计算 optimization_level 数组有多少个元素。
    size_t max_level = sizeof(optimization_level) / sizeof(*optimization_level);
    if (offset >= max_level) offset = max_level - 1;
    return optimization_level[offset];
}