Redis底层数据结构之Hash

文章目录

    • 1. Redis底层hash编码格式
    • 2. Redis 6源码分析
    • 3. Redis 7源码分析

1. Redis底层hash编码格式

在redis6中hash的编码格式分别是ziplist(压缩列表)和hashtable,但在redis7中hash的编码格式变为了listpack(紧凑列表)和hashtable。

2. Redis 6源码分析

首先我们看一下redis6的默认配置

config get hash*

在这里插入图片描述
hash-max-ziplist-entries:使用压缩列表保存时哈希集合中最大元素个数

hash-max-ziplist-value:使用压缩列表保存时哈希集合中单个元素的最大长度

如果hash类型键的字段个数小于hash-max-ziplist-entries并且每个字段名和字段值的长度小于hash-max-ziplist-value,redis才会使用OBJ_ENCODING_ZIPLIST来存储该键,前面条件任意一个不满足的时候则会转化为OBJ_ENCODING_HT
在这里插入图片描述

我们修改一下配置:

config set hash-max-ziplist-entries 3
config set hash-max-ziplist-value 8

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
从上面的案例我们可以看出上面两个配置的作用。下面我们看另一种情况:

在这里插入图片描述

我们可以看见不管我们怎么操作,只要有一个时间点不满足前面配置,底层编码都会转化

所以,在redis6中哈希对象报错的键值对个数要小于512,所有的键值对的键和值的字符串长度都小于64个字节时用ziplist否则用hashtable。

注意:ziplist可以升级为hashtable,但hashtable不能降级为ziplist,在节省内存空间方面哈希表是没有压缩列表高效的!

我们可以把上面的流程总结如下

在这里插入图片描述
首先进入t_hash.c。首先在redis中,hashtable被称为字典,它是一个数组加链表的结构。OBJ_ENCODING_HT这种编码格式才是真正的hash表,或称为字典结构,其实现O(1)复杂度的读写操作,因此效率很高,再redis内部,从OBJ_ENCODING_HT类型到底层真正的散列表数据结构是一层一层嵌套下去的。

在这里插入图片描述
我们看dict.h

struct dict { //hash条目
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

typedef struct dictType {//类型
    uint64_t (*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);
    int (*expandAllowed)(size_t moreMem, double usedRatio);
} dictType;

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {//hash表
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dict { //字典
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
} dict;

总的来说,每一个键值对都会对应一个dictEntry

下面解读一下hset这个命令,进入t_hash.c

void hsetCommand(client *c) {
    int i, created = 0;
    robj *o;

    if ((c->argc % 2) == 1) {
        addReplyErrorFormat(c,"wrong number of arguments for '%s' command",c->cmd->name);
        return;
    }

    if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
    hashTypeTryConversion(o,c->argv,2,c->argc-1);

    for (i = 2; i < c->argc; i += 2)
        created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);

    /* HMSET (deprecated) and HSET return value is different. */
    char *cmdname = c->argv[0]->ptr;
    if (cmdname[1] == 's' || cmdname[1] == 'S') {
        /* HSET */
        addReplyLongLong(c, created);
    } else {
        /* HMSET */
        addReply(c, shared.ok);
    }
    signalModifiedKey(c,c->db,c->argv[1]);
    notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id);
    server.dirty += (c->argc - 2)/2;
}

hashTypeTryConversion这个函数就进行了编码类型的判断和转化

/* Check the length of a number of objects to see if we need to convert a
 * ziplist to a real hash. Note that we only check string encoded objects
 * as their string length can be queried in constant time. */
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
    int i;
    size_t sum = 0;

    if (o->encoding != OBJ_ENCODING_ZIPLIST) return;

    for (i = start; i <= end; i++) {
        if (!sdsEncodedObject(argv[i]))
            continue;
        size_t len = sdslen(argv[i]->ptr);
        //如果长度大于hash_max_ziplist_value,则直接转化为hash表
        if (len > server.hash_max_ziplist_value) {
            hashTypeConvert(o, OBJ_ENCODING_HT);
            return;
        }
        sum += len;
    }
    if (!ziplistSafeToAdd(o->ptr, sum))
        hashTypeConvert(o, OBJ_ENCODING_HT);
}

