【面试题】redis

180 阅读12分钟

redis是单线程还是多线程

redis 4.0 之前,redis 是完全单线程的。 4.0开始引入多线程,但是核心流程还是单线程的。 核心流程:接收命令、解析命令、执行命令、返回结果等

redis 6.0 版本又一次引入了多线程概念,与 4.0 不同的是,这次的多线程会涉及到上述的核心流程。 redis 6.0 中,多线程主要用于网络 I/O 阶段,也就是接收命令写回结果阶段,而在执行命令阶段,还是由单线程串行执行。由于执行时还是串行,因此无需考虑并发安全问题。

redis 6.0 执行流程介绍:

  1. 当有读事件到来时,主线程将该客户端连接放到全局等待读队列
  2. 读取数据
    1. 主线程将等待读队列的客户端连接通过轮询调度算法分配给 I/O 线程处理;
    2. 同时主线程也会自己负责处理一个客户端连接的读事件;
    3. 当主线程处理完该连接的读事件后,会自旋等待所有 I/O 线程处理完毕
  3. 命令执行:主线程按照事件被加入全局等待读队列的顺序(这边保证了执行顺序是正确的),串行执行客户端命令,然后将客户端连接放到全局等待写队列
  4. 写回结果:跟等待读队列处理类似,主线程将等待写队列的客户端连接使用轮询调度算法分配给 I/O 线程处理,同时自己也会处理一个,当主线程处理完毕后,会自旋等待所有 I/O 线程处理完毕,最后清空队列。

redis6.0工作流程图.jpg

为什么redis是单线程

完全内存操作,CPU不会成为瓶颈,瓶颈多为内存和网络带宽。 最近的 6.0 版本就对核心流程引入了多线程,主要用于解决 redis 在网络 I/O 上的性能瓶颈。而对于核心的命令执行阶段,目前还是单线程的。

Redis 为什么使用单进程、单线程也很快

  1. 基于内存的操作
  2. 使用了 I/O 多路复用模型,select、epoll 等,基于 reactor 模式开发了自己的网络事件处理器
  3. 单线程可以避免不必要的上下文切换和竞争条件,减少了这方面的性能消耗。
  4. 对数据结构进行了优化,简单动态字符串、压缩列表、跳表等。

redis的数据结构

基础的5种:

  • String:字符串,最基础的数据类型。

  • List:列表。

  • Hash:哈希对象。

  • Set:集合。

  • Sorted Set:有序集合,Set 的基础上加了个分值。 高级的4种:

  • HyperLogLog:通常用于基数统计。使用少量固定大小的内存,来统计集合中唯一元素的数量。统计结果不是精确值,而是一个带有0.81%标准差(standard error)的近似值。所以,HyperLogLog适用于一些对于统计结果精确度要求不是特别高的场景,例如网站的UV统计。

  • Geo:redis 3.2 版本的新特性。可以将用户给定的地理位置信息储存起来, 并对这些信息进行操作:获取2个位置的距离、根据给定地理位置坐标获取指定范围内的地理位置集合。

  • Bitmap:位图。

    • 用户状态统计;key为天数,用户ID为offset
    • 布隆过滤器
  • Stream:主要用于消息队列,类似于 kafka,可以认为是 pub/sub 的改进版。提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

有序集合底层实现方式

  1. zipList:压缩列表
  2. skipList:跳表

ziplist:使用压缩列表实现,当保存的元素长度都小于64字节,同时数量小于128时,使用该编码方式,否则会使用 skiplist。这两个参数可以通过 zset-max-ziplist-entries、zset-max-ziplist-value 来自定义修改。

skiplist:zset实现,一个zset同时包含一个字典(dict)和一个跳跃表(zskiplist)

跳表为啥不用红黑树

  1. 两者时间复杂度相等,查询和维护都是 O(nlogn)
  2. 红黑树实现代码复杂

hash底层存储数据结构

  1. zipList:压缩列表
  2. hash表:(不如java的hashmap)

Hash 对象的扩容流程

hash 对象在扩容时使用了一种叫“渐进式 rehash”的方式,步骤如下:

  1. 计算新表的size,并分配空间

  2. 将 rehash 索引计数器变量 rehashidx 的值设置为0,表示 rehash 正式开始。

  3. 在 rehash 进行期间,每次对字典执行添加、删除、査找、更新操作时,程序除了执行指定的操作以外,还会触发额外的 rehash 操作,在源码中的 _dictRehashStep 方法。 _dictRehashStep:从名字也可以看出来,大意是 rehash 一步,也就是 rehash 一个索引位置。 该方法会从 ht[0] 表的 rehashidx 索引位置上开始向后查找,找到第一个不为空的索引位置,将该索引位置的所有节点 rehash 到 ht[1],当本次 rehash 工作完成之后,将 ht[0] 索引位置为 rehashidx 的节点清空,同时将 rehashidx 属性的值加一。

  4. 将 rehash 分摊到每个操作上确实是非常妙的方式,但是万一此时服务器比较空闲,一直没有什么操作,难道 redis 要一直持有两个哈希表吗? 答案当然不是的。我们知道,redis 除了文件事件外,还有时间事件,redis 会定期触发时间事件,这些时间事件用于执行一些后台操作,其中就包含 rehash 操作:当 redis 发现有字典正在进行 rehash 操作时,会花费1毫秒的时间,一起帮忙进行 rehash。

  5. 随着操作的不断执行,最终在某个时间点上,ht[0] 的所有键值对都会被 rehash 至 ht[1],此时 rehash 流程完成,会执行最后的清理工作:释放 ht[0] 的空间、将 ht[0] 指向 ht[1]、重置 ht[1]、重置 rehashidx 的值为 -1。

