redis有五种基本数据结构:字符串、hash、set、zset、list。但是你知道构成这五种结构的底层数据结构是怎样的吗?今天我们来花费五分钟的时间了解一下。(目前redis版本为3.0.6)

动态字符串SDS

SDS是"simple dynamic string"的缩写。redis中所有场景中出现的字符串,基本都是由SDS来实现的

  • 所有非数字的key。例如 setmsg"hello world" 中的key msg.

  • 字符串数据类型的值。例如`` set msg "hello world"中的msg的值"hello wolrd"

  • 非字符串数据类型中的“字符串值”。例如 RPUSH fruits"apple""banana""cherry"中的"apple" "banana" "cherry"

SDS长这样:

free:还剩多少空间 len:字符串长度 buf:存放的字符数组

空间预分配

为减少修改字符串带来的内存重分配次数,sds采用了“一次管够”的策略:

  • 若修改之后sds长度小于1MB,则多分配现有len长度的空间

  • 若修改之后sds长度大于等于1MB,则扩充除了满足修改之后的长度外,额外多1MB空间

惰性空间释放

为避免缩短字符串时候的内存重分配操作,sds在数据减少时,并不立刻释放空间。

int

就是redis中存放的各种数字 包括以下这种,故意加引号“”的

双向链表

长这样:

分两部分,一部分是“统筹部分”:橘黄色,一部分是“具体实施方“:蓝色。

主体”统筹部分“:

  • head指向具体双向链表的头

  • tail指向具体双向链表的尾

  • len双向链表的长度

具体"实施方":一目了然的双向链表结构,有前驱 pre有后继 next

listlistNode两个数据结构构成。

ziplist

压缩列表。redis的列表键和哈希键的底层实现之一。此数据结构是为了节约内存而开发的。和各种语言的数组类似,它是由连续的内存块组成的,这样一来,由于内存是连续的,就减少了很多内存碎片和指针的内存占用,进而节约了内存。

然后文中的 entry的结构是这样的:

元素的遍历

先找到列表尾部元素:

然后再根据ziplist节点元素中的 previous_entry_length属性,来逐个遍历:

连锁更新

再次看看 entry元素的结构,有一个 previous_entry_length字段,他的长度要么都是1个字节,要么都是5个字节:

  • 前一节点的长度小于254字节,则 previous_entry_length长度为1字节

  • 前一节点的长度大于254字节,则 previous_entry_length长度为5字节

假设现在存在一组压缩列表,长度都在250字节至253字节之间,突然新增一新节点 new, 长度大于等于254字节,会出现:

程序需要不断的对压缩列表进行空间重分配工作,直到结束。

除了增加操作,删除操作也有可能带来“连锁更新”。请看下图,ziplist中所有entry节点的长度都在250字节至253字节之间,big节点长度大于254字节,small节点小于254字节。

哈希表

哈希表略微有点复杂。哈希表的制作方法一般有两种,一种是:开放寻址法,一种是 拉链法。redis的哈希表的制作使用的是 拉链法

整体结构如下图:

也是分为两部分:左边橘黄色部分和右边蓝色部分,同样,也是”统筹“和”实施“的关系。具体哈希表的实现,都是在蓝色部分实现的。先来看看蓝色部分:

这也分为左右两边“统筹”和“实施”的两部分。

右边部分很容易理解:就是通常拉链表实现的哈希表的样式;数组就是bucket,一般不同的key首先会定位到不同的bucket,若key重复,就用链表把冲突的key串起来。

新建key的过程:

假如重复了:

rehash

再来看看哈希表总体图中左边橘黄色的“统筹”部分,其中有两个关键的属性:htrehashidxht是一个数组,有且只有俩元素ht[0]和ht[1];其中,ht[0]存放的是redis中使用的哈希表,而ht[1]和rehashidx和哈希表的 rehash有关。

rehash指的是重新计算键的哈希值和索引值,然后将键值对重排的过程。

加载因子(load factor)=ht[0].used/ht[0].size

扩容和收缩标准

扩容:

  • 没有执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的 加载因子大于等于1。

  • 正在执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的 加载因子大于等于5。

收缩:

  • 加载因子小于0.1时,程序自动开始对哈希表进行收缩操作。

扩容和收缩的数量

扩容:

  • 第一个大于等于 ht[0].used*2的 2^n(2的n次方幂)。

收缩:

  • 第一个大于等于 ht[0].used的 2^n(2的n次方幂)。

(以下部分属于细节分析,可以跳过直接看扩容步骤)对于收缩,我当时陷入了疑虑:收缩标准是 加载因子小于0.1的时候,也就是说假如哈希表中有4个元素的话,哈希表的长度只要大于40,就会进行收缩,假如有一个长度大于40,但是存在的元素为4即( ht[0].used为4)的哈希表,进行收缩,那收缩后的值为多少?