上面就分析了编码类判断的底层源码,下面分析重点ziplist,我们进入ziplist.c

ziplist压缩列表是一种紧凑编码格式,总体思想是多花时间来换取节约空间,即以部分读写性能为代价来换取极高的内存空间利用率,因此只会用于字段个数少,且字段值小的场景。压缩列表里利用率极高的原因与其连续内存的特性是分不开的。

为了节约内存的开发,它是由连续内存块组成的顺序数据结构,有点类似于数组,ziplist是一个经过特殊编码的双向链表,它不存储指向前一个链表节点的prev和指向下一个链表节点的next而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,节约内存是一种时间换空间的思想,只用在字段个数少,字段值小的场景里面。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
前面大致讲解了ziplist得大致结构,下面我们分析zlentry,即压缩列表的节点的构成:

typedef struct zlentry {
    unsigned int prevrawlensize; /* Bytes used to encode the previous entry len*/
    unsigned int prevrawlen;     /* Previous entry len. */
    unsigned int lensize;        /* Bytes used to encode this entry type/len.
                                    For example strings have a 1, 2 or 5 bytes
                                    header. Integers always use a single byte.*/
    unsigned int len;            /* Bytes used to represent the actual entry.
                                    For strings this is just the string length
                                    while for integers it is 1, 2, 3, 4, 8 or
                                    0 (for 4 bit immediate) depending on the
                                    number range. */
    unsigned int headersize;     /* prevrawlensize + lensize. */
    unsigned char encoding;      /* Set to ZIP_STR_* or ZIP_INT_* depending on
                                    the entry encoding. However for 4 bits
                                    immediate integers this can assume a range
                                    of values and must be range-checked. */
    unsigned char *p;            /* Pointer to the very start of the entry, that
                                    is, this points to prev-entry-len field. */
} zlentry;
  • prevrawlensize:上一个链表节点占用的长度所需要的字节数
  • prevrawlen:存储上一个链表节点的长度值
  • lensize:存储当前链表节点长度数值所需要的字节数
  • len:当前链表节点占用的长度
  • headersize:当前链表节点的头部大小(prevrawlensize+lensize),即非数据域的大小
  • encoding:编码方式
  • p:压缩链表以字符串的形式保存,该指针指向当前节点的起始位置

下面分析ziplist的存取情况,分析下面这条命令。

 hset user01 name 1 age 2

上面这个底层的编码类型是ziplist,总共有两个kv对,分别为name-1和age-2,在ziplist存储时就会生成两个Entry

在这里插入图片描述
在这里插入图片描述

前节点:(前节点占用的内存字节数)表示前1个zlentry的长度,previous_entry_length有两种取值情况:1字节或5字节。取值1字节时,表示上一个entry节点的长度小于254字节,虽然1字节的值能表示0-255,但是压缩列表中zlend的取值默认为255,因此就默认用255表示整个压缩列表的结束,其他表示长度的地方就不能用255个值了,所以,当上一个entry长度小于254个字节的时候,prev_len取值就是1字节,否则就是5字节。记录长度的好处是:占用内存小,1或者5个字节。

encoding:记录节点的content保存数据的类型和长度

content:保存的实际数据内容

在这里插入图片描述
在这里插入图片描述

为什么记录前一个接待你的长度?

链表存储在内存中,一般是不连续的,遍历相对比较慢,而ziplist就可以解决这个问题,如果知道了当前的开始地址,因为entry是连续的,entry之后一定是另一个entry,想知道下一个entry的地址,只要将当前开始地址加上当前entry的长度即可,如果还想继续遍历,重复上面操作即可。

