精通Redis2、Redis底层数据结构分析(一)

如何才能精通 Redis? 关于这个模块 0,其实设置的初衷就是,考虑到各个小伙伴对 Redis 掌握程度并不一致,肯定有小伙伴是刚接触 Redis,另外也为了让我们学习起始能有个平缓的学习曲线,这里会介绍一些 Redis 的基础知识作为我们的先导入门。但是这个过程中,会碰到原理帖比较旧,或者原理帖与我的假想实现不一样的情况,这个时候就会去翻翻源码,佐证自己的想法。其中,第 0 模块解决的是“知道”层面的问题,第 1 模块解决的是“熟练”层面的问题,剩余的模块重点在解决“精通”层面的问题,也会涉及 “熟悉” 层面的问题。 阅读详情

一.String 底层数据结构

Sds (Simple Dynamic String,简单动态字符串)是 Redis 底层所使用的字符串表示,它被用
在几乎所有的 Redis 模块中。

Sds 在 Redis 中的主要作用有以下两个:

  1. 实现字符串对象(StringObject);
  2. 在 Redis 程序内部用作 char* 类型的替代品;

1.1sds 优点

在 C 语言中,字符串可以用一个 \0 结尾的 char 数组来表示。
比如说, hello world 在 C 语言中就可以表示为 “hello world\0” 。
这种简单的字符串表示在大多数情况下都能满足要求,但是,它并不能高效地支持长度计算和
追加(append)这两种操作:

  • 每次计算字符串长度(strlen(s))的复杂度为 θ(N) 。
  • 对字符串进行 N 次追加,必定需要对字符串进行 N次内存重分配(realloc)。

在 Redis 内部,字符串的追加和长度计算并不少见,而 APPEND 和 STRLEN 更是这两种操
作在 Redis 命令中的直接映射,这两个简单的操作不应该成为性能的瓶颈。
另外, Redis 除了处理 C 字符串之外,还需要处理单纯的字节数组,以及服务器协议等内容,
所以为了方便起见, Redis 的字符串表示还应该是二进制安全的:程序不应对字符串里面保存
的数据做任何假设,数据可以是以 \0 结尾的 C 字符串,也可以是单纯的字节数组,或者其他
格式的数据。
考虑到这两个原因, Redis 使用 sds 类型替换了 C 语言的默认字符串表示: sds 既可以高效地
实现追加和长度计算,并且它还是二进制安全的。

1.2 sds 的实现

在前面的内容中,我们一直将 sds 作为一种抽象数据结构来说明,实际上,它的实现由以下两
部分组成:

typedef char *sds;
struct sdshdr {
// buf 已占用长度
int len;
// buf 剩余可用长度
int free;
// 实际保存字符串数据的地方
char buf[];
};

其中,类型 sds 是 char * 的别名 (alias),而结构 sdshdr 则保存了 len 、 free 和 buf 三个
属性。
作为例子,以下是新创建的,同样保存 hello world 字符串的 sdshdr 结构:

struct sdshdr {
len = 11;
free = 0;
buf = "hello world\0"; // buf 的实际长度为 len + 1
};

通过 len 属性, sdshdr 可以实现复杂度为 θ(1) 的长度计算操作。
另一方面,通过对 buf 分配一些额外的空间,并使用 free 记录未使用空间的大小, sdshdr 可
以让执行追加操作所需的内存重分配次数大大减少,下一节我们就会来详细讨论这一点。
当然, sds 也对操作的正确实现提出了要求——所有处理 sdshdr 的函数,都必须正确地更新
len 和 free 属性,否则就会造成 bug 。

Redis数据库,经常被用作速度要求严苛、数据频繁修改的场合,如果每次修改字符串长度都需要执行一次内存分配的话,那么光是执行内存分配的时间就会占去字符串修改所用时间的一大部分,若是这样,可能对性能产生影响。

为了避免C字符串的这种缺陷,SDS通过未使用的空间解除字符串长度和底层之间的关联,在SDS中,buf数组的长度一定就是字符串数量加一,数组里可以包含未使用的字节,这些字节的数量就是SDS中free中属性的记录。

注意:当分配空间不足时是,修改len 小于1Mb,free 等于Len
空间总是翻倍,len大于1mb,free等于1MB.

