聊聊Redis面试题

125 阅读8分钟

大家好,许久不见,我是Leo。

最近抽了一部分时间在BOOS上解答了一些面试题,点赞量和Get 数挺多的。这篇文章就聊一些Redis的一些面试题。

什么是Redis的缓存预热?

缓存预热主要是双十一流量比较大的业务场景。为了提升用户的访问性能,减少一开始缺少缓存Key到数据库查询数据的慢性能问题。一般会采用系统上线前把热点缓存预热到redis中。这样用户就不用一开始去数据库查了,直接Redis内存返给你了。

Redis单线程性能为什么优于多线程?

一个CPU在运行多个线程时,会存在多线程调用的消耗问题,而且还有多个线程调用时数据一致性的问题。这些都要单独处理,单独处理又会消耗性能。于是Redis统筹兼顾采用了单,多线程并用的思路。

在处理数据写入,读取属于键值对数据操作,采用单线程操作。在请求连接,从socket中读取请求,解析客户端发送请求,采用多线程操作

Redis巧妙的把所有需要延迟等待的操作全部转交给了多线程处理,在不需要等待的全部单线程处理。个人感觉这种设计思路很棒

多路复用机制

IO多路复用机制是指一个线程处理多个IO流,也是我们经常听到的select/epoll机制。那么那些连接,等待的操作Redis都是如何处理的呢?

在Redis只运行单线程的情况下,同一时间存在多个监听套接字,和已连接的套接字,内核会一直监听这些连接请求和数据请求。一旦客户端发送请求就会以事件的方式通知Redis主线程处理。这就是Redis线程处理多个IO流的效果。

上文说到以事件方式通知Redis这里我们做一个扩展,select/epoll提供了基于事件的回调机制,不同的事件会调用相应的处理函数。一旦请求来了,立刻加到事件队列中,Redis单线程就会源源不断的处理该事件队列。解决了等待与扫描的资源浪费问题。

Redis中如何解决缓存的一致性问题

很多人一上来就是加锁,这样的回答显然不是面试官想看到的,我们可以搭一个知识树。比如可以这样回答。

Redis缓存分为两种 读写缓存只读缓存 。不同的缓存类型会引发不同的问题。

对于读写缓存来说,读压力和写压力都来源于一个库,如果对数据发生了修改。我们除了要考虑数据库的一致性以外,还要考虑缓存中的数据一致性。

这里可以展开聊一下写压力的写回策略

  • 当采用同步直写策略,写缓存时,可以保证缓存与数据库的数据一致性。
  • 当采用异步写回策略,写缓存时,由于是异步执行,无法保证命令都执行也就是无法保证数据一致性。

上述策略如何选择主要看业务的需求了,如果核心数据,一定不能错的数据,我们可以同步直写再搭配事务机制。如果不是特别重要的数据,那我们就可以采用异步写回策略来提升性能。

对于只读缓存来说,会有一个子库,专门提供读服务,但是如果有新数据进来,子库必须跟着主库进行数据同步,来保证主从库的数据一致性。

这里可以展开聊一下 Redis,MySQL的原子性是怎么保证的

  • 如果先删缓存,后更新数据库:缓存删除成功,数据更新失败,导致用户向缓存读数据时,没有发现缓存key就会打向数据库,而数据库数据没有更新成功导致读到旧值
  • 如果先更新数据库,后删缓存: 更新数据库成功时,缓存删除失败,就会导致数据库保留了最新的值,用户向Redis读数据时,发现缓存key存在,直接返回了就拿到了上一个旧值。

会发现上述都不能保证,这里为什么我还要提呢?告诉面试官你是经过很多思考的,别搞的跟背答案似的。

解决方案

可以采用重试机制,比如用Redis的List当做消息队列,或者采用MQ消息队列来处理。

不管是 先删缓存,后更新数据库 还是 先更新数据库后删缓存 一旦失败就会出现两边不一致的情况,那我们就可以采用MQ消费机制,只有当两种情况全部成功,才把MQ这条数据删掉,要不然就重复消费。也就是重新再执行一遍。

这里扩展一下,我们在执行重复消费的那一刻,如果有数据打过来怎么办?

所以我们在解决时,一般会给他sleep一小段时间再进行缓存操作。这样就会避免这种那一刻的误差。

Redis为什么要设置随机过期时间

Redis设置的过期时间问题,如果很多key在同一时刻大面积过期,会影响Redis的性能。

Redis是使用单线程读写数据的,大面积过期会阻塞Redis的处理效率。所以,在应用Redis的key时要避免两种情况,bigkey大面积过期key

对于第二种情况,我们只要设置一个随机过期时间就不会存在大面积过期啦。

这里还可以跟面试官聊一下Redis的过期删除策略和过期的删除机制。因为上文的删除key的依据是来源于过期策略和删除机制的。

过期删除机制

过期删除机制是Redis用来回收内存空间的常用机制,可以对键值对设置过期时间,默认情况下,Redis每100毫秒就会删除一些过期的key,具体算法如下:

  • 采样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 个数的 key,并将其中过期的 key 全部删除;
  • 如果超过 25% 的 key 过期了,则重复删除的过程,直到过期 key 的比例降至 25% 以下。

ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 默认为20。毫秒的概念可能不太明显,我们换算一下也就是每秒删除200个过期key(20个/100毫秒)=(200个/秒)

如果按照第一种的话,并不是造成太大的影响。如果命中了第二种,就会造成key大面积失效过期,过期的key超过了25%,会一直删除直至降至25%。这段删除期间会大量释放内存空间,大量插入链表填补。Redis就变慢了。

过期删除策略

这个应该是比较简单的

  • 定时删除:采用定时器方式,时间一到就删
  • 惰性删除:惰性删除并不是当到达过期时间去删除,而是每次获取键值时,会判断是否过期,如果过期就删,并返回空。没过期就返回键值
  • 定期删除:每隔一段时间,就对数据库中的键进行检查,如果过期则删除。至于要删除多少什么时候删除则是通过具体程序绝对的

定时删除策略

优点是:对内存友好。因为通过定时器,当一个键到达过期时间时就会立马被删除,直接就释放了内存。

缺点是:对 CPU 不友好。因为如果过期键比较多,那么删除这些过期键会占用相当一部分 CPU 时间,如果 CPU 时间非常紧张的话,还将 CPU 时间用在删除和当前任务无关的过期键上,会对服务器的响应时间以及吞吐量造成影响。

因此,通过 定时删除 策略来时间过期键的删除不太现实。

惰性删除

优点:对 CPU 时间友好。程序只会在取出键时才会判断是否删除,并且只作用到当前键上,其他过期键不会花费 CPU 时间去处理。

缺点:对内存不友好。因为只有键被使用时才会去检查是否删除,如果有大量的键一直不被使用,那么这些键就算过期了也不会被删除,会一直占用着内存。这种可以理解为是一种内存泄漏——大量无用的数据一直占用着内存,并且不会被删除。

定期删除

相比较定时删除对 CPU 的不友好,惰性删除的对内存不友好。定期删除采用了一种折中的方式:

定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。并且,通过定期删除过期键,有效的减少了过期键带来的内存浪费。但删除的时长和频率比较难定义,

因为:如果频率太高或者时长太长,那么会占用大量的 CPU 时长。如果过短又会出现内存浪费的情况。

因此。如果采用定期删除策略的话需要通过具体的业务场景来定义时长和频率。

\