面试题:明明已经有链表了,为什么还要研究一个压缩链表:

  1. 普通的双向链表会有两个指针,在存储数据很小的情况下,我们存储的实际数据的大小可能还没有指针占用的内存大,得不偿失。ziplist是一个特殊的双向链表没有维护双向指针:previous next;而是存储上一个entry的长度和当前entry的长度,通过长度推算下一个元素在什么地方。牺牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储entry长度更费内存。这是典型的 “时间换空间”
  2. 链表在内存中一般是不连续的,遍历相对比较慢而ziplist可以很好的解决这个问题,普通数组的遍历是根据数组里存储的数据类型找到下一个元素的(例如int类型的数组访问下一个元素时每次只需要移动一个sizeof(int) 就行),但是ziplist的每个节点的长度是可以不一样的,而我们面对不同长度的节点又不可能直接sizeof(entry),所以ziplist只好将一些必要的偏移量信息记录在了每一个节点里,使之能跳到上一个节点或下一个节点。
    备注:sizeof实际上是获取了数据在内存中所占用的存储空间,以字节为单位来计数。
  3. 头节点里同时还有一个参数len,和string类型提到的SDS 类似,这里是用来记录链表长度的。因此获取链表长度时不用再遍历整个链表,直接拿到len值就可以了,这个时间复杂度是O(1)
  • 总结

前面说到了ziplist为了节省内存空间,采用了紧凑的连续存储,ziplist是一个双向链表,可以在时间复杂度为O(1)下从头部,尾部进行POP或PUSH,但是它也有缺点,即新增元素可能会出现连锁更新现象(这也是它被listpack代替的原因),同时不能保存过多的元素,否则查询效率就会降低,数量小和内容小的情况下可以使用。

上面提到entry中的prevlen属性可能是1个字节也可能是5个字节,那么我们来设想这么一种场景:
假设一个ziplist中,连续多个entry的长度都是一个接近但是又不到254的值(介于250~253之间),那么这时候ziplist中每个节点都只用了1个字节来存储上一个节点的长度,假如这时候添加了一个新节点,如entry1,其长度大于254个字节,此时entry1的下一个节点entry2的prelen属性就必须要由1个字节变为5个字节,也就是需要执行空间重分配,而此时entry2因为增加了4个字节,导致长度又大于254个字节了,那么它的下一个节点entry3的prelen属性也会被改变为5个字节。依此类推,这种产生连续多次空间重分配的现象就称之为连锁更新。同样的,不仅仅是新增节点,执行删除节点操作同样可能会发生连锁更新现象。虽然ziplist可能会出现这种连锁更新的场景,但是一般如果只是发生在少数几个节点之间,那么并不会严重影响性能,而且这种场景发生的概率也比较低,所以实际使用时不用过于担心。

在这里插入图片描述

上图因为entry1节点的prevlen属性只有1个字节大小,无法保存新节点的长度,此时就需要对压缩列表的空间重分配操作并将entry1节点的prevlen属性从原理的1字节大小扩展到5字节大小。就出现了下面连锁更新现象:

在这里插入图片描述

3. Redis 7源码分析

在这里插入图片描述
hash-max-listpack-entries:使用压缩列表保存时hash集合中的最大元素个数
hash-max-listpack-value:使用压缩列表保存时hash集合中单个元素的最大长度

hash类型键的字段个数小于hash-max-listpack-entries且每个字段名和字段值的长度小于hash-max-listpack-value时,Redis才会使用OBJ_ENCODING_LISTPACK来存储该键,前述条件任意一个不满足则会转化为OBJ_ENCODING_HT编码方式。

redis 7为了兼容和过度依旧保留了ziplist的使用,但是实际上真正起作用的是listpack。现在我们将上面两个有关listpack的配置修改一下。

config set hash-max-listpack-entries 3
config set hash-max-listpack-value 5