例子:若修改之后,SDS的len将变成13字节,那么程序也会分配13个字节的未使用空间,sds数组的实际长度会是13+13+1=27字节

若SDS 的len修改之后将大于等于1MB ,以30MB为例子,那么sds的数组的实际长度会是 30mb +1mb +1byte.

这点需要注意String 中的命令

APPEND key value

如果 key 已经存在并且是一个字符串, APPEND 命令将 value 追加到 key 原来的值的末尾。

如果 key 不存在, APPEND 就简单地将给定 key 设为 value ,就像执行 SET key value 一样。

如果大规模频繁的最字符串进行扩容操作,则注意Redis 服务器的内存情况

这种思路就是利用空间换取时间,线上分析问题的时候注意时间段,空间。

通过这种预分配策略,SDS将连续增长N次所需的内存分配次数从必定N次降低为最多N次

代码分析:

代码版本6.0.8

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {sdshdr
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

可以看到容量成倍增加,注释中解释sdshdr5 is never used。

  • len:字符串的长度(实际使用的长度)
  • alloc:分配内存的大小
  • flags:标志位,低三位表示类型,其余五位未使用
  • buf:字符数组

1.3惰性空间释放

惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不是立即使用内存重分配来回收缩短多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。

SDS提供了相应的API,让我们在需要时,真正释放SDS的未使用空间。

1.4二进制安全

C字符串的字符必须符合某种编码,并且除了字符串的末尾之外,字符串里边不能包含空字符串,否则最先被程序读入的空字符串被误认为字符串的结尾,C只能保存文本数据。而不能保存像图片、音频、视频、压缩文件这样的二进制数据。

SDS的API都是二进制安全的 binzry-safe
,所有SDS API 都会处理二进制的方式来处理SDS buffer数组里的数据

1.5 兼容部分C 字符串函数

二、Redis 内部编码

对于 Redis的常用 5 种数据类型(String、Hash、List、Set、sorted set),每种数据类型都提供了 最少两种 内部的编码格式,而且每个数据类型内部编码方式的选择 对用户是完全透明的,Redis会根据数据量自适应地选择较优化的内部编码格式。

如果想查看某个键的内部编码格式,可以使用 OBJECT ENCODING keyname 指令来进行,比如:

127.0.0.1:6379> 
127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> 
127.0.0.1:6379> object encoding foo  // 查看某个Redis键值的编码
"embstr"
127.0.0.1:6379> 
127.0.0.1:6379>

Redis 的每个键值内部都是使用一个名字叫做 redisObject 这个 C语言结构体保存的,其代码如下:

typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

解释如下:

  • type:表示键值的数据类型,包括 String、List、Set、ZSet、Hash
  • encoding:表示键值的内部编码方式,从 Redis源码看目前取值有如下几种:
/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */
#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

refcount:表示该键值被引用的数量,即一个键值可被多个键引用

INT 编码格式

命令示例: set foo 123

当字符串键值的内容可以用一个 64位有符号整形 来表示时,Redis会将键值转化为 long型来进行存储,此时即对应 OBJ_ENCODING_INT 编码类型。

OBJ_ENCODING_INT 编码类型内部的内存结构可以形象地表示如下:
image

set foo 123 时键值的内存结构
而且 Redis 启动时会预先建立 10000 个分别存储 0~9999 的 redisObject 变量作为共享对象,这就意味着如果 set字符串的键值在 0~10000 之间的话,则可以 直接指向共享对象 而不需要再建立新对象,此时键值不占空间!

因此,当执行如下指令时:

set key1 100
set key2 100

其实 key1 和 key2 这两个键值都直接引用了一个 Redis 预先已建立好的共享 redisObject 对象,就像下面这样:
image
共享对象
源码之前,了无秘密,我们再对照下面的源码,来理解一下上述过程
image
INT编码的源码

EMBSTR编码格式

命令示例: set foo abc

Redis 在保存长度小于 44 字节的字符串时会采用 OBJ_ENCODING_EMBSTR 编码方式,口说无凭,我们来瞅瞅源码:
image
EMBSTR编码的判断条件
从上述代码中很容易看出,对于长度小于 44的字符串,Redis 对键值采用OBJ_ENCODING_EMBSTR 方式,EMBSTR 顾名思义即:embedded string,表示嵌入式的String。从内存结构上来讲 即字符串 sds结构体与其对应的 redisObject 对象分配在 同一块连续的内存空间,这就仿佛字符串 sds 嵌入在 redisObject 对象之中一样,这一切从下面的代码即可清楚地看到:
image
embedded string
因此,对于指令 set foo abc 所设置的键值,其内存结构示意图如下:
image
set foo abc时的键值内存结构

RAW 编码格式

指令示例: set foo abcdefghijklmnopqrstuvwxyzabcdeffasdffsdaadsx

正如指令示例,当字符串的键值为长度大于 44 的 超长字符串 时,Redis 则会将键值的内部编码方式改为 OBJ_ENCODING_RAW 格式,这与上面的 OBJ_ENCODING_EMBSTR 编码方式的不同之处在于 此时动态字符串 sds 的内存与其依赖的 redisObject 的 内存不再连续 了,以 set foo abcdefghijklmnopqrstuvwxyzabcdeffasdffsdaadsx 为例,其键值的内存结构如下所示:
image
set foo abcdefghijklmnopqrstuvwxyzabcdeffasdffsdaadsx时键值的内存结构
到此就讲完了最基本的String数据类型的内部编码情况,怎么样,还是挺好理解的吧!

后续我们将继续剖析 Redis 中 Hash 数据类型的内部编码格式。

参考资料:https://www.jianshu.com/p/666452a22855

三、链表

链表作为数组之外的一种常用序列抽象,是大多数高级语言的基本数据类型,因为 C 语言本身
不支持链表类型,大部分 C 程序都会自己实现一种链表类型, Redis 也不例外——它实现了一
个双端链表结构。

/* Node, List, and Iterator are the only data structures used currently. */

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

typedef struct listIter {
    listNode *next;
    int direction;
} listIter;

typedef struct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void *key);
    unsigned long len;
} list;

