赞
踩
常见的有五种:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。随着 Redis 版本的更新,后面又支持了四种数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。
String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数)
String 类型的底层的数据结构实现主要是 int 和 SDS(简单动态字符串)。
SDS 相比于 C 的原生字符串:
字符串对象的内部编码包括:int、embstr、raw
embstr与raw区别:
embstr会通过一次内存分配函数来分配一块连续的内存空间来保存redisObject和SDS
raw编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObject和SDS
embstr编码的字符串对象实际上是只读的,当对embstr编码的字符串对象执行修改命令时,程序会先将编码改为raw再执行修改命令
应用场景:
列表的最大长度为 2^32 - 1,也即每个列表支持超过 40 亿个元素。
List 类型的底层数据结构是由双向链表或压缩列表实现的:
如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;
如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;
在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。
应用场景:
1、消息队列
消息队列在存取消息时,必须要满足三个需求,分别是消息保序、处理重复的消息和保证消息可靠性。
消息保序:使用 LPUSH + RPOP;
阻塞读取:使用 BRPOP;
重复消息处理:生产者自行实现全局唯一 ID;
消息的可靠性:使用 BRPOPLPUSH
List 不支持多个消费者消费同一条消息(不支持消费组的实现)
键值对集合,适合存储对象
Hash 类型的底层数据结构是由压缩列表或哈希表实现的:
如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
应用场景:
1、缓存对象
Hash 类型的 (key,field, value) 的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
一般对象用 String + Json 存储,对象中某些频繁变化的属性可以考虑抽出来用 Hash 类型存储。
2、购物车
以用户 id 为 key,商品 id 为 field,商品数量为 value,恰好构成了购物车的3个要素
当前仅仅是将商品ID存储到了Redis 中,在回显商品具体信息的时候,还需要拿着商品 id 查询一次数据库,获取完整的商品的信息。
Set 类型和 List 类型的区别如下:
List 可以存储重复元素,Set 只能存储非重复元素;
List 是按照元素的先后顺序存储元素的,而 Set 则是无序方式存储元素的。
Set 类型的底层数据结构是由哈希表或整数集合实现的:
如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。
应用场景:
适合用来数据去重和保障数据的唯一性、还可用来统计多个集合的交集、错集和并集等
1、点赞:
Set 类型可以保证一个用户只能点一个赞
2、共同关注:
Set 类型支持交集运算,所以可以用来计算共同关注的好友、公众号等。
3、抽奖活动:
存储某活动中中奖的用户名 ,Set 类型因为有去重功能,可以保证同一个用户不会中奖两次。
Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),即每个存储元素相当于有两个值组成,一个是有序集合的元素值,一个是排序值。
Zset 类型的底层数据结构是由压缩列表或跳表实现的:
如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
应用场景:
Zset 类型(Sorted Set,有序集合) 可以根据元素的权重来排序(可自定义元素权重值)
在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,可以优先考虑使用 Sorted Set。
Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。
内部实现:
Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。String 类型是会保存为二进制的字节数组。把Bitmap 看作是一个 bit 数组。
应用场景:
1、签到统计
2、判断用户登陆态
3、连续签到用户总数
Redis HyperLogLog 是 Redis 2.8.9 版本新增的数据类型,是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数。(HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。)HyperLogLog 提供不精确的去重计数。
应用场景:
1、百万级网页 UV 计数(UV:用户访问量,也就是独立访客。简单点说就是访问某网站的电脑的数量)
Redis GEO 是 Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。
内部实现:
GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。
GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。
一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。
应用场景:
1、滴滴打车
Redis Stream 是 Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。
传统消息队列的缺陷:
基于Stream的消息队列:
Redis 消息中间件会不会丢消息?
会,Redis 在以下 2 个场景下,都会导致数据丢失:
像 RabbitMQ 或 Kafka 这类专业的队列中间件,在使用时是部署一个集群,生产者在发布消息时,队列中间件通常会写「多个节点」,也就是有多个副本,这样一来,即便其中一个节点挂了,也能保证集群的数据不丢失。
Redis Stream 消息可堆积吗?
Redis 的数据都存储在内存中,一旦发生消息积压,会导致 Redis 的内存持续增长,如果超过机器内存上限,就会面临被 OOM 的风险。
Redis 的 Stream 提供了可以指定队列最大长度的功能,当指定队列最大长度时,队列长度超过上限后,旧消息会被删除,只保留固定长度的新消息。(当Stream消息积压超过指定的最大长度,会发生消息丢失)
Redis 发布/订阅机制为什么不可以作为消息队列?
发布订阅机制存在以下缺点,都是跟丢失数据有关:
发布/订阅机制没有基于任何数据类型实现,所以不具备「数据持久化」的能力,也就是发布/订阅机制的相关操作,不会写入到 RDB 和 AOF 中,当 Redis 宕机重启,发布/订阅机制的数据也会全部丢失。
发布订阅模式是“发后既忘”的工作模式,如果有订阅者离线重连之后不能消费之前的历史消息。
当消费端有一定的消息积压时,也就是生产者发送的消息,消费者消费不过来时,如果超过 32M 或者是 60s 内持续保持在 8M 以上,消费端会被强行断开,这个参数是在配置文件中设置的,默认值是 client-output-buffer-limit pubsub 32mb 8mb 60。
Redis键值对数据库图:
void * key 和 void * value 指针指向的是 Redis 对象,Redis 中的每个对象都由 redisObject 结构表示
redisObject结构图:
对象结构里包含的成员变量:
C语言字符串的缺陷
结构设计
Redis 5.0 的 SDS 的数据结构如下图:
结构中成员变量:
优势:
O(1)复杂度获取字符串长度
二进制安全:用len成员变量来记录长度,可存储包含“\0”的数据( SDS 的 API 都是以处理二进制的方式来处理 SDS 存放在 buf[] 里的数据,程序不会对其中的数据做任何限制,数据写入的时候时什么样的,它被读取时就是什么样的)
不会发生缓冲区溢出:SDS API 通过 alloc - len 计算,可以算出剩余可用的空间大小,当空间不够时,自动将空间扩大(如果所需的 sds 长度小于 1 MB,那么最后的扩容是按照翻倍扩容来执行的,即 2 倍的newlen
如果所需的 sds 长度超过 1 MB,那么最后的扩容长度应该是 newlen + 1MB)
节省内存空间:设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间;还使用了专门的编译优化来节省内存空间,即在 struct 声明了 __ attribute __ ((packed)),作用是:告诉编译器取消结构体在编译过程中的优化对齐,按照实际占用字节数进行对齐
比如,sdshdr16 类型的 SDS,默认情况下,编译器会按照 2 字节对齐的方式给变量分配内存,这意味着,即使一个变量的大小不到 2 个字节,编译器也会给它分配 2 个字节。
链表节点结构设计
构成双向链表
链表结构设计
链表优势、劣势
优势:
获取某个节点的前置节点或后置节点的时间复杂度只需O(1),而且 prev 和 next 指针都可以指向 NULL,所以链表是无环链表
获取链表的表头节点和表尾节点、获取链表中的节点数量的时间复杂度只需O(1)
节点可以保存各种不同类型的值
劣势:
无法很好充分利用CPU缓存
保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大
结构设计
由连续内存块组成的顺序型数据结构
· zlbytes,记录整个压缩列表占用对内存字节数;
· zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
· zllen,记录压缩列表包含的节点数量;
· zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)。
查找第一个元素和最后一个元素,时间复杂度均为O(1),除此之外,其它元素查找只能顺序查找,时间复杂度为O(n)
节点结构
· prevlen,记录了「前一个节点」的长度,目的是为了实现从后向前遍历;
· encoding,记录了当前节点实际数据的「类型和长度」,类型主要有两种:字符串和整数。
· data,记录了当前节点的实际数据,类型和长度都由 encoding 决定;
Redis 为了节省内存而采用的根据数据大小和类型进行不同的空间大小分配的设计思想
prevlen 属性的空间大小跟前一个节点长度值有关:
· 如果前一个节点的长度小于 254 字节,那么 prevlen 属性需要用 1 字节的空间来保存这个长度值;
· 如果前一个节点的长度大于等于 254 字节,那么 prevlen 属性需要用 5 字节的空间来保存这个长度值;
encoding 属性的空间大小跟数据是字符串还是整数,以及字符串的长度有关:
· 如果当前节点的数据是整数,则 encoding 会使用 1 字节的空间进行编码,也就是 encoding 长度为 1 字节。通过 encoding 确认了整数类型,就可以确认整数数据的实际大小了,比如如果 encoding 编码确认了数据是 int16 整数,那么 data 的长度就是 int16 的大小。
· 如果当前节点的数据是字符串,根据字符串的长度大小,encoding 会使用 1 字节/2字节/5字节的空间进行编码,encoding 编码的前两个 bit 表示数据的类型,后续的其他 bit 标识字符串数据的实际长度,即 data 的长度。
连锁更新问题
压缩列表新增某个元素或修改某个元素时,如果空间不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。
缺陷:
连锁更新一旦发生,就会导致压缩列表占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能。只会用于保存的节点数量不多的场景
结构设计
哈希表是一个数组(dictEntry **table),数组的每个元素是一个指向「哈希表节点(dictEntry)」的指针
节点结构
dictEntry 结构里不仅包含指向键和值的指针,还包含了指向下一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对链接起来,以此来解决哈希冲突的问题,这就是链式哈希。
Redis采用链式哈希解决哈希冲突
每个哈希表节点都有一个 next 指针,用于指向下一个哈希表节点,被分配到同一个哈希桶上的多个节点可以用这个单项链表连接起来
rehash
当链表长度增加,查询链表上的数据就会增加耗时,此时需要对哈希表大小进行扩展
Redis 定义一个 dict 结构体,这个结构体里定义了两个哈希表(ht[2])。
在正常服务请求阶段,插入的数据都会写入到「哈希表 1」,此时的「哈希表 2 」 并没有被分配空间。
随着数据逐步增多,触发了 rehash 操作,这个过程分为三步:
问题:如果哈希表1数据量很大,在迁移至哈希表2时,会涉及大量数据拷贝,可能会对Redis造成阻塞
渐进式rehash
将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。
渐进式 rehash 步骤如下:
rehash触发条件
触发 rehash 操作的条件,主要有两个:
当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。
整数集合是 Set 对象的底层实现之一。当一个 Set 对象只包含整数值元素,并且元素数量不大时,就会使用整数集这个数据结构作为底层实现
结构设计
本身是一块连续内存空间
保存元素的容器是一个 contents 数组,contents 数组的真正类型取决于 intset 结构体里的 encoding 属性的值。
整数集合升级操作
整数集合升级的过程不会重新分配一个新类型的数组,而是在原本的数组上扩展空间,然后在将每个元素按间隔类型大小分割,如果 encoding 属性值为 INTSET_ENC_INT16,则每个元素的间隔就是 16 位。
整数集合升级的好处是节省内存资源,不支持降级操作。
Redis 只有 Zset 对象的底层实现用到了跳表,跳表的优势是能支持平均 O(logN) 复杂度的节点查找。
zset 结构体里有两个数据结构:一个是跳表,一个是哈希表。这样的好处是既能进行高效的范围查询,也能进行高效单点查询。
Zset 对象能支持范围查询(如 ZRANGEBYSCORE 操作),这是因为它的数据结构设计采用了跳表,而又能以常数复杂度获取元素权重(如 ZSCORE 操作),这是因为它同时采用了哈希表进行索引。
跳表结构设计
跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表
跳表结构里包含了:
跳表节点结构
Zset 对象要同时保存「元素」和「元素的权重」,每个跳表节点都有一个后向指针(struct zskiplistNode *backward),指向前一个节点,目的是为了方便从跳表的尾节点开始访问节点,这样倒序查找时很方便。
level 数组中的每一个元素代表跳表的一层。zskiplistLevel 结构体里定义了「指向下一个跳表节点的指针」和「跨度」,跨度是用来记录两个节点之间的距离。
跳表节点查询
查找一个跳表节点的过程时,跳表会从头节点的最高层开始,逐一遍历每一层。
在遍历某一层的跳表节点时,会用跳表节点中的 SDS 类型的元素和元素的权重来进行判断:
当不满足上述条件 或 下一个节点为空时,沿着下一层指针继续查找
跳表节点层数设置
跳表的相邻两层的节点数量的比例会影响跳表的查询性能。
如何维持相邻两层节点数量比例为2:1?
Redis 跳表在创建节点的时候,随机生成每个节点的层数,并没有严格维持相邻两层的节点数量比例为 2 : 1 的情况。
跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数。
**从内存占用上来比较,跳表比平衡树更灵活一些。**平衡树每个节点包含 2 个指针(分别指向左右子树),而跳表每个节点包含的指针数目平均为 1/(1-p),具体取决于参数 p 的大小。如果像 Redis里的实现一样,取 p=1/4,那么平均每个节点包含 1.33 个指针,比平衡树更有优势。
**在做范围查找的时候,跳表比平衡树操作要简单。**在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在跳表上进行范围查找就非常简单,只需要在找到小值之后,对第 1 层链表进行若干步的遍历就可以实现。
**从算法实现难度上来比较,跳表比平衡树要简单得多。**平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单又快速。
在 Redis 3.2 的时候,List 对象的底层改由 quicklist 数据结构实现。quicklist 就是「双向链表 + 压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表。
quicklist解决压缩列表的连锁更新:通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题。因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能。
结构设计
节点结构设计
链表节点的元素不再是单纯保存元素值,而是保存了一个压缩列表,通过*zl指针指向压缩列表
quicklist添加元素,会检查插入位置的压缩列表是否能容纳该元素,如果能容纳就直接保存到 quicklistNode 结构里的压缩列表,如果不能容纳,才会新建一个新的 quicklistNode 结构。
Redis 在 5.0 新设计一个数据结构叫 listpack,目的是替代压缩列表
listpack 中每个节点不再包含前一个节点的长度了,压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患。
结构设计
listpack 节点会采用不同的编码方式保存不同大小的数据。
节点结构设计
主要包含三个方面内容:
listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。