我想了一下:按照前文所讲的内容,应该是4。但是,假如是4,存在和收缩后的长度相等,是不是又该扩容?翻开源码看看:

收缩具体函数:

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的大小

}

int dictExpand(dict *d, unsigned long size) //根据size调整或创建字典d的哈希表

{

dictht n;

unsigned long realsize = _dictNextPower(size); //获得一个最接近2^n的realsize

if (dictIsRehashing(d) || d->ht[0].used > size) //正在rehash或size不够大返回出错标志

return DICT_ERR;

if (realsize == d->ht[0].size) return DICT_ERR; //如果新的realsize和原本的size一样则返回出错标志

/* Allocate the new hash table and initialize all pointers to NULL */

//初始化新的哈希表的成员

n.size = realsize;

n.sizemask = realsize-1;

n.table = zcalloc(realsize*sizeof(dictEntry*));

n.used = 0;

/* Is this the first initialization? If so it's not really a rehashing

* we just set the first hash table so that it can accept keys. */

if (d->ht[0].table == NULL) { //如果ht[0]哈希表为空,则将新的哈希表n设置为ht[0]

d->ht[0] = n;

return DICT_OK;

}

d->ht[1] = n; //如果ht[0]非空,则需要rehash

d->rehashidx = 0; //设置rehash标志位为0,开始渐进式rehash(incremental rehashing)

return DICT_OK;

}

static unsigned long _dictNextPower(unsigned long size)

{

unsigned long i = DICT_HT_INITIAL_SIZE; //DICT_HT_INITIAL_SIZE 为 4

if (size >= LONG_MAX) return LONG_MAX + 1LU;

while(1) {

if (i >= size)

return i;

i *= 2;

}

}

由代码我们可以看到,假如收缩后长度为4,不仅不会收缩,甚至还会报错。(?)

我们回过头来再看看设定:题目可能成立吗?哈希表的扩容都是2倍增长的,最小是4, 4 ===》 8 ====》 16 =====》 32 ======》 64 ====》 128

也就是说:不存在长度为 40多的情况,只能是64。但是如果是64的话,64 X 0.1(收缩界限)= 6.4 ,也就是说在减少到6的时候,哈希表就会收缩,会缩小到多少呢?是8。此时,再继续减少到4,也不会再收缩了。所以,根本不存在一个长度大于40,但是存在的元素为4的哈希表的。

扩容步骤

收缩步骤

渐进式refresh

