• redis笔记【面试】


    前言

    2022/12/1 10:26

    以下内容源自A minor
    仅供学习交流使用

    推荐

    Redis

    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)
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    【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 做准备
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    【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 的优点设计出来的。
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    【Redis】基础结构(四):Set 类型命令、应用、原理

    Set 集合采用了整数集合和字典两种方式来实现的,当满足如下两个条件的时候,采用整数集合实现;一旦有一个条件不满足时则采用字典来实现。
    
    Set 集合中的所有元素都为整数
    Set 集合中的元素个数不大于 512(默认 512,可以通过修改 set-max-intset-entries 配置调整集合大小)
    
    
    整数集合(intset)
    
    intset 顾名思义,是由整数组成的集合。实际上,intset 是一个由整数组成的有序集合,从而便于在上面进行二分查找,用于快速地判断一个元素是否属于这个集合。
    
    字典(dict)
    
    在 Redis 中,hashtable 被称为字典(dictionary),它是一个数组+链表的结构。
    
    在之前的文章我们介绍了,Redis 的 KV 结构是通过一个 dictEntry 来实现的:
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    【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 跳表与红黑树
    
    
    ---------
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    【Redis】原理分析(一):底层结构、持久化、缓存淘汰策略

    1.Redis 底层结构
    
    Redis 单线程为什么还能这么快?
    内存
    
    Redis 单线程如何处理那么多的并发客户端连接?
    IO多路复用
    
    2.数据持久化
    2.1 RDB快照(snapshot)(默认)
    
    2.2 AOF(append-only file)
    
    2.3 混合持久化
    
    3.缓存淘汰策略
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    【Redis】事务浅析

    1.事务的用法
    
    2.事务可能遇到的问题
    
    3.为什么 Redis 事务不支持回滚
    
    4.总结
    Redis 的事务有如下几个特点:
    	按进入队列的顺序执行:事务中的所有命令都会序列化、按顺序地执行。
    	不受其他客户端请求影响:事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
    	没有隔离级别的概念:队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行(也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这个让人万分头痛的问题)
    	不保证原子性:Redis 同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
    在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的安全性。Redis 事务保证了其中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    【Redis】高阶使用(一):几个高级命令

    1.keys:全量遍历键
    2.scan:渐进式遍历键
    3.Info:查看 redis 服务运行信息
    
    • 1
    • 2
    • 3

    【Redis】高阶使用(二):管道与 lua 脚本

    【Redis】高阶使用(三):缓存穿透,缓存击穿,缓存雪崩及解决方案

    1.缓存穿透
    
    缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。
    
    缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。 造成缓存穿透的基本原因有两个:
    	自身业务代码或者数据出现问题。
    	一些恶意攻击、 爬虫等造成大量空命中。
    
    缓存穿透问题解决方案:
    方案一:缓存空对象
    方案二:布隆过滤器
    
    2.缓存击穿
    开发人员使用“缓存+过期时间”的策略既可以加速数据读写, 又保证数据的定期更新, 这种模式基本能够满足绝大部分需求。 但是有两个问题如果同时出现, 可能就会对应用造成致命的危害:
    	当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。
    	重建缓存不能在短时间完成, 可能是一个复杂计算, 例如复杂的SQL、 多次IO、 多个依赖等。
    在缓存失效的瞬间,有大量线程来重建缓存, 造成后端负载加大, 甚至可能会让应用崩溃。 要解决这个问题主要就是要避免大量线程同时重建缓存。 我们可以利用互斥锁来解决,此方法只允许一个线程重建缓存, 其他线程等待重建缓存的线程执行完, 重新从缓存获取数据即可。
    
    3.缓存雪崩
    缓存雪崩是指,我们设置缓存时采用了相同的过期时间,所以大批量缓存在同一时间失效,导致流量会像奔逃的野牛一样, 打向后端存储层。
    
    另外,预防和解决缓存雪崩问题, 还可以从以下三个方面进行着手:
    
    保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。
    依赖隔离组件为后端限流并降级。比如使用Hystrix限流降级组件。
    提前演练。 在项目上线前, 演练缓存层宕掉后, 应用以及后端的负载情况以及可能出现的问题, 在此基础上做一些预案设定。
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    【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操作,造成阻塞)
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    最后

    2022/12/2 17:30

    这篇博客能写好的原因是:站在巨人的肩膀上

    这篇博客要写好的目的是:做别人的肩膀

    开源:为爱发电

    学习:为我而行

  • 相关阅读:
    【代码随想录】算法训练营 第三天 第二章 链表 Part 1
    Spring Authorization Server 系列(二)获取授权码
    HTML网页设计作业:文化网站设计——基于HTML古典中国风工艺美术网页设计(9页)
    【postgres】安装、配置、主备、归档超详细介绍
    Docker本地部署Drupal并实现公网访问
    关于代码性能优化的总结
    使用vpn后,不能正常上网或连接vpn
    K8s控制器
    迷你主机的AIO(All in one)实战记录【pve+openwrt+windows+centos+群晖】(一)
    【算法】【递归与动态规划模块】字符串之间转换的最小代价
  • 原文地址:https://blog.csdn.net/qq_51625007/article/details/128127623