redis中hash表的使用

假定我们有一个hashmap的逻辑结构,用户编号的为15的人,name是dlf,school是xdu
当我们在redic-cli命令行下敲下 hset id:15 name dlf时
redis里面都发生了什么事情呢?


任何一个使用过redis的用户,即使没有看过redis的源码,想一下这个过程,那么肯定都包含下面这几步
1 socket连接
2 redis-server收到命令信息
3 redis-server解析命令信息(找到对应的命令,及附带的参数)
4 调用对应的命令
5 返回结果
当然在redis-cli发送命令之前,redis-server首先启动,然后加载各种配置,初始化服务器等等。


在这篇博客里,我们只介绍第四点,就是找到redis内部的命令后,并且也已经分析出了参数,如何调用的过程。


首先我们看一个时序图(那个,我得声明,我并没有学习过时序图的精确概念,下面的图大概只能说明调用过程,如果绘制的某些部分不符合时序图的规定,大家见谅哦)


dict的图示:


另外,我默认大家都了解redis内部dict的数据结构。
/*
 * 哈希表节点
 */
typedef struct dictEntry {
    
    // 键
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
/*
 * 哈希表
 *
 * 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
 */
typedef struct dictht {
    
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;

} dictht;

/*
 * 字典
 */
typedef struct dict {

    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash 索引
    // 当 rehash 不在进行时,值为 -1 初始化的时候 就为-1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */
} dict;


如果有不懂的,大家请参考 http://blog.csdn.net/androidlushangderen/article/details/39860693


在redis中不管是列表还是map,数据结构就是list和dictht,但是真正的底层却是redisObject
typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    // 引用计数
    int refcount;
    // 指向实际值的指针
    void *ptr;
} robj;

其中type指得是这个obj里是stirng还是hash,是list还是set
encoding值的是编码格式


OK,在看一张图
下面是我们执行
hset id:14 name zhangsan
hset id:14 school xdu

之后数据在内存中的分配图:


图整体分为3部分,分别是aaa,bbb,ccc大图如下:




好了现在正式开始一步一步调试:
void hsetCommand(redisClient *c) {
    int update;
    robj *o;

    // 取出或新创建哈希对象
	//hset id:15 name zhangsan
	//那么c->argv[1] 指向的就是id:15
    if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;

    // 如果需要的话,转换哈希对象的编码
    //hashmap底层可以使用dict存储也可以使用list存储
    hashTypeTryConversion(o,c->argv,2,3);

    // 编码 field 和 value 对象以节约空间
    hashTypeTryObjectEncoding(o,&c->argv[2], &c->argv[3]);

    // 设置 field 和 value 到 hash
	//将name zhangsan这个两个域写到id:15的value
	//这个o是空的argv[2][3] 里面放的就是name和zhangsan
	//新建立entry的value指向这个robj
    update = hashTypeSet(o,c->argv[2],c->argv[3]);

    // 返回状态:显示 field-value 对是新添加还是更新
    addReply(c, update ? shared.czero : shared.cone);

    // 发送键修改信号
    signalModifiedKey(c->db,c->argv[1]);

    // 发送事件通知
    notifyKeyspaceEvent(REDIS_NOTIFY_HASH,"hset",c->argv[1],c->db->id);


    // 将服务器设为脏
    server.dirty++;
}
robj *hashTypeLookupWriteOrCreate(redisClient *c, robj *key) {

    //对于hset userId:14 name zhangsan
    //这个命令来说,key就是user:14
    robj *o = lookupKeyWrite(c->db,key);

    // 对象不存在,创建新的
    if (o == NULL) {
	//第一次写入hset userId:14 ~~ 库里肯定没有user:14对应的robj
        o = createHashObject();
        dbAdd(c->db,key,o);

    // 对象存在,检查类型
    } else {
        if (o->type != REDIS_HASH) {
            addReply(c,shared.wrongtypeerr);
            return NULL;
        }
    }

    // 返回对象
    return o;
}


请告诉我,在lookupKeyWrite方法里最终返回的o,在我给的内存分布图里是哪块内容?
是下面这个:


当第一次调用hset userid:15 name zhangsan这个命令的时候,lookupKeyWrite返回的是null。
时序图中,第五步的dbAdd最终会到达

