Redis底层数据结构之Hash

news2024/10/5 15:29:37

文章目录

    • 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;

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HADOOP完全分布式搭建(饭制版)

HADOOP完全分布式搭建&#xff08;饭制版&#xff09; 1.虚拟机安装 安装系统 点击VMware Workstation左上角文件&#xff0c;新建虚拟机 选择自定义&#xff0c;点击下一步 点击下一步 选择稍后安装操作系统&#xff08;后续我们使用的操作系统为CentOS7&#xff09;,点击…

解决vue2+elementUI的下拉框出现自动校验的问题

问题&#xff1a; 总结原因是因为新增的时候&#xff0c;传了空值进去 可以这样子解决 this.formData.value && this.$set(this.model, this.formData.key, this.formData.value)这种是只有值存在的时候才会给他赋值&#xff0c;但是这只解决单选下拉框&#xff0c;…

SQLiteC/C++接口详细介绍之sqlite3类(五)

快速跳转文章列表&#xff1a;SQLite—系列文章目录 上一篇&#xff1a;SQLiteC/C接口详细介绍之sqlite3类&#xff08;四&#xff09; 下一篇&#xff1a;SQLiteC/C接口详细介绍之sqlite3类&#xff08;六&#xff09;&#xff08;未发表&#xff09; 14.sqlite3_busy_handle…

【STL】string各种函数的应用

1.string 基本赋值操作 string assign&#xff08;string str&#xff0c;int n&#xff09; string assign&#xff08;string str,int pos,int n&#xff09; 2.string存取字符操作 (at()) 注意&#xff1a;[ ]越界不会抛出异常&#xff0c;at越界会抛出异常 3.string拼接…

从零开始利用MATLAB进行FPGA设计(一):建立脉冲检测模型的Simulink模型1

文章灵感来源于MATLAB官方免费教程&#xff1a;HDL Coder Self-Guided Tutorial 考虑到MATLAB官网的英文看着慢&#xff0c;再加上视频讲解老印浓浓的咖喱味&#xff0c;我决定记录利用MATLAB&Simulink&SystemGenerator进行FPGA数字信号处理的学习过程。 在进行数字信…

vue学习笔记24-组件事件配合v-model使用

搜索时v-model绑定的search数据时时发生变化 watch侦听器时时监察变化&#xff0c;一旦数据发生变化 &#xff0c;就实时发送数据给父组件 子组件的完整代码&#xff1a; <template>搜索&#xff1a;<input type"text" v-model"search"> <…

如何把网站的http改成https?

想把网站从不安全的HTTP换成安全的HTTPS&#xff1f;来瞧瞧下面几步操作&#xff1a; 1.挑个SSL证书&#xff1a; - 根据你的网站情况&#xff08;比如就一个域名、多个域名还是啥域名都得管&#xff09;&#xff0c;找一款适合的SSL证书&#xff0c;有免费的DV&#xff08;验…

k8s-Istio服务网络 27

官网&#xff1a;https://istio.io/latest/zh/about/service-mesh/ Istio与k8s的区别 SpringCloud传统微服务结合k8s与Istio与k8s结合&#xff1a; Istio数据面&#xff1a;通过envoy以sidecar方式拦截svc的流量来进行治理。 Istio控制面&#xff1a;pilot list/watch APIserv…

【WSN覆盖优化】基于改进黏菌算法的无线传感器网络覆盖 WSN覆盖优化【Matlab代码#65】

文章目录 【可更换其他算法&#xff0c;获取资源请见文章第5节&#xff1a;资源获取】1. 改进SMA算法1.1 改进参数p1.2 混沌精英突变策略 2. WSN节点感知模型3. 部分代码展示4. 仿真结果展示5. 资源获取 【可更换其他算法&#xff0c;获取资源请见文章第5节&#xff1a;资源获取…

TypeScript(五)交叉类型,联合类型,映射类型

