String
一、 概念
String 就是字符串,它是Redis中最基本的数据对象,最大为512MB,可以修改;
二、使用场景
用来存字节数据,文本数据,序列化后的对象数据等
计数场景;因为 Redis 处理命令是单线程, 所以执行命令的过程是原子的
三、底层实现
1. 三种编码方式
String的三种编码方式:
int 编码:存一个整型;在long的范围内的整数,如果超过将以字符串形式存储
embstr 编码:字符串小于等于阈值字节,使用 embstr 编码
raw 编码:字符串大于阈值字节,则用 raw 编码
embstr 和 raw 都是由 redisObject 和 SDS两个结构组成
区别在于:
- embstr :redisObject 和 SDS 是连续的内存空间
- raw :redisObject 和 SDS 的内存是分开的
embstr 这样设计的优点是可以一次性分配空间,缺点在于如何重新分配空间,整体需要再分配,所以 embstr 设计为只读,任何写操作之后 embstr 都会变成 raw,理念是发生过修改的字符串通常都是易变的;因为 embstr 重新分配代价更高(需要整体重新分批额),而 raw 的 redisObject 不会重新分配
2. 为什么要用SDS
简单动态字符串拼接;更方便的使用字符串
主要解决 c 语言数组使用不方便的问题:
- 计算字符串长度为 o(N)
- 对字符串追加,需要重新分配内存(数组扩容)
- 非二进制安全('\0')
SDS 的结构:
关键字段:
- 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
当满足如下条件后,用 ziplist 编码:
- 列表对象保存的所有字符串对象长度都小于64字节
- 列表元素对象个数少于512个;注意,这是 list 的限制,不是 ziplist 的限制
ziplist 底层用压缩列表实现;数据是紧凑相连的
不满足条件,则使用 linkedlist 编码:
- 列表个数或节点数据长度比较大的时候
2. quicklist
ziplist是为了数据较少时节约内存,linkedlist 是为数据多时提高更新效率
但是,ziplist 数据稍多时插入数据会导致很多内存复制,使用linkedlist的话 节点就会很多,占用不少内存
ziplist 内存是整体分配的;
插入元素需要重新分配
为了优化这些问题,3.2之后引入了quicklist(ziplist 和 linkedlist结合体)
linkedlist原来时单个节点,只能存一个数据,现在时单个节点存的是一个ziplist,及多个数据
这种方案其实是用ziplist,linkedlist综合的结构,取代二者本身
当数据较少的时候,quicklist 的节点就只有一个,此时其实相当于一个ziplist
当数据很多的时候,则同时利用了ziplist和linkedlist的优势
3. ziplist优化
ziplist本身存在一个连锁更新的问题,所以再redis 7.0之后,使用了listpack的编码模式取代了ziplist,而他们本质都是一种压缩的列表
四、底层数据结构压缩链表
1. 压缩列表是什么
顾名思义,排列紧凑的列表
2. 压缩列表解决什么问题
压缩列表主要作为底层数据结构提供紧凑型的数据存储方式,能节约内存(节省指针的开销),小数据量的时候遍历性能好(连续 + 缓存命中率高)
3. ziplist整体结构
比如:3个节点的ziplist结构
字段含义:
-
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;
同时满足以下两个条件,用压缩列表:
- hash对象保存的所有值和键的长度都小于64字节
- hash对象元素个数少于512个
ziplist就是再数据量较小时将数据紧凑排序,对应到hash,就是将field-value当作entry放入ziplist
否则适用hashtable:
hashtable在set中也有应用,和set的区别在于,在set中value始终为null,但是在hash中,是有对应的值的
四、底层数据结构HashTable
1.概述
hashtable,可以想象成目录,要翻看什么内容,直接通过目录能找到页数,翻过去看,如果没有目录,我们需要一页一页往后翻,效率很低
在计算机世界里,hashtalbe就扮演着这样一个快速索引的角色,通过hashtable我们可以只用o(1)时间复杂度就能快速找到key对应的value
2. HashTable结构
最外层是一个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冲突,用的是头插法
实际上平常使用的就是一个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还提供了查交集,并集的能力,可以很方便地实现共同关注的能力
三、编码方式
Redis 出于性能和内存的综合考虑,也支持两种编码方式:
如果集合都是整数,且元素数量不超过512个,就可以用intset编码
intset排列比较紧凑,内存占用少,但是查询时需要二分查找
否则,使用hashtable:
hashtable查询一个元素的性能很高,能o(1)时间就能找到一个元素是否存在
ZSet
一、概述
ZSet就是有序集合,是一组按关联积分有序的字符串集合,这里的分数是否抽象概念,任何指标都可以抽象为分数,以满足不同场景
二、适用场景
用与需要排序集合的场景,最为典型的就是游戏排行榜
三、编码方式
ZSet底层编码有两种,一种是ziplist,一种是skiplist + hashtable
如果满足以下规则,ZSet就用ziplist编码
- 列表对象保存的所有字符串对象长度都小于64字节
- 列表对象元素少于128个
否则,适用skiplist
skiplist是一种可以快速查找的多级链表结构,通过skiplist可以快速定位到数据所在
Redis还使用了hashtable来配合查询,这样可以在o(1)时间复杂度查到成员的分数值
四、底层数据结构跳表
1. 跳表的结构
跳表本质上还是链表,在链表的基础上,给链表增加了多级的索引,通过索引可以一次实现多个接待你的跳跃,提高性能
查询规则:每次查找时走最高索引,如果超过目标值,则走下级索引
标准的跳表有如下限制:
- score 值不能重复
- 只有向前指针,没有回退指针
2. Redis的跳表实现
score可以重复并且每个节点多了一个回退指针
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:距离下个节点的步数
性能优化了多少?
O(N) => O(log n)最坏还是O(N)