在这里插入图片描述
下面我们测试一下:

在这里插入图片描述

流程和ziplist一样,只是底层的数据结构从ziplist换成了listpack

在这里插入图片描述
下面我们开始看源码,首先看object.c

robj *createHashObject(void) {
    unsigned char *zl = lpNew(0);
    robj *o = createObject(OBJ_HASH, zl);
    o->encoding = OBJ_ENCODING_LISTPACK;
    return o;
}

上面代码首先调用lpNew创建了一个listpack数据结构,然后创建了一个redisObject对象,最后指定了编码为OBJ_ENCODING_LISTPACK,下面我们着重分析一下lpNew函数。

/* Create a new, empty listpack.
 * On success the new listpack is returned, otherwise an error is returned.
 * Pre-allocate at least `capacity` bytes of memory,
 * over-allocated memory can be shrunk by `lpShrinkToFit`.
 * */
unsigned char *lpNew(size_t capacity) {
    unsigned char *lp = lp_malloc(capacity > LP_HDR_SIZE+1 ? capacity : LP_HDR_SIZE+1);
    if (lp == NULL) return NULL;
    lpSetTotalBytes(lp,LP_HDR_SIZE+1);
    lpSetNumElements(lp,0);
    lp[LP_HDR_SIZE] = LP_EOF;
    return lp;
}

lpNew函数创建了一个空的listpack,一开始分配的大小为LP_HDR_SIZE加上1个字节,LP_HDR_SIZE宏定义是在listpack.c中,它默认是6个字节,其中4个字节记录listpack总字节树,2个字节是记录listpack的元素数量,此外listpack的最后一个字节是用来标识listpack的结束,器默认值是宏定义LP_EOF,和ziplist列表项的结束标记一样,LP_EOF的值也是255。lpNew函数将listpacack创建完后,回到createHashObject函数,接着会调用createObject来创建redisObject对象。

robj *createObject(int type, void *ptr) {
    robj *o = zmalloc(sizeof(*o));
    o->type = type;
    o->encoding = OBJ_ENCODING_RAW;
    //ptr指向前面创建的listpack数据结构
    o->ptr = ptr;
    o->refcount = 1;
    o->lru = 0;
    return o;
}

分析:明明有ziplist了,为什么出来一个listpack紧凑列表?
前面我们分析压缩列表时,我们知道每个entry都会记录一个prevlen,即前继节点的长度,如果前一个节点的长度小于254个字节,则prevlen用1个字节表示,否则prevlen就用5个字节表示。但这会存在一个连锁更新现象。紧凑列表就是redis设计用来取代掉ziplist的数据结构,它通过每个节点记录自己长度其放在自己节点的尾部,来彻底解决掉了ziplist存在的连锁更新现象。

  • listpack结构

listpack主要由4部分组成,分别是total Bytes、Num Elem、Entry以及End。

TotalBytes为整个listpack的空间大小,占用4个字节,每个listpack最多占用4294967295Bytes
num-element为listpack的元素个数,即Entry的个数占用2个字节
element-1~element-n具体的元素
listpack-end-byte为listpack的结束标志,内容为0xFF

在这里插入图片描述

  • entry的结构

entry从上图也可以看出大致结构,分别有下面几个部分:

  1. 当前元素的编码类型(entry-encoding)
  2. 元素数据(entry-data)
  3. 编码类型和元素数据这两部分的长度(entry-len)
//listpack.h
/* Each entry in the listpack is either a string or an integer. */
typedef struct {
    /* When string is used, it is provided with the length (slen). */
    unsigned char *sval;
    uint32_t slen;
    /* When integer is used, 'sval' is NULL, and lval holds the value. */
    long long lval;
} listpackEntry;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/452801.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何不依赖Unity直接解压unitypackage的内容