rehash在数据量大的情况下有啥不好的地方

  1. 扩容期开始时,会先给 ht[1] 申请空间,所以在整个扩容期间,会同时存在 ht[0] 和 ht[1],会占用额外的空间。

  2. 扩容期间同时存在 ht[0] 和 ht[1],查找、删除、更新等操作有概率需要操作两张表,耗时会增加。

  3. redis 在内存使用接近 maxmemory 并且有设置驱逐策略的情况下,出现 rehash 会使得内存占用超过 maxmemory,触发驱逐淘汰操作,导致 master/slave 均有有大量的 key 被驱逐淘汰,从而出现 master/slave 主从不一致。

Redis 的网络事件处理器(Reactor 模式)

Redis 删除过期键的策略(缓存失效策略、数据过期策略)

  1. 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。对内存最友好,对 CPU 时间最不友好。

  2. 惰性删除:放任键过期不管,但是每次获取键时,都检査键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。对 CPU 时间最优化,对内存最不友好。

  3. 定期删除:每隔一段时间,默认100ms,程序就对数据库进行一次检査,删除里面的过期键。至 于要删除多少过期键,以及要检査多少个数据库,则由算法决定。前两种策略的折中,对 CPU 时间和内存的友好程度较平衡。

Redis 使用惰性删除和定期删除。

Redis 的内存淘汰(驱逐)策略

Redis 的 LRU 算法怎么实现的?

Redis 的持久化机制有哪几种,各自的实现原理和优缺点?

Redis 持久化我该怎么选择

一般来说, 如果想尽量保证数据安全性, 你应该同时使用 RDB 和 AOF 持久化功能,同时可以开启混合持久化。

如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。

如果你的数据是可以丢失的,则可以关闭持久化功能,在这种情况下,Redis 的性能是最高的。

使用 Redis 通常都是为了提升性能,而如果为了不丢失数据而将 appendfsync 设置为 always 级别时,对 Redis 的性能影响是很大的,在这种不能接受数据丢失的场景,其实可以考虑直接选择 MySQL 等类似的数据库。

redis如何保证高可用

  1. 主从
  2. 哨兵
  3. 集群模式

主从模式

哨兵模式

集群模式

Redis 里面有1亿个 key,其中有 10 个 key 是包含 java,如何将它们全部找出来?

  1. keys java 命令,该命令性能很好,但是在数据量特别大的时候会有性能问题
  2. scan 0 MATCH java

scan 实现原理

Redis 和 Memcached 的比较

redis实现分布式锁以及redlock

使用缓存时是先写缓存还是先写数据库

两者都有脏数据的情况,看那种脏数据多了

更新缓存还是让缓存失效?

让缓存失效,原因:失效操作是幂等的

如何保证数据库和缓存的数据一致性

由于数据库和缓存是两个不同的数据源,要保证其数据一致性,其实就是典型的分布式事务场景,可以引入分布式事务来解决,常见的有:2PC、TCC、MQ事务消息等。

所以在实际使用中,通常不会去保证缓存和数据库的强一致性,而是做出一定的牺牲,保证两者数据的最终一致性。

如果是实在无法接受脏数据的场景,则比较合理的方式是放弃使用缓存,直接走数据库。

1)更新数据库,数据库产生 binlog。

2)监听和消费 binlog,执行失效缓存操作。

3)如果步骤2失效缓存失败,则引入重试机制,将失败的数据通过MQ方式进行重试,同时考虑是否需要引入幂等机制。

image.png

兜底:当出现未知的问题时,及时告警通知,人为介入处理。

人为介入是终极大法,那些外表看着光鲜艳丽的应用,其背后大多有一群苦逼的程序员,在不断的修复各种脏数据和bug。

缓存穿透描述以及解决方案

描述:访问一个缓存和数据库都不存在的 key,此时会直接打到数据库上,并且查不到数据,没法写缓存,所以下一次同样会打到数据库上。此时,缓存起不到作用,请求每次都会走到数据库,流量大时数据库可能会被打挂。此时缓存就好像被“穿透”了一样,起不到任何作用。

解决方案:

  1. 接口校验。在正常业务流程中可能会存在少量访问不存在 key 的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如商品查询中,商品的ID是正整数,则可以直接对非正整数直接过滤等等。

  2. 缓存空值。当访问缓存和DB都没有查询到值时,可以将空值写进缓存,但是设置较短的过期时间,该时间需要根据产品业务特性来设置。

  3. 布隆过滤器。使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库。

布隆过滤器实现方案

缓存击穿描述以及解决方案

描述: 某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。

解决方案:

  1. 互斥锁;本地/分布式锁
  2. 热点数据不过期。直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。

缓存雪崩描述以及解决方案

描述:大量的热点 key 设置了相同的过期时间,导在缓存在同一时刻全部失效,造成瞬时数据库请求量大、压力骤增,引起雪崩,甚至导致数据库被打挂。

解决方案:

  1. 过期时间打散。既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。

  2. 热点数据不过期。该方式和缓存击穿一样,也是要着重考虑刷新的时间间隔和数据异常如何处理的情况。

  3. 加互斥锁。该方式和缓存击穿一样,按 key 维度加锁,对于同一个 key,只允许一个线程去计算,其他线程原地阻塞等待第一个线程的计算结果,然后直接走缓存即可。

redis的事件有哪些

  1. 文件事件
    1. 1
  2. 时间事件 2. 2

参考

redis 2021 最全面试题