前言
2022/12/1 10:26
以下内容源自A minor
仅供学习交流使用
推荐
redis笔记【面试】
【Redis】关系型数据库与非关系型数据库
【Redis】基本操作及特性分析
【Redis】基础结构(一):String 类型命令、应用、原理
字符串类型的内部编码有三种: 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)
【Redis】基础结构(二):Hash 类型命令、应用、原理
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 做准备
【Redis】基础结构(三):List 类型命令、应用、原理
在 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 的优点设计出来的。
【Redis】基础结构(四):Set 类型命令、应用、原理
Set 集合采用了整数集合和字典两种方式来实现的,当满足如下两个条件的时候,采用整数集合实现;一旦有一个条件不满足时则采用字典来实现。 Set 集合中的所有元素都为整数 Set 集合中的元素个数不大于 512(默认 512,可以通过修改 set-max-intset-entries 配置调整集合大小) 整数集合(intset) intset 顾名思义,是由整数组成的集合。实际上,intset 是一个由整数组成的有序集合,从而便于在上面进行二分查找,用于快速地判断一个元素是否属于这个集合。 字典(dict) 在 Redis 中,hashtable 被称为字典(dictionary),它是一个数组+链表的结构。 在之前的文章我们介绍了,Redis 的 KV 结构是通过一个 dictEntry 来实现的:
【Redis】基础结构(五):ZSet 类型命令、应用、原理
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 跳表与红黑树 ---------
【Redis】原理分析(一):底层结构、持久化、缓存淘汰策略
1.Redis 底层结构 Redis 单线程为什么还能这么快? 内存 Redis 单线程如何处理那么多的并发客户端连接? IO多路复用 2.数据持久化 2.1 RDB快照(snapshot)(默认) 2.2 AOF(append-only file) 2.3 混合持久化 3.缓存淘汰策略
【Redis】事务浅析
1.事务的用法 2.事务可能遇到的问题 3.为什么 Redis 事务不支持回滚 4.总结 Redis 的事务有如下几个特点: 按进入队列的顺序执行:事务中的所有命令都会序列化、按顺序地执行。 不受其他客户端请求影响:事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 没有隔离级别的概念:队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行(也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这个让人万分头痛的问题) 不保证原子性:Redis 同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚 在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的安全性。Redis 事务保证了其中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)。
【Redis】高阶使用(一):几个高级命令
1.keys:全量遍历键 2.scan:渐进式遍历键 3.Info:查看 redis 服务运行信息
【Redis】高阶使用(二):管道与 lua 脚本
【Redis】高阶使用(三):缓存穿透,缓存击穿,缓存雪崩及解决方案
1.缓存穿透 缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。 缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。 造成缓存穿透的基本原因有两个: 自身业务代码或者数据出现问题。 一些恶意攻击、 爬虫等造成大量空命中。 缓存穿透问题解决方案: 方案一:缓存空对象 方案二:布隆过滤器 2.缓存击穿 开发人员使用“缓存+过期时间”的策略既可以加速数据读写, 又保证数据的定期更新, 这种模式基本能够满足绝大部分需求。 但是有两个问题如果同时出现, 可能就会对应用造成致命的危害: 当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。 重建缓存不能在短时间完成, 可能是一个复杂计算, 例如复杂的SQL、 多次IO、 多个依赖等。 在缓存失效的瞬间,有大量线程来重建缓存, 造成后端负载加大, 甚至可能会让应用崩溃。 要解决这个问题主要就是要避免大量线程同时重建缓存。 我们可以利用互斥锁来解决,此方法只允许一个线程重建缓存, 其他线程等待重建缓存的线程执行完, 重新从缓存获取数据即可。 3.缓存雪崩 缓存雪崩是指,我们设置缓存时采用了相同的过期时间,所以大批量缓存在同一时间失效,导致流量会像奔逃的野牛一样, 打向后端存储层。 另外,预防和解决缓存雪崩问题, 还可以从以下三个方面进行着手: 保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。 依赖隔离组件为后端限流并降级。比如使用Hystrix限流降级组件。 提前演练。 在项目上线前, 演练缓存层宕掉后, 应用以及后端的负载情况以及可能出现的问题, 在此基础上做一些预案设定。
【Redis】键值设计规范
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
这篇博客能写好的原因是:站在巨人的肩膀上
这篇博客要写好的目的是:做别人的肩膀
开源:为爱发电
学习:为我而行