学校里的C语言,暂时不能调试redis源码

image

  1. 作为列表键的底层实现之一:当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。
  2. 除此之外,发布与订阅、慢查询、监视器等功能也用到了链表,Redis服务器本身还使用链表来保存多个客户端的状态信息, 以及使用链表来构建客户端输出缓冲区。
  • 双向链表从
    listNode 的结构我们可以看到,包含了前直接点和后置节点。所以可以很方便的进行正向和反向的遍历。

  • 无环链表
    表头结点的 prev 指针和表尾结点的 next 指针都指向 null, 所以 Redis 的链表是一个无环链表。

  • 带有头指针和尾指针
    在 list 结构中,保存了当前链表的表头指针和表尾指针,因此可以快速的从头进行遍历或者从尾部开始遍历。

  • 带有长度计数器
    list 结构中保存了当前链表长度的长度,因此对链表长度的统计时间复杂度是 O(1).


四、字典

字典(dictionary),又名映射(map)或关联数组(associative array), 它是一种抽象数据结
构,由一集键值对(key-value pairs)组成,各个键值对的键各不相同,程序可以将新的键值对
添加到字典中,或者基于键进行查找、更新或删除等操作。
C语言并没有内置这种数据结构,Redis 实现了自己的字典。

字典的主要用途有以下两个:

  1. 实现数据库键空间(key space);
  2. 用作 Hash 类型键的其中一种底层实现;

4.1实现数据库键空间 key space

Redis 是一个键值对数据库,数据库中的键值对就由字典保存:每个数据库都有一个与之相对
应的字典,这个字典被称之为键空间(key space)。
当用户添加一个键值对到数据库时(不论键值对是什么类型),程序就将该键值对添加到键空
间;当用户从数据库中删除一个键值对时,程序就会将这个键值对从键空间中删除;等等。
举个例子,执行 FLUSHDB 可以清空键空间上的所有键值对数据:

redis> FLUSHDB
OK

执行 DBSIZE 则返回键空间上现有的键值对:

redis> DBSIZE
(integer) 0

还可以用 SET 设置一个字符串键到键空间,并用 GET 从键空间中取出该字符串键的值:

redis> SET number 10086
OK
redis> GET number
"10086"
redis> DBSIZE
(integer) 1

4.2用作 Hash 类型键的其中一种底层实现

Redis 的 Hash 类型键使用以下两种数据结构作为底层实现:

  1. 字典;
  2. 压缩列表;

