Redis对象详解!!!快来了解一下吧

114 阅读12分钟

String

一、 概念

String 就是字符串,它是Redis中最基本的数据对象,最大为512MB,可以修改;

二、使用场景

用来存字节数据,文本数据,序列化后的对象数据等

计数场景;因为 Redis 处理命令是单线程, 所以执行命令的过程是原子的

三、底层实现

1. 三种编码方式

String的三种编码方式:

image-20240707142423266

int 编码:存一个整型;在long的范围内的整数,如果超过将以字符串形式存储

embstr 编码:字符串小于等于阈值字节,使用 embstr 编码

raw 编码:字符串大于阈值字节,则用 raw 编码

embstr 和 raw 都是由 redisObject 和 SDS两个结构组成

image-20240707143012678

区别在于:

  • embstr :redisObject 和 SDS 是连续的内存空间
  • raw :redisObject 和 SDS 的内存是分开的

embstr 这样设计的优点是可以一次性分配空间,缺点在于如何重新分配空间,整体需要再分配,所以 embstr 设计为只读,任何写操作之后 embstr 都会变成 raw,理念是发生过修改的字符串通常都是易变的;因为 embstr 重新分配代价更高(需要整体重新分批额),而 raw 的 redisObject 不会重新分配

2. 为什么要用SDS

简单动态字符串拼接;更方便的使用字符串

主要解决 c 语言数组使用不方便的问题:

  • 计算字符串长度为 o(N)
  • 对字符串追加,需要重新分配内存(数组扩容)
  • 非二进制安全('\0')

SDS 的结构:

image-20240707145235066

关键字段:

  • len:表示使用了多少内存
  • alloc:表示分配多少内存

alloc-len:这两个字段差值就是预留空间的大小;

针对 c 语言方式对症下药:

  • 增加长度 len
  • 增加空余空间 alloc-len
  • 不在以'/0'作为判断标准,二进制安全

预留空间规则:

  • len < 1M:alloc = 2 * len,预留 len 大小的空间
  • len > 1M:alloc = 1M + len,预留1M 大小的空间

List

一、概述

Redis List 是一组连接起来的字符串集合

最大元素个数 2 ^ 32 - 1

二、适用场景

比如存储一批任务数据,存储一批消息等

三、底层实现

1. 编码方式

3.2版本之前,List 对象有两种编码方式,一种是 ziplist,另一种是 linkedlist

image-20240707153005674

当满足如下条件后,用 ziplist 编码:

  • 列表对象保存的所有字符串对象长度都小于64字节
  • 列表元素对象个数少于512个;注意,这是 list 的限制,不是 ziplist 的限制

ziplist 底层用压缩列表实现;数据是紧凑相连的

不满足条件,则使用 linkedlist 编码:

  • 列表个数或节点数据长度比较大的时候

2. quicklist

ziplist是为了数据较少时节约内存,linkedlist 是为数据多时提高更新效率

但是,ziplist 数据稍多时插入数据会导致很多内存复制,使用linkedlist的话 节点就会很多,占用不少内存

ziplist 内存是整体分配的;

插入元素需要重新分配

为了优化这些问题,3.2之后引入了quicklist(ziplist 和 linkedlist结合体)

image-20240707155120456

linkedlist原来时单个节点,只能存一个数据,现在时单个节点存的是一个ziplist,及多个数据

image-20240707155207641

这种方案其实是用ziplist,linkedlist综合的结构,取代二者本身

当数据较少的时候,quicklist 的节点就只有一个,此时其实相当于一个ziplist

当数据很多的时候,则同时利用了ziplist和linkedlist的优势

3. ziplist优化

ziplist本身存在一个连锁更新的问题,所以再redis 7.0之后,使用了listpack的编码模式取代了ziplist,而他们本质都是一种压缩的列表

四、底层数据结构压缩链表

1. 压缩列表是什么

顾名思义,排列紧凑的列表

2. 压缩列表解决什么问题

压缩列表主要作为底层数据结构提供紧凑型的数据存储方式,能节约内存(节省指针的开销),小数据量的时候遍历性能好(连续 + 缓存命中率高)

3. ziplist整体结构

比如:3个节点的ziplist结构

image-20240707162209996

字段含义:

  • zlbytes:该zplist总共多少字节,包含zlbytes

  • zltail:ziplist尾巴节点相对于ziplist的开头,偏移字节数;

    • 通过这个字段可以快速定位到尾部
    • 假设有一个ziplist 开头为zl,如果要获取尾巴节点,则 zl + zltail
    • 如果没有尾节点,就定位到zlend
  • zllen:多少个数据节点

  • entryx:数据节点

  • zlend:一个特殊的entry节点,表示ziplist的结束

