四.Redis中那些你不知道的秘密-五大基本结构Hash的实现原理

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: Hash也是Redis中非常常用的一种存储结构了,Redis的hash底层用到了两种存储结构,ziplist压缩列表 和 hash 表,当存储的所有键值对的键和值的字符串长度都小于64字节,且元素个数少于512个,Redis会选择ziplist存储,这样会比较省内存,否则他会选择hashtable hash表去成,这里的hash表它底层结构和Java中的HashMap比较像,都是数组+链表,链表是为了解决hash冲突。

前言

Hash也是Redis中非常常用的一种存储结构了,Redis的hash底层用到了两种存储结构,ziplist压缩列表 和 hash 表,当存储的所有键值对的键和值的字符串长度都小于64字节,且元素个数少于512个,Redis会选择ziplist存储,这样会比较省内存,否则他会选择hashtable hash表去成,这里的hash表它底层结构和Java中的HashMap比较像,都是数组+链表,链表是为了解决hash冲突。

dict底层结构

Hash的存储结构叫dict,译作字典,我们先睹为快

typedef struct dict {
   
   
    /*类型*/
    dictType *type;    
    /*私有数据*/
    void *privdata;  
    /*2个哈希表,一个存储值,一个位空,哈希表进行rehash的时候才会用到第2个哈希表  */
    dictht ht[2];    
    /*rehash目前进度,rehash的时候用,其他情况下为-1*/
    int rehashidx;    
    /*目前正在运行的安全迭代器的数量*/
    int iterators;
}dict;

dict中维护了2个 dictht , 2个哈希表,一个存储值,一个位空,在哈希表进行rehash重新散列的时候才会用到第2个哈希表,rehash后面再说。看下dictht的结构

typedef struct dictht{
   
   
     / * 哈希表数组 */
     dictEntry **table;
     / * 哈希表大小 */
     unsigned long size;
      / * 掩码,用于计算索引值 : size-1 */
     unsigned long sizemask;
     / * 哈希表已有节元素个数 */
     unsigned long used;

}dictht

dictht中的 dictEntry **table; 是一个二维哈希表数组,是真正用来存储数据的,使用的是 dictEntry 类型,dictEntry 结构如下:

typedef struct dictEntry{
   
   
     /* 键 */
     void *key;
      /* 值*/
     union{
   
   
             /*值*/
          void *val;
          /*无符号 64位整数*/
          uint64_tu64;
          /*有符号 64位整数*/
          int64_ts64;
     }v;

      /* 指向下一个哈希表节点,形成链表*/
     struct dictEntry *next;
}dictEntry

key是键,val是值,next是下一个hash节点的指针,形成链表。val可以存储64位带符号整数,和无符号整数,占8字节。

有了解过HahsMap底层实现的小朋友应该知道,HahsMap为了解决hash冲突(多个不同的key计算出相同的hash值,意味着他们要存储在hash表中的同一个索引位置,这个是不允许的)采用了链地址法。Redis的hash也是同样,这里的next是指向下一个hash元素,从而形成链表。这里梳理一下结构如图:

在这里插入图片描述

rehash重新散列

rehash重新散列指的是对hashtable进行扩容,或者缩容,当元素越来越多势必要扩展空间,当元素变少,多余的空间就是浪费,所以也要缩容。

Hash中 中元素满了就会以原来数组的 2 倍 扩容。但是如果 正在做 bgsave 持久化操作是不会去扩容,因为要减少内存页的过多分离(Copy On Write)。如果 Hash元素的个数达到了数组长度的 5 倍时Redis 会强制扩容。当元素慢慢减少就会缩容,缩容不受bgsave影响。缩容条件是元素个数少于数组长度的 10%

刚才看到在dict中有两个hash表 dictht ht[2]; ,平时存储数据的话就使用 dictht ht[0], dictht ht[1] 定义了结构不分配空间,当执行rehash时就会使用到dictht ht[1] 。redis的rehash不是一次性把ht[0]的元素全部复制到ht[1],而是采用的是渐进式扩容,步骤如下:

  • 为ht[1]分配空间
  • 将rehashindex的值设置为0
  • 在rehash期间,每次对字典执行增删改查操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashindex索引上的所有键值对rehash到ht[1],当rehash工作完成以后,rehashindex的值+1
  • 随着字典操作的不断执行,最终会在某一时间段上ht[0]的所有键值对都会被rehash到ht[1],这时将rehashindex的值设置为-1,表示rehash操作结束
  • 最后把 ht[0] 释放,把ht[1] 设置为 ht[0]以便下一次扩容