在"扩容步骤"和"收缩步骤" 两幅动图中每幅图的第四步骤“将ht[0]中的数据利用哈希函数重新计算,rehash到ht[1]”,并不是一步完成的,而是分成N多步,循序渐进的完成的。因为hash中有可能存放几千万甚至上亿个key,毕竟Redis中每个hash中可以存 2^32-1 键值对(40多亿),假如一次性将这些键值rehash的话,可能会导致服务器在一段时间内停止服务,毕竟哈希函数就得计算一阵子呢((#^.^#))。

哈希表的refresh是分多次、渐进式进行的。

渐进式refresh和下图中左边橘黄色的“统筹”部分中的 rehashidx密切相关:

  • rehashidx 的数值就是现在rehash的元素位置

  • rehashidx 等于 -1 的时候说明没有在进行refresh

甚至在进行期间,每次对哈希表的增删改查操作,除了正常执行之外,还会顺带将ht[0]哈希表相关键值对rehash到ht[1]。

以扩容步骤为例:

intset

整数集合是集合键的底层实现方式之一。

跳表

跳表这种数据结构长这样:

redis中把跳表抽象成如下所示:

看这个图,左边“统筹”,右边实现。统筹部分有以下几点说明:

  • header: 跳表表头

  • tail:跳表表尾

  • level:层数最大的那个节点的层数

  • length:跳表的长度

实现部分有以下几点说明:

  • 表头:是链表的哨兵节点,不记录主体数据。

  • 是个双向链表

  • 分值是有顺序的

  • o1、o2、o3是节点所保存的成员,是一个指针,可以指向一个SDS值。

  • 层级高度最高是32。每次创建一个新的节点的时候,程序都会随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是“高度”

redis五种数据结构的实现

redis对象

redis中并没有直接使用以上所说的各种数据结构来实现键值数据库,而是基于一种对象,对象底层再间接的引用上文所说的具体的数据结构。

结构如下图:

字符串

其中:embstr和raw都是由SDS动态字符串构成的。唯一区别是:raw是分配内存的时候,redisobject和 sds 各分配一块内存,而embstr是redisobject和raw在一块儿内存中。

列表

hash

set

zset

更多精彩内容,请关注我的微信公众号 互联网技术窝 或者加微信共同探讨交流:

参考文献

  • https://throwsnew.com/2017/09/12/%E4%B8%BA%E4%BB%80%E4%B9%88Redis%E4%BD%BF%E7%94%A8ziplist%E8%83%BD%E8%8A%82%E7%9C%81%E5%86%85%E5%AD%98/

  • 《redis设计与实现》

字符动图_图解redis五种数据结构底层实现(动图哦)相关推荐

  1. Redis学习总结(14)——图解redis五种数据结构底层实现(动图版)

    Redis有五种基本数据结构:字符串.hash.set.zset.list.但是你知道构成这五种结构的底层数据结构是怎样的吗? 今天我们来花费五分钟的时间了解一下. (目前redis版本为3.0.6) ...

  2. 图解redis五种数据结构底层实现

    redis有五种基本数据结构:字符串.hash.set.zset.list.但是你知道构成这五种结构的底层数据结构是怎样的吗?今天我们来花费五分钟的时间了解一下.(目前redis版本为3.0.6) 动 ...

  3. 图解 Redis 五种数据结构底层实现

    Redis 是一个基于内存中的数据结构存储系统,可以用作数据库.缓存和消息中间件.Redis 支持五种常见对象类型:字符串(String).哈希(Hash).列表(List).集合(Set)以及有序集 ...

  4. Redis五种数据结构及其使用场景

    Redis五种数据结构及其使用场景 先有个概念,redis 数据库其实就是一个大的 map,它容纳了所有的 key, key 都是 string 类型,而 value 则有 string, list, ...

  5. Redis五种数据结构

    对redis来说,所有的key(键)都是字符串,所谓的5种数据结构是指针对value而言 1.Redis五种数据结构如下: 数据结构类型 说明 使用场景 常用方法 其他链接 String字符串类型 1 ...

  6. Redis五种数据结构应用场景

    文章目录 前言 二.字符串String 2.1.常用操作 2.2.应用场景 2.2.1.单值缓存(最常用) 2.2.2.对象缓存 2.2.3.分布式锁 2.2.4.计数器 三.哈希hash 3.1.常 ...

  7. 真正说透Redis五种数据结构

    系列文章目录 真正说透Redis五种数据结构 Redis持久化之RDB+AOF+混合持久化实战演练 Redis高可用之主从架构 文章目录 系列文章目录 前言 一.5种基本数据结构 二.字符串Strin ...

  8. 硬核资源!Redis 五种数据结构以及三种高级数据结构解析(详解)

    上一篇分享的是<深入理解JVM>,这篇给大家分享<Redis 五种数据结构以及三种高级数据结构解析>. 前言 在 Redis 最重要最基础就属 它丰富的数据结构了,Redis ...

  9. 万字长文的Redis五种数据结构详解(理论+实战),建议收藏。

    本文脑图 前言 Redis是基于c语言编写的开源非关系型内存数据库,可以用作数据库.缓存.消息中间件,这么优秀的东西一定要一点一点的吃透它. 关于Redis的文章之前也写过三篇,阅读量和读者的反映都还 ...

最新文章

  1. ImageResizer for .net 图片处理强大类库
  2. angularjs结合d3js实现资源展示
  3. P03: 多重背包问题
  4. 模拟退火算法通俗讲解
  5. 如何将zipoutputstream返回_excel:vlookup如何区间查找?如何使用通配符?如何多条件查找?...
  6. 机器学习基础-一元线性回归-01
  7. oracle解析select,oracle_select语句例子解析
  8. java string输入_怎么让Java获取用户输入的字符串
  9. 这篇文章可以满足你80%日常工作!一线互联网公司面经总结
  10. One year ago begininginginginging!
  11. 最短路径问题(信息学奥赛一本通-T1342)
  12. STM32(三)-------流水灯(标准库函数)
  13. Google发转码工具 可将安卓程序转至iOS
  14. PHP 递归算法操作文件编程
  15. 国庆福利!384种故宫美色!Matlab中国风配色工具ColorPM
  16. 使用css样式做出亚克力背景和透明背景
  17. 如何在 Titanic Kaggle Challenge 中获得0.8134分
  18. hiho 1613 墨水滴 [Offer收割]编程练习赛32 Problem C 优先队列+BFS
  19. 跨次元!目标检测类别超20000!
  20. 目标定位算法(二)之基于测距的定位算法

热门文章

  1. 浅谈动态规划和分治、贪心算法的区别
  2. 贪心策略——哈夫曼编码
  3. 大咖直播 | 7月26日20点 ,我们请中信银行技术大咖聊聊GoldenDB的研发与应用实践...
  4. 为节省8亿做迁移,13亿记录出错赔偿29亿
  5. 举一反三:跨平台版本迁移之 XTTS 方案操作指南
  6. AI专家一席谈:复用算法、模型、案例,AI Gallery带你快速上手应用开发
  7. 3分钟了解ServiceStage 应用智能化运维【华为云分享】
  8. python让你再也不为文章配图与素材发愁,让高清图片占满你的硬盘! #华为云·寻找黑马程序员#
  9. ProjectMan是这样炼成的
  10. SpringCloud与Feign