因为压缩列表比字典更节省内存,所以程序在创建新 Hash 键时,默认使用压缩列表作为底层
实现,当有需要时,程序才会将底层实现从压缩列表转换到字典。
当用户操作一个 Hash 键时,键值在底层就可能是一个哈希表:

redis> HSET book name "The design and implementation of Redis"
(integer) 1
redis> HSET book type "source code analysis"
(integer) 1
redis> HSET book release-date "2013.3.8"
(integer) 1
redis> HGETALL book
1) "name"
2) "The design and implementation of Redis"
3) "type"
4) "source code analysis"
5) "release-date"
6) "2013.3.8"

《哈希表》章节给出了关于哈希类型键的更多信息,并介绍了压缩列表和字典之间的转换条件。
介绍完了字典的用途,现在让我们来看看字典数据结构的定义。

4.3字典的定义

字典的实现

实现字典的方法有很多种:

  • • 最简单的就是使用链表或数组,但是这种方式只适用于元素个数不多的情况下;
    • 要兼顾高效和简单性,可以使用哈希表;
    • 如果追求更为稳定的性能特征,并且希望高效地实现排序操作的话,则可以使用更为复
    杂的平衡树;
    在众多可能的实现中, Redis 选择了高效且实现简单的哈希表作为字典的底层实现。
    dict.h/dict 给出了这个字典的定义
/*
*字典
*/
typedef struct dict {
    dictType *type;
    void *privdata;
    //hash表个数
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

ht属性是一个包含了两个项的数组,数组的每个项都是一个dictht
哈希表,一般情况下字典只使用ht[0]哈希表,ht[1]哈希表只是在ht[0]哈希表进行rehash时使用。

rehashidx 记录rehash 的进度,如果没有进行 值为-1

哈希表实现

字典所使用的哈希表实现由 dict.h/dictht 类型定义:

/*
* 哈希表
*/
typedef struct dictht {
// 哈希表节点指针数组(俗称桶, bucket)
dictEntry **table;
// 指针数组的大小
unsigned long size;
// 指针数组的长度掩码,用于计算索引值
unsigned long sizemask;
// 哈希表现有的节点数量
unsigned long used;
} dictht;

table 属性是一个数组,数组的每个元素都是一个指向 dictEntry 结构的指针。
每个 dictEntry 都保存着一个键值对,以及一个指向另一个 dictEntry 结构的指针:

/*
*哈希节点
*/
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

image

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e6II8y7a-1601172888918)(https://i.imgrpost.com/imgr/2020/09/26/FoxitReader_VIM1v3M0Nt.png)]

4.4 hash 算法

Redis 目前使用两种不同的哈希算法:

  1. MurmurHash2 32 bit 算法:这种算法的分布率和速度都非常好,具体信息请参考 MurmurHash 的主页: http://code.google.com/p/smhasher/ 。
  2. 基 于 djb 算 法 实 现 的 一 个 大 小 写 无 关 散 列 算 法: 具 体 信 息 请 参 考

该方法在dictGenHashFunction中实现

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.  */
    //m和r这两个值用于计算哈希值,只是因为效果好。
    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;

    //将字符串key每四个一组看成uint32_t类型,进行运算的到h
    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  */
    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. */
    h ^= h >> 13;
    h *= m;
    h ^= h >> 15;

    return (unsigned int)h;
}

http://www.cse.yorku.ca/~oz/hash.html 。
使用哪种算法取决于具体应用所处理的数据:
• 命令表以及 Lua 脚本缓存都用到了算法 2 。
• 算法 1 的应用则更加广泛:数据库、集群、哈希键、阻塞操作等功能都用到了这个算法。

在哈希表实现中,当两个不同的键拥有相同的哈希值时,我们称这两个键发生碰撞(collision),
而哈希表实现必须想办法对碰撞进行处理。
字典哈希表所使用的碰撞解决方法被称之为链地址法:这种方法使用链表将多个哈希值相同的
节点串连在一起,从而解决冲突问题。

image

考虑到速度,节点总是被添加到链表头部位置,复杂度为O(1)

这里可以对比Java的hashMap 对比记忆。

4.5 rehash

当哈希表的大小不能满足需求,就可能会有两个或者以上数量的键被分配到了哈希表数组上的同一个索引上,于是就发生冲突(collision).