使用场景 我们都知道unity的资源导出是导出成.unitypackage文件,如果要里面的内容,得打开Unity,将unitypackage导入进去才能看到里面的内容。 但是很多时候我们下了几十个unitypackage资源包,又不清楚好不好用,而且导入之后编译特别慢,unity又不提供批量解压的功能,所…

好消息!电商平台订单API同步订单详情信息免申请审核调用指南!

淘宝开放平台订单类API 测试key获取 拼多多开放平台订单API列表 custom-自定义API操作 taobao.custom/pinduoduo.custom 公共参数 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;secretString是调用密钥api_nameString是API接口名称&…

day08-Mybatis入门

MyBatis 是一款优秀的 持久层 框架&#xff0c;用于简化 JDBC 的开发。 官网&#xff1a;https://mybatis.org/mybatis-3/zh/index.html 一、快速入门 1.1 Mybatis 操作数据库的步骤 准备工作(创建 springboot 工程、数据库表 user、实体类 User)引入 Mybatis 的相关依赖&…

基于Qt 和python 的自动升级功能

需求&#xff1a; 公司内部的一个客户端工具&#xff0c;想加上一个自动升级功能。 服务端&#xff1a; 1&#xff0c;服务端使用python3.7 &#xff0c;搭配 fastapi 和uvicorn 写一个简单的服务&#xff0c;开出一个get接口&#xff0c;用于客户端读取安装包的版本&#…

北京市办理大兴道路运输许可证所需条件及注意事项

尊敬的客户&#xff1a; 感谢您选择北京经典世纪集团有限公司作为您的信任合作伙伴。我们从多个角度&#xff0c;为您详细解析办理大兴道路运输许可证所需的条件及注意事项&#xff0c;以便您轻松高效地完成相关手续。&#xff08;游览器搜经典世纪胡云帅&#xff09;。 我们…

Android7.1 ANR error 弹窗处理

Android7.1 ANR error 弹窗处理 问题描述解决方法 郑重声明:本人原创博文&#xff0c;都是实战&#xff0c;均经过实际项目验证出货的 转载请标明出处:攻城狮2015 Platform: Rockchip OS:Android 7.1.2 Kernel: 3.10 问题描述 有时会用到第三方apk&#xff0c;内置到系统中&…

Linux从0到1——Linux环境基础开发工具的使用(上)

Linux从0到1——Linux环境基础开发工具的使用&#xff08;上&#xff09; 1. Linux软件包管理器yum1.1 yum介绍1.2 用yum来下载软件1.3 更新yum源 2. Linux编辑器&#xff1a;vi/vim2.1 vim的基本概念2.2 vim的基本操作2.3 vim正常模式命令集2.4 vim底行模式命令集2.5 视图模式…

【全志H616】-2 写一个自己的串口

【全志H616】-2 写一个自己的串口 1、基本命令 重启 sudo rebootLinux系统下一个文件夹的文件复制到另一个文件夹下 cp flags.c /home/user05/lab09/flags_revised.c //复制当前文件夹下的 flags.c 文件到 lab09 文件夹下flags_recised.c 文件cp oled_demo.c /home/orangep…

在图片上进行标记

文章目录 需求分析 需求 底图是一张图片&#xff0c;要在图上做标记&#xff0c;对标记的位置有交互行为鼠标滚顶页面&#xff0c;标记位置不发生变化页面发生缩放&#xff0c;标记位置不发生变化 分析 <template><divv-loading"loading"class"point-m…

什么是智慧公厕?对公共厕所智能实时监测管理控制,城市管理更高效智能

公共厕所一直以来都是城市管理的难题之一&#xff0c;但随着智慧科技的发展和应用&#xff0c;智慧公厕成为了解决这一问题的利器。智慧公厕是一种信息化的新型公共厕所&#xff0c;通过全面感知平台实时监测公共厕所的使用状态&#xff0c;并将数据转化为可视、可算、可管的数…

读取txt文件并统计每行最长的单词以及长度

