头图

前不久,有位朋友去腾讯面试,他说被问到了很多关于 Redis 的问题,比如为什么用 Redis 作为 MySQL 的缓存?Redis 中大量 key 集中过期怎么办?如何保证缓存和数据库数据的一致性?我将它们整理出来,跟大家一起来探讨如何回答这些问题,希望对大家有所帮助。

Redis 为什么这么快?

为什么用 Redis 作为 MySQL 的缓存?

Redis 除了做缓存,还能做什么?

使用 redis 分布式锁,如何合理设置过期时间?

Redis 单线程模型了解吗?

Redis 中大量 key 集中过期怎么办?

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

Redis 为什么这么快?

Redis 内部做了非常多的性能优化,比较重要的有下面几点:

  1. Redis 基于内存,内存的访问速度比磁盘快很多;
  2. Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环IO 多路复用
  3. Redis 内置了多种优化过后的数据类型/结构实现,性能非常高。
  4. Redis 通信协议实现简单且解析高效。

扩展:那既然都这么快了,为什么不直接用 Redis 当主数据库呢?

  • 主要是因为内存成本太高且 Redis 提供的数据持久化仍然有数据丢失的风险。

为什么用 Redis 作为 MySQL 的缓存?

主要是因为 Redis 具备高性能高并发两种特性。下面来详细介绍一个高性能和高并发。(这个问题是个开放题,我的答案仅供参考)

高性能

假如用户第一次访问 MySQL 中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据缓存在 Redis 中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了,操作 Redis 缓存就是直接操作内存,所以速度相当快。

如果 MySQL 中的对应数据改变的之后,同步改变 Redis 缓存中相应的数据即可。

高并发

单台设备的 Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。

所以,直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

Redis 除了做缓存,还能做什么?

  1. 分布式锁:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。
  2. 限流:一般是通过 Redis + Lua 脚本的方式来实现限流。如果不想自己写 Lua 脚本的话,也可以直接利用 Redisson 中的 RRateLimiter 来实现分布式限流,其底层实现就是基于 Lua 代码+令牌桶算法。
  3. 消息队列:Redis 自带的 List 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。
  4. 延时队列:Redisson 内置了延时队列(基于 Sorted Set 实现的)。
  5. 分布式 Session:利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。
  6. 复杂业务场景:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜。

使用 redis 分布式锁,如何合理设置过期时间?

Redis 单线程模型了解吗?

Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型 ,这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。

由于文件事件处理器是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。

<u>面试官又问了一个小问题,感觉回答的不错</u>

既然是单线程,那怎么监听大量的客户端连接呢?

Redis 通过 IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。

I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗,这样使用的好处是非常明显的。

使用 redis 分布式锁,如何合理设置过期时间?

需要考虑如下几个因素:

  1. 任务执行时间:确保过期时间大于预期的最长执行时间,以免任务还在执行过程中锁就被自动释放,导致并发问题
  2. 锁自动续期:如果使用了支持锁自动续期的 Redis 客户端库(如 Redisson),在持有锁的线程还在执行任务期间,可以定期自动延长锁的有效期,这样可以减小因锁过期导致的并发问题。
  3. 锁竞争激烈程度:如果锁的竞争非常激烈,过期时间不宜设置得太短,否则可能会频繁触发锁的竞争,消耗更多资源。反之,如果锁的竞争不大,可以适当缩短过期时间,更快地回收锁资源。
  4. 死锁检测与处理:设定一个合理的最大等待时间,超过这个时间还没有释放的锁可以被认为是持有锁的客户端出现问题,可以通过监控和相应的逻辑来处理此类死锁。
  5. 网络延迟和异常恢复:考虑到网络不稳定等因素,过期时间还应该预留一部分用于处理网络延迟或客户端异常恢复的情况。过期时间太短可能导致客户端未能及时释放锁或重新获取锁。
  6. 锁释放的可靠性:使用 lua 脚本来保证解锁操作的原子性,同时结合 watch 命令或事务处理,以最大程度地确保锁在业务逻辑完成后能够正确释放,降低对过期时间依赖的程度。

Redis 中大量 key 集中过期怎么办?

首先回答 大量 key 集中过期可能出现的问题:

  • 请求延迟增加: Redis 在处理过期 key 时需要消耗 CPU 资源,如果过期 key 数量庞大,会导致 Redis 实例的 CPU 占用率升高,进而影响其他请求的处理速度,造成延迟增加。
  • 内存占用过高: 过期的 key 虽然已经失效,但在 Redis 真正删除它们之前,仍然会占用内存空间。如果过期 key 没有及时清理,可能会导致内存占用过高,甚至引发内存溢出。

之后再回答 可以采取的方案:

  • 1.尽量避免 key 集中过期: 在设置键的过期时间时尽量随机一点。
  • 2.开启 lazy free 机制: 修改 redis.conf 配置文件,将 lazyfree-lazy-expire 参数设置为 yes,即可开启 lazy free 机制。开启 lazy free 机制后,Redis 会在后台异步删除过期的 key,不会阻塞主线程的运行,从而降低对 Redis 性能的影响。

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

其实感觉聊聊 Cache Aside 这个策略就可以了,细说的话没啥太大必要。

下面来说说 Cache Aside 策略:

Cache Aside 中遇到写请求是这样的,更新数据库,然后直接删除缓存。

但是必须是这两步都成功,才能解决缓存和数据库数据不一致的问题。

关于更新数据库成功,而删除缓存这一步失败的这种情况,是可能发生的,简单说有两个解决方案:

  • 缓存失效时间变短(不推荐,治标不治本):我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。用户会反馈在一段时间后,才能更新数据哦!!!
  • 增加缓存更新重试机制(常用):如果缓存服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。不过,这里更适合引入消息队列实现异步重试,将删除缓存重试的消息投递到消息队列,然后由专门的消费者来重试,直到成功。

恭喜你,面试通过!!!

就业陪跑训练营学员投稿

欢迎关注 ❤

我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。

没准能让你能刷到自己意向公司的最新面试题呢。

感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。


王中阳讲编程
819 声望300 粉丝