在Redis中解决冲突的办法是链接法(separate chaining)。但是需要尽可能避免冲突,希望哈希表的负载因子(load factor),维持在一个合理的范围之内,就需要对哈希表进行扩展或收缩。

扩容:当 hash 表中元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是原数组大小的 2 倍。不过如果 Redis 正在做 bgsave,为了减少内存页的过多分离 (Copy On Write),Redis 尽量不去扩容 (dict_can_resize),但是如果 hash 表已经非常满了,元素的个数已经达到了第一维数组长度的 5 倍 (dict_force_resize_ratio),说明 hash 表已经过于拥挤了,这个时候就会强制扩容。

缩容:当 hash 表因为元素的逐渐删除变得越来越稀疏时,,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。缩容的条件是元素个数低于数组长度的 10%。缩容不会考虑 Redis 是否正在做 bgsave。

大字典的扩容是比较耗时间的,需要重新申请新的数组,然后将旧字典所有链表中的元素重新挂接到新的数组下面,这是一个O(n)级别的操作.

扩容代码:

static int _dictExpandIfNeeded(dict *d) //扩展d字典,并初始化
{
    if (dictIsRehashing(d)) return DICT_OK;     //正在进行rehash,直接返回

    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); //如果字典(的 0 号哈希表)为空,那么创建并返回初始化大小的 0 号哈希表

    //1. 字典的总元素个数和字典的数组大小之间的比率接近 1:1
    //2. 能够扩展的标志为真
    //3. 已使用节点数和字典大小之间的比率超过 dict_force_resize_ratio
    if (d->ht[0].used >= d->ht[0].size && (dict_can_resize ||    
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))    
    {
        return dictExpand(d, d->ht[0].used*2);  //扩展为节点个数的2倍
    }
    return DICT_OK;
}

收缩代码:

int dictResize(dict *d)     //缩小字典d
{
    int minimal;

    //如果dict_can_resize被设置成0,表示不能进行rehash,或正在进行rehash,返回出错标志DICT_ERR
    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;

    minimal = d->ht[0].used;            //获得已经有的节点数量作为最小限度minimal
    if (minimal < DICT_HT_INITIAL_SIZE)//但是minimal不能小于最低值DICT_HT_INITIAL_SIZE(4)
        minimal = DICT_HT_INITIAL_SIZE;
    return dictExpand(d, minimal);      //用minimal调整字典d的大小
} 

参考链接:https://www.jianshu.com/p/7f53f5e683cf

渐进式rehash
详细步骤:

  1. 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个hash表

  2. 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,标识rehash工作正式开始。

  3. 在rehash期间,每次对字典执行添加、删除、查找或者更新操作时,程序出了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上所有的键值对rehash到ht[1],当rehash工作完成之后,将rehashidx属性值增一。

  4. 随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx的树形值设置为-1,表示rehash操作已经完成。

渐进式rehash好处是分而治之,将rehash键值所需的计算工作分摊到对字典的每个添加、删除、查找和跟新操作上,从而避免了集中式rehash而带来的庞大计算量。

在渐进rehash期间,同时使用ht[0]和ht[1]两个哈希表,先查找ht[0],再查找ht[1]

新添加到字典的键值对,一律保存到ht[1]
里面

本文禁止任何形式的转载,内容参考《Redis设计与实现》。

文中Redis版本6.0.8
如有错误之处欢迎沟通交流。

【AutoDL + Langchain-Chatchat】从零构建高效本地 RAG 知识库问答系统:实战保姆级教程(含避坑指南) 本文详细阐述如何在AutoDL云平台,如何利用Langchain-Chatchat构建高效的本地RAG知识库问答系统,并深入解析RAG架构。从环境搭建到实战部署,从问题排查到性能优化,提供一份全面实用的指南,助力开发者快速构建智能问答应用。 阅读详情

相关推荐

PT静态时序分析实战:从基础命令到高级报告解析

本文详细介绍了PT(PrimeTime)静态时序分析(STA)的实战技巧,从基础命令如pt_shell和report_timing的使用,到高级报告解析与问题排查。通过丰富的实例和脚本示例,帮助工程师快速掌握时序分析的核心技能,提升芯片设计验证效率。

weixin_29289821的博客 954

Redis超详细入门到精通