4. ziplist 节点结构

 <prevlen> <encoding> <entry-data>
  • prevlen:表示上一个节点的数据长度。

    • 前一个节点(前一个entry)的大小,小于254字节,prevlen 属性用1字节来保存这个长度值,255是特殊值,被zlend使用了
    • 前一个节点的长度大于 254,那么prevlen属性需要用5字节的长度空间来保存这个长度值,注意 5个字节中的第一个字节为11111110,也就是254,标志这是个5个字节的prevlen信息,剩下4字节用来表示大小
  • encoding:编码类型。编码类型还包含了一个entry的长度信息

    • encoding字段是一个整形数据,其二进制编码由内容数据的类型内容数据的字节长度两部分组成
    • String类型:encoding由两部分,一般前几位标识类型,后几位标识长度
    • int类型:整体1字节编码,只标识了类型
  • entry-data:实际的数据

5. ziplist查询数据

查询数据总量:

ziplist的header定义了记录节点数量的字段zllen,所以通常是可以在o(1)时间复杂度直接返回的

但是,zllen是2个字节,也就是说当zllen 最大65534时,再多需要遍历获取

查询指定数据总量:

变量整个压缩列表,复杂度o(N)

6. ziplist更新数据

ziplist更新操作平均复杂度为o(N);因为增加一个节点会导致后面节点都往后移;

其中要主要的更新操作可能带来连锁更新。连锁更新是指发生了多次,而不是一次;

比如,增加一个头部新节点,后面依赖它的节点,需要用prevlen记录它的大小,原本只需要一字节,但是如果这个新节点大于254,后面节点需要的prevlen需要变为5个字节,导致连锁反应

7. listpack优化

listpack是为了解决ziplist最大的痛点——连锁更新,主要就是因为 ziplist中的prevlen 记录了 上一个节点的数据长度导致的

listpack解决了这个问题,看listpack的节点定义

 <encoding-type><element-data><element-tot-len>
  • encoding-type:编码类型
  • element-data:数据内容
  • element-tot-len:存储整个节点除它自身之外的长度(type + data)

重点就是element-tot-len:

element-tot-len 所占用的每个字节的第一个bit用于标识是否结束0 是结束, 1 是继续;剩下的7个bit来存储大小

当我们需要上一个元素时,我们可以从后向前一次查找每个字节,找到上一个entry的element-tot-len字段的结束标识,就可以算出上一个节点的首位置了

Hash

一、概述

Redist Hash是一个field,value都为String的hash表,存储在Redis的内存中

Redis中每个hash可以存储2(32)-1键值对

二、适用场景

适用于o(1)时间字典查找某个field对应数据的场景,比如任务信息的配置,就可以任务类型为field,任务配置参数为value

三、编码方式

Hash底层有两种编码结构,一个是压缩列表,一个是hashtable;

image-20240707194749887

同时满足以下两个条件,用压缩列表:

  • hash对象保存的所有值和键的长度都小于64字节
  • hash对象元素个数少于512个

ziplist就是再数据量较小时将数据紧凑排序,对应到hash,就是将field-value当作entry放入ziplist

否则适用hashtable:

hashtable在set中也有应用,和set的区别在于,在set中value始终为null,但是在hash中,是有对应的值的

image-20240707195112014

四、底层数据结构HashTable

1.概述

hashtable,可以想象成目录,要翻看什么内容,直接通过目录能找到页数,翻过去看,如果没有目录,我们需要一页一页往后翻,效率很低

在计算机世界里,hashtalbe就扮演着这样一个快速索引的角色,通过hashtable我们可以只用o(1)时间复杂度就能快速找到key对应的value

2. HashTable结构

image-20240707201436587

最外层是一个dictht结构,其中字段含义如下:

table:指向实际hash存储,存储可以看做一个数组,所以是*table的表示,在C语言*table可以表示一个数组

size:哈希表大小

sizemask:哈希表大小的掩码表示,总是等于size - 1;这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面,规则index = hash & sizemask

used:表示已经使用的节点数量。

3. Hash表渐进式扩容

顾名思义就是一点一点慢慢扩容,而不是一股脑直接做完;

其实为了渐进式扩容,Redis中没有直接把dictht暴露给上层,而是在封装了一层

dict结构里面,包含了2个dictht结构,也就是2个hashtable结构,dictEntry是链表结构,也就是用拉链法解决Hash冲突,用的是头插法