渐进式rehash采用的是一种分而治之的方式,将rehash的操作分摊在每一个的访问中,避免集中式rehash而带来的庞大计算量。

需要注意的是在渐进式rehash的过程,如果有增删改查操作时,如果index大于rehashindex,访问ht[0],否则访问ht[1]。

ziplist和hashtable的选择

如果你还不知道什么是ziplist,点击这里了解,我们来看一下redis是如何选择ziplist和hashtable的,hset 命令源码如下:

void hsetCommand(redisClient *c) {
   
   
    int update;
    robj *o;

    / * 查找或创建哈希对象 */
    if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;

    / * [重要]如果需要的话,转换哈希对象的编码 */
    hashTypeTryConversion(o,c->argv,2,3);

    / * 编码 field 和 value 对象*/
    hashTypeTryObjectEncoding(o,&c->argv[2], &c->argv[3]);

     / * 把 field 和 value set到 hash ,这个函数里面会判断元素长度达到 512 选择ahsh*/
    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++;
}

根据hset命令的源码可以看出他做了这些事情

  • 根据传入的key查找已有的hash对象,如果没有就创建一个hash对象(hashTypeLookupWriteOrCreate)
  • 判断hash对象是使用zipList还是使用hashtable去存储(hashTypeTryConversion)
  • 对hash对象进行编码,为了节约内存嘛(hashTypeTryObjectEncoding)
  • 把 field 和 value 设置到 hash对象
  • 发布修改通知和相关事件

接着我们看一下hashTypeTryConversion是如何选择存储类型的

void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
   
   
    int i;

    / * 这里在判断编码,如果不是ziplist直接返回*/
    if (o->encoding != REDIS_ENCODING_ZIPLIST) return;


    for (i = start; i <= end; i++) {
   
   
     / * 检查元素的值的字符串长度是否超标 REDIS_HASH_MAX_ZIPLIST_VALUE  默认 64 */
        if (sdsEncodedObject(argv[i]) &&
            sdslen(argv[i]->ptr) > server.hash_max_ziplist_value)
        {
   
   
            / * 值的长度超过 64,对象使用hash编码 REDIS_ENCODING_HT */
            hashTypeConvert(o, REDIS_ENCODING_HT);
            break;
        }
    }
}

从这里我们看到它的选择条件,判断元素的值的长度超过REDIS_HASH_MAX_ZIPLIST_VALUE 默认 64 就使用hash,还有一个判断在hashTypeSet函数中,源码如下:

int hashTypeSet(robj *o, robj *field, robj *value) {
   
   
    int update = 0;

    /* 判断编码为ziplist */
    if (o->encoding == REDIS_ENCODING_ZIPLIST) {
   
   
        unsigned char *zl, *fptr, *vptr;

         / * 解码field和value*/
        field = getDecodedObject(field);
        value = getDecodedObject(value);

         / *  这里在从ziplist中查找key,如果找到了就会删除旧的键值对,添加新的键值对*/
        zl = o->ptr;
        fptr = ziplistIndex(zl, ZIPLIST_HEAD);
        if (fptr != NULL) {
   
   
             / *  找到 field*/
            fptr = ziplistFind(fptr, field->ptr, sdslen(field->ptr), 1);
            if (fptr != NULL) {
   
   
                /* Grab pointer to the value (fptr points to the field) */
                 / *  找到值*/
                vptr = ziplistNext(zl, fptr);
                redisAssert(vptr != NULL);

                 / * 更新操作*/
                update = 1;

                /* Delete value */
                  / *  删除旧的*/
                zl = ziplistDelete(zl, &vptr);

                /* Insert new value */
                / *  添加新的*/
                zl = ziplistInsert(zl, vptr, value->ptr, sdslen(value->ptr));
            }
        }

         / *  如果不是更新就是一个添加操作*/
        if (!update) {
   
   
            /* Push new field/value pair onto the tail of the ziplist */
             / *  将添加的键值对天爱到到 ziplist 的末尾*/
            zl = ziplistPush(zl, field->ptr, sdslen(field->ptr), ZIPLIST_TAIL);
            zl = ziplistPush(zl, value->ptr, sdslen(value->ptr), ZIPLIST_TAIL);
        }

        o->ptr = zl;
        decrRefCount(field);
        decrRefCount(value);

         / *  【重要】检查是否需要将 ZIPLIST 编码转换成 Hash编码,REDIS_HASH_MAX_ZIPLIST_ENTRIES 默认 512*/
        / * #define REDIS_HASH_MAX_ZIPLIST_ENTRIES 512
        if (hashTypeLength(o) > server.hash_max_ziplist_entries)
            hashTypeConvert(o, REDIS_ENCODING_HT);

      / *  如果不是ZIPLIST 编码如果是hash编码*/
    } else if (o->encoding == REDIS_ENCODING_HT) {
   
   

          / *  添加或修改键值对到字典*/
        if (dictReplace(o->ptr, field, value)) {
   
    /* Insert */
            incrRefCount(field);
        } else {
   
    /* Update */
            update = 1;
        }

        incrRefCount(value);
    } else {
   
   
        redisPanic("Unknown hash encoding");
    }
    return update;
}