这里写目录标题1、Nosql概述1.1、为什么要用Nosql1.2、单机时代1.3、Memcached(缓存)+Mysql+垂直拆分(分库分表)1.4、分库分表+水平拆分+Mysql集群1.5、如今年代1.6、为什么要用NoSQL!1.7、什么是Noqsl1.8、Nosql特点2、Nosql的四大分类文档型数据库(bson格式和json一样):列储存数据库分布式文件系统图关系数据库3、Redis入门下载RedisWindows 下安装Linux 源码安装性能测试4、基本知识select可以使用进行切换dbs

编程愣头青 3665

拼多多爬虫实战:逆向sign与anti_content动态参数解析

现代电商爬虫已远超基础HTTP请求范畴,其核心在于理解服务端如何通过动态签名(sign)、设备指纹、行为验证等多维机制定义‘合法请求’。拼多多作为典型代表,将价格、销量、评价等关键数据完全剥离HTML,转由强校验API返回,依赖HMAC-SHA256嵌套签名、anti_content前端加密载荷及滑动验证token闭环。这类机制本质上是客户端环境可信度与请求上下文一致性的实时博弈,技术价值在于保障数据安全与业务风控。典型应用场景包括竞品价格监控、销量趋势分析、评价情感挖掘等需高保真数据的商业分析任务。本文聚

weixin_29741447的博客 831

Redis 从入门到精通:redis的数据结构、redis指令大全、SpringBoot整合redis、redis面试问题详解

目录 一、Redis基础简介 1、Redis官方介绍 2、redis的数据结构及应用场景 2.1数据结构 2.2应用场景 二、redis在Linux中的操作 1、常规操作 2、redis的指令 2.1 value为String类型 2.2 value为hash类型 2.3 value为list类型 2.4value为set类型 2.5value为zs...

方才coding 3510

高阶玩法——Dify生成的数据自动写入mysql数据库

采用自定义工具方式,利用 python编写一个服务sql_save_post,将接收到的内容写入指定数据库的表中,并为该服务撰写一个 OpenAPI 的 schema。配置工作流:我们以写入天气预报为例,在生成天气预报内容后,连接自定义工具“sql_save_post”,将所需的字段配置完成后,实现将生成的内容自动写入mysql数据库中。编写OpenAPI的schema:在dify平台的工具栏目,创建自定义工具,输入工具名称“sql_save_post”。工具:sql_save_post;

qq_44493378的博客 8539

AI人工智能领域多模态数据融合在智能诊断系统的应用案例

在医疗诊断领域,单一模态数据(如CT影像或电子病历)的局限性日益凸显——就像老中医只"望"不"闻"会漏掉关键线索。本文将深入解析多模态数据融合技术如何突破这一瓶颈:从生活化的"中医四诊"类比切入,拆解数据层/特征层/决策层融合的技术原理;结合肺癌智能诊断的真实案例,展示CT影像、病理文本、基因数据、生命体征的多模态融合全流程;最后探讨技术落地的核心挑战与未来方向。无论你是医疗从业者、AI开发者,还是对智能诊断感兴趣的爱好者,都能从中获得技术启发与应用思路。

AGI×大数据,开启智能时代的认知跃迁;解码AGI,赋能数据驱动的智能革命。 832

Redis入门到精通超详细熬夜吐血之作

RedisKey基本命令 启动服务端:redis-server 连接客户端:redis-cli 切换数据库(默认16个数据库):select 2 数据库大小:dbsize 当前数据库所有key:keys * 清空当前库:flushdb 清空所有库:flushall 是否存在key(1存在0不存在):exists key 移动一个key到另一个数据库:move key db 设置一个key过期的秒数:expire key seconds 获取key的有效时间(秒):ttl key 获取key的存储类型:typ

〆、挽风的博客 1228

国产化替代落地:C#+Avalonia跨平台开发(麒麟系统适配全流程)

摘要: 随着国产化替代政策的推进,麒麟操作系统成为国产替代的重要选择。本文介绍了如何使用C#和Avalonia框架开发跨平台应用,适配麒麟系统。Avalonia支持Windows、Linux(包括麒麟)和macOS,通过XAML设计UI,便于开发者快速上手。文章详细讲解了开发环境配置、XAML控件设计、Linux特性适配及性能优化,并强调政府30%研发补贴带来的政策红利。未来,随着国产系统生态完善,C#+Avalonia将成为企业跨平台开发的高效解决方案。

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 3474