读取txt文件并统计每行最长的单词以及长度 题目 在 D:\\documant.txt 文本中,文件中有若干行英文文本,每行英文文本中有若干个单词&#xff0c;每个单词不会跨行出现每行至多包含100个字符,要求编写一个程序,处理文件,分析各行中的单词,找到每行中的最长单词&#xff0c;分别…

互联网剧本杀小程序,如何创新发展提高收益

近年来&#xff0c;剧本杀深受年轻人的喜欢&#xff0c;一度成为了大众的社交方式&#xff0c;剧本杀为年轻人提供了一个全新的娱乐游戏和社交为一体的模式。 不过随着剧本杀市场入局的人越来越多&#xff0c;市场的发展也迎来了“拐点”&#xff0c;剧本杀逐渐趋向高质量发展…

190基于matlab的tfrSTFT时频分布图

基于matlab的tfrSTFT时频分布图&#xff0c;计算时间序列的STFT时频分布图&#xff0c;得到瞬时频率。通过GUI可以调节图像的展示样式。程序已调通&#xff0c;可直接运行。 190 STFT时频分布图 瞬时频率 能量谱 (xiaohongshu.com)

openGauss使用BenchmarkSQL进行性能测试(上)

一、前言 本文提供openGauss使用BenchmarkSQL进行性能测试的方法和测试数据报告。 BenchmarkSQL&#xff0c;一个JDBC基准测试工具&#xff0c;内嵌了TPC-C测试脚本&#xff0c;支持很多数据库&#xff0c;如PostgreSQL、Oracle和Mysql等。 TPC-C是专门针对联机交易处理系统…

软考高项总结:第16章采购管理(一)

一、管理基础 1、项目采购管理包括从项目团队外部采购或获取所需产品、服务或成果的各个过程。例如合同、订购单、协议备忘录(MOA)和服务水平协议(SLA)。被授权采购项目所需货物、服务的人员可以是项目团队、管理层或组织采购部的成员。 2、协议可以是合同、服务水平协议(SL…

基于AM62X+FPGA/MCU的B码对时定制化整机解决方案

什么是IRIG-B码对时 IRIG-B(inter-range instrumentationgroup-B)码是一种时间同步标准&#xff0c;通常用于精确的时间测量和数据同步&#xff0c;广泛应用于电力、通信、航空等领域。 IRIG-B码为每秒一帧的时间串码&#xff0c;一帧串码中包含100个码元&#xff0c;频率为1K…

git svn混用

背景 项目代码管理初始使用的svn, 由于svn代码操作&#xff0c;无法在本地暂存&#xff0c;有诸多不便&#xff0c;另外本人习惯使用git. 所以决定迁移至git管理 迁移要求&#xff1a; 保留历史提交记录 迁移流程 代码检出 git svn svn_project_url git代码提交 修改本…

可回馈式直流电子负载原理是怎样的

可回馈式直流电子负载可以将电能回馈到电网中&#xff0c;从而实现对电源系统的测试和分析。其工作原理主要包括以下几个方面&#xff1a; 1. 能量转换&#xff1a;可回馈式直流电子负载通过内部的功率开关管将输入的直流电转换为交流电&#xff0c;然后通过变压器将电压升高或…

【阿里云系列】-部署ACK集群的POD应用日志如何集成到日志服务(SLS)中

介绍 我们在实际部署应用到阿里云的ACK集群后&#xff0c;由于后期应用服务的持续维护诉求可能需要跟踪排查问题&#xff0c;此时就要具备将应用的历史日志存档便于后期排查问题 处理方式 为了解决以上的普遍需求&#xff0c;需要将ACK中的应用日志采集到SLS的Logstore中,然…

SpringBoot实战项目——博客笔记项目

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、项目介绍二、项目的整体框架 2.1 数据库模块 2.2 前端模块 2.3 后端模块三、项目图片展示四、项目的实现 4.1 准备工作 4.…