image-20240709143948180

实际上平常使用的就是一个hashtable,在触发扩容之后,就会有两个HashTable同时使用

过程如下:

  • 向字典添加元素,发现需要扩容就会Rehash
  • 增加新表 ht[1] , 新表的大小为第一个大于等于原表2倍used的2次幂;
  • 将字典的偏移索引从静默状态-1,设置为0,表示Rehash工作的开始
  • 在每次对字典执行增删改查操作,程序还会顺带迁移 ht[0]哈希表在rehashidx索引上的所有键值对rehash到 ht[1] ,完成之后,程序还将rehashidx属性的值赠一
  • 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] ,然后会将ht[1]ht[0]的对象指针互换, 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

ReHash的作用就是让hash表的负载因子维持在一个合理的范围内,避免哈希表的键值对过多或过少

总结一下:渐进式扩容的核心就是操作时顺带迁移

4. 扩容时机

Redis提出了一个负载因子的概念,负载因子表示目前Redis hashtable的负载情况

设负载因子为k,那么k = ht[0].user / ht[0].size;也就是使用空间和总空间的大小的比例

Redis 会根据负载因子的情况来扩容

  • 负载因子大于等于1,说明空间非常紧张。新数据是在链表上叠加的,越来越多的数据无法在o(1)时间复杂度找到,还需要遍历一次链表,如果此时服务器没有执行bgsave或bgrewriteaof这两个命令,就会发生扩容
  • 负载因子 > 5, 不堪重负了,即使复制命令在进行,也要进行Rehash了

5.缩容

Redis还是用负载因子来控制什么使用缩容

当负载因子小于0.1,此时进行缩容,新表大小为第一个大于等于原表used的2次幂

当然,bgsave或bgwriteaof这两个复制命令,缩容也会进行,不会进行

Set

一、概述

Redis 的 Set 是一个不重复,无序的字符串集合;

如果是 intset 编码的时候其实是有序的,但是不应该依赖这个,整体还是无序来说

二、使用场景

适用于无序集合场景,比如某个用户关注了哪个人

Set还提供了查交集,并集的能力,可以很方便地实现共同关注的能力

三、编码方式

image-20240707192251785

Redis 出于性能和内存的综合考虑,也支持两种编码方式:

如果集合都是整数,且元素数量不超过512个,就可以用intset编码

image-20240707192427060

intset排列比较紧凑,内存占用少,但是查询时需要二分查找

否则,使用hashtable:

hashtable查询一个元素的性能很高,能o(1)时间就能找到一个元素是否存在

image-20240707192809988

ZSet

一、概述

ZSet就是有序集合,是一组按关联积分有序的字符串集合,这里的分数是否抽象概念,任何指标都可以抽象为分数,以满足不同场景

二、适用场景

用与需要排序集合的场景,最为典型的就是游戏排行榜

三、编码方式

ZSet底层编码有两种,一种是ziplist,一种是skiplist + hashtable

image-20240709154448483

如果满足以下规则,ZSet就用ziplist编码

  • 列表对象保存的所有字符串对象长度都小于64字节
  • 列表对象元素少于128个

image-20240709160016282

否则,适用skiplist

skiplist是一种可以快速查找的多级链表结构,通过skiplist可以快速定位到数据所在

Redis还使用了hashtable来配合查询,这样可以在o(1)时间复杂度查到成员的分数值

image-20240709160115191

四、底层数据结构跳表

1. 跳表的结构

跳表本质上还是链表,在链表的基础上,给链表增加了多级的索引,通过索引可以一次实现多个接待你的跳跃,提高性能

image-20240709161035473

查询规则:每次查找时走最高索引,如果超过目标值,则走下级索引

标准的跳表有如下限制:

  • score 值不能重复
  • 只有向前指针,没有回退指针

2. Redis的跳表实现

score可以重复并且每个节点多了一个回退指针

image-20240709161437130

Redis跳表单个节点的定义:

 typedef struct zskiplistNode {
     sds ele;
     double score;
     struct zskiplistNode *backward;
     struct zskiplistLevel {
         struct zskiplistNode *forward;
         unsigned long span;
     } level[];
 } zskiplistNode;

字段含义:

  • ele:SDS结构,存储数据

  • score:节点的分数,浮点型

  • backward:指向上一个节点的回退指针

  • level:是个zskiplistlevel结构体数组

    • forward:该层下个能跳到的节点
    • span:距离下个节点的步数

image-20240709162419574

性能优化了多少?

O(N) => O(log n)最坏还是O(N)