Redis 从入门到精通

学习Redis看这篇就够了!(持续更新中……)

qq_45714426的博客 1092

Qt视频播放器项目(打开文件,视频的播放与停止,进度条与视频进度绑定,调整音量,设置按钮图标,将槽函数与按钮绑定等功能)完整代码地址在末尾

【代码】Qt视频播放器项目。

分享计算机视觉,C++,网络摄像头研发,音视频开发,嵌入式等知识。 754

7.DJI-PSDK:psdk负载应用固件升级(基于RTOS)

在单片机应用程序开发阶段,我们可以借助keil5和J-LINK来直接下载应用程序进行开发和调试, 但在产品交付之后,我们对应用程序做了改动和升级,是断然不可能采用这种方式的,我们应该将新版本的程序固件打包发给客户, 能够方便用户通过DJI Assistant2选择更新负载设备的固件,导入本地固件程序包,更新负载设备。使用STM32F4 开发的负载设备从Flash 中直接读取固件文件并运行负载设备程序,因此开发者需先烧录Bootloader 再烧录应用固件,烧录Bootloader 的步骤不再赘述。

哒宰的博客 673

深入浅出Redis:一文掌握Redis底层数据结构与实现原理

Redis之所以高效,不仅在于它的内存操作,更在于巧妙的数据结构设计。本文带你揭开Redis底层数据结构的神秘面纱,包括动态字符串SDS如何解决C语言字符串问题、IntSet如何高效处理整数集合、Dict哈希字典如何快速实现键值对存储与扩容、ZipList如何节省空间但又引发“连锁更新”的问题、QuickList如何兼具空间高效与操作性能、SkipList跳表如何做到类似红黑树的查询效率,以及RedisObject如何统一不同类型的数据存储。在阅读完本文后,你将对Redis数据结构的设计思想与源码实现原理有

weixin_47762494的博客 8728

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

Redis底层数据结构(详细篇)

redis主要的底层数据结构

kingandsong的博客 4884

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

图解Redis底层数据结构实现原理

待更新中

Seky_fei的博客 9434

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

4、Redis7底层数据结构解析

我们已经知道了Redis有上层的应用类型,也有底层的数据结构。那么这些上层数据类型和底层数据结构是怎么对应的呢?OKstring"embstr"这就是一种对应关系。也就是说,在应用层面,我们操作的是string这样的数据类型,但是Redis在底层,操作的是embstr这样一种数据结构。但是,这些上层的数据类型和底层的数据结构之间,是不是就是简单的一一对应的关系呢?

输入知识,消化知识,输出知识,分享知识,教人让我学习更好,业精于勤荒于嬉,行成于思毁于随 1527

16、Redis底层数据结构

我们已经知道了Redis有上层的应用类型,也有底层的数据结构。那么这些上层数据类型和底层数据结构是怎么对应的呢?"embstr"这就是—种对应关系。也就是说,在应⽤层⾯,我们操作的是string这样的数据类型,但是Redis在底层,操作的是embstr这样—种数据结构。但是,这些上层的数据类型和底层的数据结构之间,是不是就是简单的一一对应的关系呢?"int"OKstring0"raw"从这⾥能够看到,每—种上层数据类型对应底层多种不同的数据结构,

srrsheng的博客 897

redis底层数据结构(redis底层存储结构、源码分析)

一、redis为什么快?二、redis的底层数据结构2.1、redis的底层存储的扩容机制2.1.1、扩容时间2.1.2、扩容多大2.1.3、扩容后的rehash2.1.4、何时进行rehash2.1.5、俩hashtable访问那个呢?三、redis的key的底层数据类型(sds)3.1、sds(Simple dynamic string)3.2、sds诞生的原因3.3、sds的数据结构3.4、sds扩容原理3.5、redis

weixin_42351206的博客 6419

【Redis】Redis 底层数据结构、五种数据类型

当我们执行命令setbit key 7 0的时候,相当于把字符0x33变成了0x32,这样字符串的值就变成了”22”。而value则比较复杂,为了在同⼀个dict内能够存储不同类型的value,这就需要⼀个通⽤的数据结构,这个通用的数据结构就是robj,全名是redisObject。String的内部存储结构⼀般是sds(Simple Dynamic String,可以动态扩展内存),但是如果⼀个String类型的value的值是数字,那么Redis内部会把它转成long类型来存储,从⽽减少内存的使用。

