Redis(三):set/get 命令源码解析

本文深入解析Redis中的setCommand和getCommand,从hash算法到命令处理,了解这两个基本操作背后的实现细节。get操作简单且在无冲突时具有O(1)复杂度,而set操作涉及更多步骤,包括参数解析、数据编码、超时处理等。通过对流程的时序图分析,加深对Redis kv存储机制的理解。
摘要由CSDN通过智能技术生成

经过前两篇的介绍,我们对整个redis的动作流程已经有比较清晰的认识。

  接下来就是到具体的命令处理方式的理解了,想来我们用这些工具的意义也是在此。虽然没有人觉得,一个set/get方法会有难度,但是我们毕竟不是很清楚,否则也不至于在谈到深处就懵逼了。

  我觉得本文的一个重要意义就是: 让set/get还原成它本来样子,和写"hello world"一样简单。

框架性质的东西,我们前面已经讲解,就直接进入主题: set/get 的操作。

  set/get 对应的两个处理函数 (redisCommand) 定义是这样的: 

    // rF 代表 getCommand 是只读命令,又快又准,时间复杂度 O(1)或者O(log(n))
    // wm 代表 setCommand 是个写命令,当心空间问题
    {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0}
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0}

  所以,我们只要理解了, setCommand,getCommand 之后,就可以完全自信的说,set/get 就是和 "hello world" 一样简单了。

零、hash 算法 


  很显然,kv型的存储一定是hash相关算法的实现。那么redis中如何使用这个hash算法的呢?

  redis 中许多不同场景的hash算法,其原型是在 dictType 中定义的。

typedef struct dictType {
    // hash 算法原型
    unsigned int (*hashFunction)(const void *key);
    void *(*keyDup)(void *privdata, const void *key);
    void *(*valDup)(void *privdata, const void *obj);
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    void (*keyDestructor)(void *privdata, void *key);
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

针对大部分场景,我们的key一般都是 string 类型的,但是还是会稍微有不一样的。这里我们就两个场景来说明下:

1. 命令集构建的hash算法

  即是 server.commands 中的key的hash算法,这里元素是有限的。其定义如下:

/* Command table. sds string -> command struct pointer. */
dictType commandTableDictType = {
    // 即 dictSdsCaseHash 是 command 的hash算法实现
    dictSdsCaseHash,            /* hash function */
    NULL,                       /* key dup */
    NULL,                       /* val dup */
    dictSdsKeyCaseCompare,      /* key compare */
    dictSdsDestructor,          /* key destructor */
    NULL                        /* val destructor */
};
// server.c, 不区分大小写的key hash
unsigned int dictSdsCaseHash(const void *key) {
    return dictGenCaseHashFunction((unsigned char*)key, sdslen((char*)key));
}
// dict.c, 不区分大小写的key-hash算法: hash * 33 + c
/* And a case insensitive hash function (based on djb hash) */
unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len) {
    // static uint32_t dict_hash_function_seed = 5381;
    unsigned int hash = (unsigned int)dict_hash_function_seed;

    while (len--)
        hash = ((hash << 5) + hash) + (tolower(*buf++)); /* hash * 33 + c */
    return hash;
}

2. 针对普通kv查询的hash算法

  整个nosql就是kv的增删改查,所以这是个重要的算法。

/* Db->dict, keys are sds strings, vals are Redis objects. */
dictType dbDictType = {
    // k 的hash算法实现
    dictSdsHash,                /* hash function */
    NULL,                       /* key dup */
    NULL,                       /* val dup */
    dictSdsKeyCompare,          /* key compare */
    dictSdsDestructor,          /* key destructor */
    dictObjectDestructor   /* val destructor */
};
// server.c, 调用 dict 的实现
unsigned int dictSdsHash(const void *key) {
    return dictGenHashFunction((unsigned char*)key, sdslen((char*)key));
}
// dict.c, 数据key的hash算法,或者通用的 string 的hashCode 算法
/* MurmurHash2, by Austin Appleby
 * Note - This code makes a few assumptions about how your machine behaves -
 * 1. We can read a 4-byte value from any address without crashing
 * 2. sizeof(int) == 4
 *
 * And it has a few limitations -
 *
 * 1. It will not work incrementally.
 * 2. It will not produce the same results on little-endian and big-endian
 *    machines.
 */
unsigned int dictGenHashFunction(const void *key, int len) {
    /* 'm' and 'r' are mixing constants generated offline.
     They're not really 'magic', they just happen to work well.  */
    uint32_t seed = dict_hash_function_seed;
    const uint32_t m = 0x5bd1e995;
    const int r = 24;

    /* Initialize the hash to a 'random' value */
    uint32_t h = seed ^ len;

    /* Mix 4 bytes at a time into the hash */
    const unsigned char *data = (const unsigned char *)key;
    // 核心算法: step1. *m, ^k>>r, *m, *m, ^k, 每4位做一次运算
    while(len >= 4) {
        uint32_t k = *(uint32_t*)data;

        k *= m;
        k ^= k >> r;
        k *= m;

        h *= m;
        h ^= k;

        data += 4;
        len -= 4;
    }

    /* Handle the last few bytes of the input array  */
    // step2. 倒数第三位 ^<<16, 第二位 ^<<8, 第一位 ^, 然后 *m
    switch(len) {
        case 3: h ^= data[2] << 16;
        case 2: h ^= data[1] << 8;
        case 1: h ^= data[0]; h *= m;
    };

    /* Do a few final mixes of the hash to ensure the last few
     * bytes are well-incorporated. */
    // step3. 再混合 ^>>13, *m, ^>>15
    h ^= h >> 13;
    h *= m;
    h ^= h >> 15;

    return (unsigned int)h;
}

可以看到,针对普通的字符串的hash可是要复杂许多呢,因为这里数据远比 command 的数据多,情况更复杂,这样的算法唯一的目标就是尽量避免hash冲突。(虽然不知道为啥这么干,但它就是牛逼)

  redis中还有其他的hash算法,比如dictObjHash,dictEncObjHash, 后续有接触我们再聊。

接下来,我们正式来看看 set/get 到底如何?

一、getCommand 解析


  很显然,get 会是个最简单的命令,自然要检软柿子捏了。

// t_string.c
void getCommand(client *c) {
    getGenericCommand(c);
}
int getGenericCommand(client *c) {
    robj *o;
    // 如果在kv里找不到,则直接响应空&
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值