梳理一下hashTypeSet的流程

  • 判断对象的编码如果是REDIS_ENCODING_ZIPLIST就走ziplist添加/修改键值对,否则走hash添加/修改键值对
  • 解码field和value,然后从ziplist中找到field和value
  • 从zipList中删除旧的键值对,添加新的键值对
  • 检查元素长度是否大于512,大于的话需要转码成REDIS_ENCODING_HT使用hash存储

文章结束,希望对你有所帮助,喜欢的话请给个好评吧!!!

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
存储 NoSQL Java
Redis如何处理Hash冲突?
在 Redis 中,哈希表是一种常见的数据结构,通常用于存储对象的属性,对于哈希表,最常遇到的是哈希冲突,那么,当 Redis遇到Hash冲突会如何处理?这篇文章,我们将详细介绍Redis如何处理哈希冲突,并探讨其性能和实现细节。
59 1
|
24天前
|
存储 NoSQL 定位技术
Redis geo原理
Redis的GEO功能基于Earth Mapper(http://earth-api.org/)库,它允许存储地理位置信息并执行一些基于该信息的操作。
25 3
|
27天前
|
存储 NoSQL Redis
Redis 哈希(Hash)
10月更文挑战第16天
34 1
|
8天前
|
存储 NoSQL Redis
【赵渝强老师】Redis的存储结构
Redis 默认配置包含 16 个数据库,通过 `databases` 参数设置。每个数据库编号从 0 开始,默认连接 0 号数据库,可通过 `SELECT &lt;dbid&gt;` 切换。Redis 的核心存储结构包括 `dict`、`expires` 等字段,用于处理键值和过期行为。添加键时需指定数据库信息。视频讲解和代码示例详见内容。
|
1月前
|
存储 分布式计算 NoSQL
大数据-40 Redis 类型集合 string list set sorted hash 指令列表 执行结果 附截图
大数据-40 Redis 类型集合 string list set sorted hash 指令列表 执行结果 附截图
27 3
|
1月前
|
设计模式 NoSQL 网络协议
大数据-48 Redis 通信协议原理RESP 事件处理机制原理 文件事件 时间事件 Reactor多路复用
大数据-48 Redis 通信协议原理RESP 事件处理机制原理 文件事件 时间事件 Reactor多路复用
37 2
|
1月前
|
存储 缓存 NoSQL
大数据-46 Redis 持久化 RDB AOF 配置参数 混合模式 具体原理 触发方式 优点与缺点
大数据-46 Redis 持久化 RDB AOF 配置参数 混合模式 具体原理 触发方式 优点与缺点
56 1
|
1月前
|
NoSQL 关系型数据库 MySQL
Redis 事务特性、原理、具体命令操作全方位诠释 —— 零基础可学习
本文全面阐述了Redis事务的特性、原理、具体命令操作,指出Redis事务具有原子性但不保证一致性、持久性和隔离性,并解释了Redis事务的适用场景和WATCH命令的乐观锁机制。
206 0
Redis 事务特性、原理、具体命令操作全方位诠释 —— 零基础可学习
|
1月前
|
消息中间件 NoSQL Kafka
大数据-116 - Flink DataStream Sink 原理、概念、常见Sink类型 配置与使用 附带案例1:消费Kafka写到Redis
大数据-116 - Flink DataStream Sink 原理、概念、常见Sink类型 配置与使用 附带案例1:消费Kafka写到Redis
130 0
|
1月前
|
存储 NoSQL Redis
redis保存数据的结构-redisobject结构体
`redisObject`结构体是Redis内部数据组织的核心,它通过集成类型标识、引用计数和编码方式等关键信息,实现了数据的高效管理和访问。这种设计允许Redis根据数据的实际需求动态调整存储结构,既保证了内存使用的高效性,也确保了数据操作的灵活性和速度。通过对 `redisObject`的深入了解,可以更好地掌握Redis如何在内存中高效存储和操作数据,进而优化数据库的性能和资源利用。
21 0