int dictAdd(dict *d, void *key, void *val)
{
	//hset userId : 14 name zhangsan
	//对于上面的命令,key是usrid:14
	//val是新建的


    // 尝试添加键到字典,并返回包含了这个键的新哈希节点
    // T = O(N)
    dictEntry *entry = dictAddRaw(d,key);


    // 键已存在,添加失败
    if (!entry) return DICT_ERR;


    // 键不存在,设置节点的值
    // T = O(1)
    dictSetVal(d, entry, val);


    // 添加成功
    return DICT_OK;
}
dictAddRaw会生成,并且绑定dictentry的key与robj


dictSetVal(d, entry, val)会绑定dictentry的value与robj


请记住,在aaa中,先有下面那个robj,再有上面的dictentry下面就是时序图中,第六步了(我假定,hash表的底层实现是hash)

/* Add an element, discard the old if the key already exists.
 * Return 0 on insert and 1 on update.
 *
 * 将给定的 field-value 对添加到 hash 中,
 * 如果 field 已经存在,那么删除旧的值,并关联新值。
 *
 * This function will take care of incrementing the reference count of the
 * retained fields and value objects. 
 *
 * 这个函数负责对 field 和 value 参数进行引用计数自增。
 *
 * 返回 0 表示元素已经存在,这次函数调用执行的是更新操作。
 *
 * 返回 1 则表示函数执行的是新添加操作。
 */
int hashTypeSet(robj *o, robj *field, robj *value) {
	//hset userid:15 name zhangsan
	//将name zhangsan这个两个域写到id:15的value
	//这个o是空的
	//新建立entry(userid:15)的value指向这个robj
    int update = 0;


    // 添加到 ziplist
    if (o->encoding == REDIS_ENCODING_ZIPLIST) {
    //省略部分代码   
    // 添加到字典
    } else if (o->encoding == REDIS_ENCODING_HT) {


        // 添加或替换键值对到字典
        // 添加返回 1 ,替换返回 0
        if (dictReplace(o->ptr, field, value)) { /* Insert */
            incrRefCount(field);
        } else { /* Update */
            update = 1;
        }
        incrRefCount(value);
    } else {
        redisPanic("Unknown hash encoding");
    }
    // 更新/添加指示变量
    return update;
}

/* Add an element, discarding the old if the key already exists.
 *
 * 将给定的键值对添加到字典中,如果键已经存在,那么删除旧有的键值对。
 *
 * Return 1 if the key was added from scratch, 0 if there was already an
 * element with such key and dictReplace() just performed a value update
 * operation. 
 *
 * 如果键值对为全新添加,那么返回 1 。
 * 如果键值对是通过对原有的键值对更新得来的,那么返回 0 。
 *
 * T = O(N)
 */
int dictReplace(dict *d, void *key, void *val)
{
    dictEntry *entry, auxentry;

    /* Try to add the element. If the key
     * does not exists dictAdd will suceed. */
    // 尝试直接将键值对添加到字典
    // 如果键 key 不存在的话,添加会成功
    // T = O(N)
    if (dictAdd(d, key, val) == DICT_OK)
        return 1;

    /* It already exists, get the entry */
    // 运行到这里,说明键 key 已经存在,那么找出包含这个 key 的节点
    // T = O(1)
    entry = dictFind(d, key);
    /* Set the new value and free the old one. Note that it is important
     * to do that in this order, as the value may just be exactly the same
     * as the previous one. In this context, think to reference counting,
     * you want to increment (set), and then decrement (free), and not the
     * reverse. */
    // 先保存原有的值的指针
    auxentry = *entry;
    // 然后设置新的值
    // T = O(1)
    dictSetVal(d, entry, val);
    // 然后释放旧值
    // T = O(1)
    dictFreeVal(d, &auxentry);


    return 0;
}


上面的dictadd是干什么的?
等等,谁能告诉我dictAdd的参数d是什么?
是它:


如果第一次执行hset user:id name zhangsan
在dictadd里直接就会保存entry的两个robj




换句话说,如果第一次执行hset user:id name zhangsan
dictadd会返回ok,然后跳出dictreplace


至于第一次调用hset userid:14 school xdu

与hset userid:14 name lisi

会怎么样,大家自己想吧



参考资料
http://blog.csdn.net/androidlushangderen/article/details/39860693



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值