交叉类型 交叉类型是将多个类型合并为一个类型。可以把现有的多种类型叠加到一起成为一种类型&#xff0c;它包含了所需的所有类型的特性。使用符号 & 表示。交叉类型 A & B 表示&#xff0c;任何一个新类型必须同时属于 A 和 B&#xff0c;才属于交叉类型 A & B …

数据库基础知识超详细解析~‍(进阶/复习版)

文章目录 前言一、数据库的操作1.登入数据库2.创建数据库3.显示当前数据库4.使用数据库5.删除数据库 二、常用数据类型三、数据库的约束1约束类型2NULL约束3UNIQUE:唯一约束4DEFAULT&#xff1a;默认值约束5 PRIMARY KEY&#xff1a;主键约束6 FOREIGN KEY&#xff1a;外键约束…

MyBatis-Plus学习记录

目录 MyBatis-Plus快速入门 简介 快速入门 MyBatis-Plus核心功能 基于Mapper接口 CRUD 对比mybatis和mybatis-plus&#xff1a; CRUD方法介绍&#xff1a; 基于Service接口 CRUD 对比Mapper接口CRUD区别&#xff1a; 为什么要加强service层&#xff1a; 使用方式 CR…

【CSS颜色】

本文章属于学习笔记&#xff0c;在https://www.freecodecamp.org/chinese/learn/2022/responsive-web-design/中练习 三、CSS颜色 1、有两种主要的颜色模型:电子设备中使用的加性RGB(红、绿、蓝)模型和印刷品中使用的减色CMYK(青色、品红、黄色、黑色)模型。 使用RGB模型。这…

Ypay源支付6.9无授权聚合免签系统可运营源码

YPay是一款专为个人站长设计的聚合免签系统&#xff0c;YPay基于高性能的ThinkPHP 6.1.2 Layui PearAdmin架构&#xff0c;提供了实时监控和管理的功能&#xff0c;让您随时随地掌握系统运营情况。 说明 Ypay源支付6.9无授权聚合免签系统可运营源码 已搭建测试无加密版本…

FastWiki v0.1.0发布!新增超多功能

FastWiki 发布 v0.1.0 https://github.com/239573049/fast-wiki/releases/tag/v0.1.0 更新日志 兼容OpenAI接口格式删除Blazor版本UI删除useEffect,解决可能存在问题的bug修复对话可以看到所有对话Merge branch ‘master’ of https://gitee.com/hejiale010426/fast-wiki更新…

14、设计模式之命令模式(Command)

一、什么是命令模式 命令模式&#xff08;Command Pattern&#xff09;是一种行为型设计模式&#xff0c;又叫动作模式或事务模式。它将请求&#xff08;命令&#xff09;封装成对象&#xff0c;使得可以用不同的请求对客户端进行参数化&#xff0c;具体的请求可以在运行时更改…

蓝桥杯-粘木棍-DFS

题目 思路 --有n根木棍&#xff0c;需要将其粘成m根木棍&#xff0c;并求出最小差值&#xff0c;可以用DFS枚举出所有情况。粘之前有n根短木棍&#xff0c;粘之后有m根长木棍&#xff0c;那么让长木棍的初始长度设为0。外循环让所有的短木棍都参与粘&#xff0c;内循环让要粘的…

基于SpringBoot的“企业客户信息反馈平台”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“企业客户信息反馈平台”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 平台首页界面图 技术文档界面图 问题信息…

C# Onnx C2PNet 图像去雾 室外场景

目录 介绍 效果 模型信息 项目 代码 下载 C# Onnx C2PNet 图像去雾 室外场景 介绍 github地址&#xff1a;https://github.com/YuZheng9/C2PNet [CVPR 2023] Curricular Contrastive Regularization for Physics-aware Single Image Dehazing 效果 模型信息 Model P…

专升本 C语言笔记-03 变量的作用域

1.变量的概念 内存中有个存储区域,这个地方的数据可以在同一类型范围内不断变化通过变量名,可以访问这块内存区域,获取里面的值; 变量名的构成:数据类型 变量名 值 C语言中变量声明格式: 数据类型 变量名 值 2.变量的注意 2.1.全局变量: 定义在函数外部的叫全局变量…