奶酪的博客 3240

Redis 数据结构底层详解

Redis 是一种开源的高性能键值存储数据库,以其极快的读写速度、丰富的数据结构和简单的操作接口,广泛应用于缓存、消息队列、会话管理、排行榜等场景。本文将深入探讨 Redis 中常用数据结构的底层实现,包括字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)有序集合是一种带有分数的集合,集合中的每个元素都有一个分数,Redis 会按照分数对集合中的元素进行排序。有序集合的底层使用了。集合类型适用于存储去重的数据集合,比如用户的好友列表、标签列表等。

qq_74042166的博客 3265

Redis 底层数据结构

哈希桶存放的是指向键值对数据的指针(dictENtry*),这样通过指针就能找到键值对数据,然后因为键值对的值可以保存字符串对象和集合数据类型的对象,所以键值对的数据结构并不是直接保存值本身,而是保存了 void * key 和void * value 指针,分别执行乐视几的键对象和值对象,这样一来,即使值是集合数据,也可以通过void * value指针找到。而且当缓冲区大小不够用的世欧。另外,**C语言标准库中字符串的操作函数是很不安全的,**对程序员很不友好,稍微不注意就会导致缓冲区溢出。

weixin_45479936的博客 1895

Redis高级----五种数据结构及其底层实现

底层结构:在redis3.2之前list的实现主要使用了LinkecList和ZipList来使用的,当数据量数据量小于512个或者大小小于64字节的时候使用的是ZipList,当数据量较大的时候使用的使用LinkedList来进行储存,因为ZipList底层使用的数组来模拟的双向链表的形式。需要连续的空间,申请代价较大。注意如果一开始set创建时使用的是intset作为底层的数据结构(即一开始初始化时储存的值是整形),那么当我们要存储字符串时,这是后就会将其底层的类型进行改变,改编成使用Dict类型。

m0_73171073的博客 2760

【2024最新版,redis7】redis底层的10种数据结构

前言:本文redis版本:7.2.4 本文语雀原文地址(首发更新):https://www.yuque.com/wzzz/redis/xg2cp37kx1s4726y 本文优快云转载地址: https://blog.youkuaiyun.com/u013625306/article/details/136842107Redis常见的数据结构有5种: String,List, Set, ZSet, Hash,这只是对外的数据结构 Redis对内的底层数据结构具体实现方法还有如下几种:int, raw, embstr,

u013625306的博客 2528

Redis之底层数据结构

Redis底层数据结构解析:Redis的性能得益于其高效的数据结构设计。String类型根据值大小分为int、embstr、raw三种存储方式;Hash类型在Redis6.x使用ziplist+hashtable,Redis7.x改用更优的listpack+hashtable。ziplist通过连续内存存储提升寻址效率,但存在连锁更新问题;listpack作为其改进版,在保持优势的同时解决了这一问题。不同类型根据配置阈值自动选择最优存储结构,体现了Redis在内存使用和性能上的极致优化。

qq_44784918的博客 1339

Redis的底层数据结构

目录 引言 Redis底层数据结构 1.SDS(Simple Dynamic String) 2.链表(list) 3.字典(dict) 4.跳跃表(zskiplist) 5.整数集合(intset) 6.压缩列表(ziplist) 7.快表(quicklist) 小结 引言 Redis是目前非常火热的一个非关系型数据库,它内置的数据类型一共有5种,string,li...

yan245294305的博客 996

Redis-底层数据结构篇

压缩列表适合小数据量下的链表操作,虽然查找元素的时间复杂度是O(n),但是由于数据量小加上利用上了CPU缓存,本质上更加高效,并且内存上更加节省,即使发生连锁更新这种极端情况的开销也是在可以接受的范围内记忆:一排书架上放书,好找。但是书之间的厚度不一,如果插入一本书超出书架,就要扩大空间并重新放后面的书。耗时up。

小泽的博客 997
上一篇: Mysql 使用UUID 作为主键的分析
下一篇: 精通Redis[3] 之 Redis底层数据结构分析(二)
hamish-wu
博客等级 码龄15年 2814粉丝 162原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值