2022/12/1 10:26
以下内容源自A minor
仅供学习交流使用
字符串类型的内部编码有三种:
int:存储 8 个字节的长整型(long,2^63-1)
embstr:代表 embstr 格式的 SDS, 存储小于 44 个字节的字符串
raw:代表 raw 格式的 SDS,存储大于 44 个字节的字符串(3.2 版本之前是 39 字节)
问题一:为什么 Redis 要用 SDS 实现字符串?
SDS
空间预分配:SDS 会预先分配一部分空闲空间,当字符串内容添加时不需要做空间申请的工作
空间惰性释放:当字符串从 buf 数组中移除时,空闲出来的空间不会立马被内存回收,防止新增字符串的内容写入时空间不够而临时申请空间
问题二:embstr 和 raw 的区别?
embstr 的使用只分配一次内存空间(因为 RedisObject 和 SDS 是连续的),而 raw 需要分配两次内存空间(分别为 RedisObject 和 SDS 分配空间)。
因此与 raw 相比,embstr 的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。 而 embstr 的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个 RedisObject 和 SDS 都需要重新分配空间,因此 Redis 中的 embstr 实现为只读。
问题三:int 和 embstr 什么时候转化为 raw?
当 int 数 据 不 再 是 整 数 , 或大小超过了 long 的范围 (2^63-1=9223372036854775807)时,自动转化为 embstr。
问题四:明明没有超过阈值,为什么变成 raw?
对于 embstr,由于其实现是只读的,因此在对 embstr 对象进行修改时,都会先转化为 raw 再进行修改。 因此,只要是修改 embstr 对象,修改后的对象一定是 raw 的,无论是否达到了 44 个字节。
问题五:当长度小于阈值时,会还原吗?
关于 Redis 内部编码的转换,都符合以下规律:编码转换在 Redis 写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换(但是不包括重新 set)
Hash 底层实现采用了 ZipList 和 HashTable 两种实现方式。
当同时满足如下两个条件时底层采用了 ZipList 实现,一旦有一个条件不满足时,就会被转码为 HashTable 进行存储
Hash 中存储的所有元素的 key 和 value 的长度都小于等于 64byte
Hash 中存储的元素个数小于 512
ZipList(压缩列表) 方式
ZipList 的优缺点比较
优点:内存地址连续,省去了每个元素的头尾节点指针占用的内存
缺点:对于删除和插入操作比较可能会触发连锁更新反应,比如在 list 中间插入删除一个元素时,在插入或删除位置后面的元素可能都需要发生相应的移动操作
HashTable 方式
在 Redis 中,hashtable 被称为字典(dictionary),它是一个数组+链表的结构。
问题:为什么要定义两个哈希表呢?
ht[2] redis 的 hash,默认使用的是 ht[0],ht[1]不会初始化和分配空间。
哈希表 dictht 是用链地址法来解决碰撞问题的。在这种情况下,哈希表的性能取决于它的大小(size 属性)和它所保存的节点的数量(used 属性)之间的比率,比率在 1:1 时,哈希表的性能最好;。
如果节点数量比哈希表的大小要大很多的话(这个比例用 ratio 表示,ratio = used / size),那么哈希表就会退化成多个链表,哈希表本身的性能优势就不再存在,所以,在这种情况下需要扩容。
问题:为什么要定义两个哈希表呢?
ht[2] redis 的 hash,默认使用的是 ht[0],ht[1]不会初始化和分配空间。
哈希表 dictht 是用链地址法来解决碰撞问题的。在这种情况下,哈希表的性能取决于它的大小(size 属性)和它所保存的节点的数量(used 属性)之间的比率,比率在 1:1 时,哈希表的性能最好;。
如果节点数量比哈希表的大小要大很多的话(这个比例用 ratio 表示,ratio = used / size),那么哈希表就会退化成多个链表,哈希表本身的性能优势就不再存在,所以,在这种情况下需要扩容。
dict_can_resize 为 1 并且 dict_force_resize_ratio 已使用节点数和字典大小之间的 比率超过 1:5,触发扩容
rehash 的步骤:
1.为字符 ht[1] 哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及 ht[0] 当前包含的键值对的数量。
2.将所有的 ht[0] 上的节点 rehash 到 ht[1]上,重新计算 hash 值和索引,然后放入指定的位置。
3.当 ht[0]全部迁移到了 ht[1]之后,释放 ht[0] 的空间,将 ht[1] 设置为 ht[0] 表, 并创建新的 ht[1],为下次 rehash 做准备
在 Redis3.2 之前,List 底层采用了 ZipList 和 LinkedList 实现的。
初始化的 List 使用的 ZipList,List 满足以下两个条件时则一直使用 ZipList 作为底层实现,当以下两个条件任一一个不满足时,则会被转换成 LinkedList
List 中存储的每个元素的长度小于 64byte
元素个数小于 512
3.2 版本之后,List 底层采用 QuickList 来存储。quicklist 存储了一个双向链表,每个节点都是一个 ziplist。
ZipList 方式
压缩列表是 redis 为了节约内存而开发的,是由一系列的特殊编码的连续内存块组成的双向链表。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值,值的类型和长度由节点的encoding属性决定。
LinkedList 方式
LinkedList 都比较熟悉了,是由一系列不连续的内存块通过指针连接起来的双向链表。
QuickList 方式
在 Redis3.2 版本之后,Redis 集合采用了 QuickList 作为 List 的底层实现,QuickList 其实就是结合了 ZipList 和 LinkedList 的优点设计出来的。
Set 集合采用了整数集合和字典两种方式来实现的,当满足如下两个条件的时候,采用整数集合实现;一旦有一个条件不满足时则采用字典来实现。
Set 集合中的所有元素都为整数
Set 集合中的元素个数不大于 512(默认 512,可以通过修改 set-max-intset-entries 配置调整集合大小)
整数集合(intset)
intset 顾名思义,是由整数组成的集合。实际上,intset 是一个由整数组成的有序集合,从而便于在上面进行二分查找,用于快速地判断一个元素是否属于这个集合。
字典(dict)
在 Redis 中,hashtable 被称为字典(dictionary),它是一个数组+链表的结构。
在之前的文章我们介绍了,Redis 的 KV 结构是通过一个 dictEntry 来实现的:
Zset 底层同样采用了两种方式来实现,分别是 ZipList 和 SkipList。当同时满足以下两个条件时,采用 ZipList 实现;反之采用 SkipList 实现。
Zset 中保存的元素个数小于 128。(通过修改 zset-max-ziplist-entries 配置来修改)
Zset 中保存的所有元素长度小于 64byte。(通过修改 zset-max-ziplist-values 配置来修改)
ZipList 方式
SkipList 方式
跳表实际就是:多级有序链表,这样我们就可以抽出索引节点,从而降低查询的复杂度。
PS:为什么不用 AVL 树或者红黑树?因为 skiplist 更加简洁(关于跳表可以参考这篇文章…)
https://yzhyaa.blog.csdn.net/article/details/108930330
-------
【数据结构】跳表:Skip List 特性浅析
1.跳表 = 有序链表+多级索引
2.时间复杂度分析
2.1 查询:O(logn))
跳表其实是基于单链表实现了二分查找
2.2 插入:O(logn)
2.3 删除
3.空间复杂度分析
O(n)
4.索引动态更新
4.1 退化成链表的问题
4.2 随机函数更新索引
们通过一个随机函数,来决定将这个结点插入到哪几级索引中,比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第 K 级这 K 级索引中。
5.跳表应用
5.1 redis有序集合
按照区间来查找数据这个操作,红黑树的效率没有跳表高。
对于按照区间查找数据这个操作,跳表可以做到 O(logn) 的时间复杂度定位区间的起点, 然后在原始链表中顺序往后遍历就可以了。这样做非常高效。
跳表更容易代码实现。 虽然跳表的实现也不简单,但比起红黑树来说还是好懂、好写多了,而简单就意味着可读性好,不容易出错。
跳表更加灵活,它可以通过改变索引构建策略,有效平衡执行效率和内存消耗。
5.2 跳表与红黑树
---------
1.Redis 底层结构
Redis 单线程为什么还能这么快?
内存
Redis 单线程如何处理那么多的并发客户端连接?
IO多路复用
2.数据持久化
2.1 RDB快照(snapshot)(默认)
2.2 AOF(append-only file)
2.3 混合持久化
3.缓存淘汰策略
1.事务的用法
2.事务可能遇到的问题
3.为什么 Redis 事务不支持回滚
4.总结
Redis 的事务有如下几个特点:
按进入队列的顺序执行:事务中的所有命令都会序列化、按顺序地执行。
不受其他客户端请求影响:事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
没有隔离级别的概念:队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行(也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这个让人万分头痛的问题)
不保证原子性:Redis 同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的安全性。Redis 事务保证了其中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)。
1.keys:全量遍历键
2.scan:渐进式遍历键
3.Info:查看 redis 服务运行信息
1.缓存穿透
缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。
缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。 造成缓存穿透的基本原因有两个:
自身业务代码或者数据出现问题。
一些恶意攻击、 爬虫等造成大量空命中。
缓存穿透问题解决方案:
方案一:缓存空对象
方案二:布隆过滤器
2.缓存击穿
开发人员使用“缓存+过期时间”的策略既可以加速数据读写, 又保证数据的定期更新, 这种模式基本能够满足绝大部分需求。 但是有两个问题如果同时出现, 可能就会对应用造成致命的危害:
当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。
重建缓存不能在短时间完成, 可能是一个复杂计算, 例如复杂的SQL、 多次IO、 多个依赖等。
在缓存失效的瞬间,有大量线程来重建缓存, 造成后端负载加大, 甚至可能会让应用崩溃。 要解决这个问题主要就是要避免大量线程同时重建缓存。 我们可以利用互斥锁来解决,此方法只允许一个线程重建缓存, 其他线程等待重建缓存的线程执行完, 重新从缓存获取数据即可。
3.缓存雪崩
缓存雪崩是指,我们设置缓存时采用了相同的过期时间,所以大批量缓存在同一时间失效,导致流量会像奔逃的野牛一样, 打向后端存储层。
另外,预防和解决缓存雪崩问题, 还可以从以下三个方面进行着手:
保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。
依赖隔离组件为后端限流并降级。比如使用Hystrix限流降级组件。
提前演练。 在项目上线前, 演练缓存层宕掉后, 应用以及后端的负载情况以及可能出现的问题, 在此基础上做一些预案设定。
1.key 设计
【建议】可读性和可管理性。
【建议】 简洁性。
【强制】不要包含特殊字符。
2.value 设计
【建议】选择适合的数据类型。
【建议】控制key的生命周期,redis不是垃圾桶。
建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期)。
【强制】拒绝 bigkey。(防止网卡流量、慢查询)
关于 bigKey
问题一:Redis 的 bigkey 具体指什么?
在Redis中,一个字符串大512MB,一个二级数据结构(例如hash、list、set、zset)可以存储大约40亿个(2^32-1)个元素,但实际中如果下面两种情况,一般就会认为它是bigkey:
字符串类型:它的big体现在单个value值很大,一般认为超过10KB就是bigkey
非字符串类型:哈希、列表、集合、有序集合,它们的big体现在元素个数太多。
一般来说,string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。 反例:一个包含200万个元素的list。
问题二:bigkey 有什么危害?
导致redis阻塞(过期删除)。有个bigkey,它安分守己(只执行简单的命令,例如hget、lpop、zscore等),但它设置了过期时间,当它过期后,会被删除,如果没有使用Redis 4.0的过期异步删除(lazyfree-lazyexpire yes),就会存在阻塞Redis的可能性。
网络拥塞。bigkey也就意味着每次获取要产生的网络流量较大,假设一个bigkey为1MB,客户端每秒访问量为1000,那么每秒产生1000MB的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个bigkey 可能会对其他实例也造成影响,其后果不堪设想。
问题三:什么情况下会产生 bigkey?
大多数情况下,bigkey的产生都是由于程序设计不当,或者对于数据规模预料不清楚造成的,来看几个例子:
社交类:粉丝列表,如果某些明星或者大v不精心设计下,必是bigkey。
统计类:例如按天存储某项功能或者网站的用户集合,除非没几个人用,否则必是bigkey。
缓存类:将数据从数据库load出来序列化放到Redis里,这个方式非常常用,但有两个地方需要注意:
第一,是不是有必要把所有字段都缓存
第二,有没有相关关联的数据,有的同学为了图方便把相关数据都存一个key下,产生bigkey
问题四:如果出现了 bigkey,那该如何优化?
第一个想法就是拆,看能不能将bigkey缩小:
big list: list1、list2、…listN
big hash:可以将数据分段存储,比如一个大的key,假设存了1百万的用户数据,可以拆分成 200个key,每个key下面存放5000个用户数据
如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出来(例如有时候仅仅需要 hmget,而不是hgetall),删除也是一样,尽量使用优雅的方式来处理。比如,非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除。
最后,还要注意防止 bigkey 过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞)
2022/12/2 17:30
这篇博客能写好的原因是:站在巨人的肩膀上
这篇博客要写好的目的是:做别人的肩膀
开源:为爱发电
学习:为我而行