跳至主要內容
Redis 面试之应用篇

Redis 面试之应用篇

钝悟...大约 10 分钟数据库KV 数据库redis数据库KV 数据库redis面试

Redis 面试之应用篇

缓存

【中级】如何避免缓存雪崩、缓存击穿、缓存穿透?

要点
  • 缓存击穿:指某个热点数据在缓存中失效,导致大量请求直接访问数据库。此时,由于瞬间的高并发,可能导致数据库崩溃。
  • 缓存穿透:指查询一个不存在的数据,缓存中没有相应的记录,每次请求都会去数据库查询,造成数据库负担加重。
  • 缓存雪崩:指多个缓存数据在同一时间过期,导致大量请求同时访问数据库,从而造成数据库瞬间负载激增。

解决方案

缓存击穿:

  • 使用互斥锁,确保同一时间只有一个请求可以去数据库查询并更新缓存。
  • 热点数据永不过期。

缓存穿透:

  • 使用布隆过滤器,过滤掉不存在的请求,避免直接访问数据库。
  • 对查询结果进行缓存,即使是不存在的数据,也可以缓存一个标识,以减少对数据库的请求。

缓存雪崩:

  • 采用随机过期时间策略,避免多个数据同时过期。
  • 使用双缓存策略,将数据同时存储在两层缓存中,减少数据库直接请求。

【中级】如何保证缓存与数据库的数据一致性?

要点

一般来说,系统如果不是严格要求缓存和数据库保持一致性的话,尽量不要将读请求和写请求串行化。串行化可以保证一定不会出现数据不一致的情况,但是它会导致系统的吞吐量大幅度下降。

一般来说缓存的更新有两种情况:

  • 先删除缓存,再更新数据库。
  • 先更新数据库,再删除缓存。

为什么是删除缓存,而不是更新缓存呢?

你可以想想当有多个并发的请求更新数据,你并不能保证更新数据库的顺序和更新缓存的顺序一致,那就会出现数据库中和缓存中数据不一致的情况。所以一般来说考虑删除缓存。

【中级】有哪些常见的内存淘汰算法?

要点

缓存淘汰的类型:

  • 基于空间 - 设置缓存空间大小。
  • 基于容量 - 设置缓存存储记录数。
  • 基于时间
    • TTL(Time To Live,即存活期)缓存数据从创建到过期的时间。
    • TTI(Time To Idle,即空闲期)缓存数据多久没被访问的时间。

缓存淘汰算法:

  • FIFO - 先进先出。在这种淘汰算法中,先进入缓存的会先被淘汰。这种可谓是最简单的了,但是会导致我们命中率很低。试想一下我们如果有个访问频率很高的数据是所有数据第一个访问的,而那些不是很高的是后面再访问的,那这样就会把我们的首个数据但是他的访问频率很高给挤出。
  • LRU - 最近最少使用算法。在这种算法中避免了上面的问题,每次访问数据都会将其放在我们的队尾,如果需要淘汰数据,就只需要淘汰队首即可。但是这个依然有个问题,如果有个数据在 1 个小时的前 59 分钟访问了 1 万次(可见这是个热点数据),再后一分钟没有访问这个数据,但是有其他的数据访问,就导致了我们这个热点数据被淘汰。
  • LFU - 最近最少频率使用。在这种算法中又对上面进行了优化,利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰。这样就避免了 LRU 不能处理时间段的问题。

这三种缓存淘汰算法,实现复杂度一个比一个高,同样的命中率也是一个比一个好。而我们一般来说选择的方案居中即可,即实现成本不是太高,而命中率也还行的 LRU。

分布式锁

【中级】Redis 中如何实现分布式锁?

要点

基础实现

  • 加锁SET key val NX PX 30000NX:仅当 key 不存在时写入,PX:超时时间,毫秒)。
  • 解锁DEL key(直接删除 key 释放锁)。

避免死锁

  • 问题:节点宕机或异常导致锁无法释放。
  • 方案:加锁时设置超时时间PX/EX),到期自动删除。
  • 原子性:使用 SET key val NX PX 替代 setnx + expire,避免组合命令非原子性问题。

超时续期(WatchDog)

  • 问题:业务未执行完,锁已过期。
  • 方案:启动定时任务检测锁剩余时间,自动续期(如 Redisson 的 WatchDog 机制)。

安全解锁

  • 问题:其他节点误删锁。
  • 方案
    • 加锁时写入唯一标识(如 UUID)。
    • 解锁时校验标识,匹配才删除(使用 Lua 脚本保证原子性):
      if redis.call("get", KEYS[1]) == ARGV[1] then
          return redis.call("del", KEYS[1])
      else
          return 0
      end
      

自旋重试

  • 问题:网络波动导致加锁失败。
  • 方案:在超时时间内循环重试加锁(伪代码示例见原内容)。

其他问题

  • 不可重入:同一线程无法重复获取同一把锁(可通过 Redisson 等客户端支持)。
  • 单点问题:主从切换可能导致锁失效(可用 RedLock 算法,但存在争议)。

【中级】Redis 的 Red Lock 是什么?你了解吗?

要点

核心机制

  • 多节点写入:同时向多个Redis主节点(≥5个)加锁
  • 多数派原则:半数以上节点加锁成功才算成功
  • 耗时控制:总加锁时间必须小于锁的过期时间
  • 强制清理:解锁时向所有节点发起请求

注意事项

  • 网络延迟:多节点操作会增加耗时风险
  • 性能代价:相比单节点锁性能更低
  • 实现复杂度:需要精确控制时间和节点状态

【中级】Redis 实现分布式锁时可能遇到的问题有哪些?

【中级】说说 Redisson 分布式锁的原理?

【中级】Redisson 看门狗(watch dog)机制了解吗?

消息队列

【中级】Redis 如何实现消息队列?

要点

Redis 可以做消息队列吗?

Redis 有哪些实现消息队列的方式?

先说结论:可以是可以,但不建议使用 Redis 来做消息队列。和专业的消息队列相比,还是有很多欠缺的地方。

基于 List 实现消息队列

Redis 2.0 之前,如果想要使用 Redis 来做消息队列的话,只能通过 List 来实现。

通过 RPUSH/LPOP 或者 LPUSH/RPOP即可实现简易版消息队列:

# 生产者生产消息
> RPUSH myList msg1 msg2
(integer) 2
> RPUSH myList msg3
(integer) 3
# 消费者消费消息
> LPOP myList
"msg1"

不过,通过 RPUSH/LPOP 或者 LPUSH/RPOP这样的方式存在性能问题,我们需要不断轮询去调用 RPOPLPOP 来消费消息。当 List 为空时,大部分的轮询的请求都是无效请求,这种方式大量浪费了系统资源。

因此,Redis 还提供了 BLPOPBRPOP 这种阻塞式读取的命令(带 B-Blocking 的都是阻塞式),并且还支持一个超时参数。如果 List 为空,Redis 服务端不会立刻返回结果,它会等待 List 中有新数据后再返回或者是等待最多一个超时时间后返回空。如果将超时时间设置为 0 时,即可无限等待,直到弹出消息

# 超时时间为 10s
# 如果有数据立刻返回,否则最多等待 10 秒
> BRPOP myList 10
null

List 实现消息队列功能太简单,像消息确认机制等功能还需要我们自己实现,最要命的是没有广播机制,消息也只能被消费一次。

基于发布订阅功能实现消息队列

Redis 2.0 引入了发布订阅 (pub/sub) 功能,解决了 List 实现消息队列没有广播机制的问题。

Redis 发布订阅 (pub/sub) 功能

pub/sub 中引入了一个概念叫 channel(频道),发布订阅机制的实现就是基于这个 channel 来做的。

pub/sub 涉及发布者(Publisher)和订阅者(Subscriber,也叫消费者)两个角色:

  • 发布者通过 PUBLISH 投递消息给指定 channel。
  • 订阅者通过SUBSCRIBE订阅它关心的 channel。并且,订阅者可以订阅一个或者多个 channel。

pub/sub 既能单播又能广播,还支持 channel 的简单正则匹配。不过,消息丢失(客户端断开连接或者 Redis 宕机都会导致消息丢失)、消息堆积(发布者发布消息的时候不会管消费者的具体消费能力如何)等问题依然没有一个比较好的解决办法。

基于 Stream 实现消息队列

为此,Redis 5.0 新增加的一个数据结构 Stream 来做消息队列。Stream 支持:

  • 发布 / 订阅模式
  • 按照消费者组进行消费(借鉴了 Kafka 消费者组的概念)
  • 消息持久化( RDB 和 AOF)
  • ACK 机制(通过确认机制来告知已经成功处理了消息)
  • 阻塞式获取消息

Stream 的结构如下:

这是一个有序的消息链表,每个消息都有一个唯一的 ID 和对应的内容。ID 是一个时间戳和序列号的组合,用来保证消息的唯一性和递增性。内容是一个或多个键值对(类似 Hash 基本数据类型),用来存储消息的数据。

这里再对图中涉及到的一些概念,进行简单解释:

  • Consumer Group:消费者组用于组织和管理多个消费者。消费者组本身不处理消息,而是再将消息分发给消费者,由消费者进行真正的消费
  • last_delivered_id:标识消费者组当前消费位置的游标,消费者组中任意一个消费者读取了消息都会使 last_delivered_id 往前移动。
  • pending_ids:记录已经被客户端消费但没有 ack 的消息的 ID。

Stream 使用起来相对要麻烦一些,这里就不演示了。

总的来说,Stream 已经可以满足一个消息队列的基本要求了。不过,Stream 在实际使用中依然会有一些小问题不太好解决比如在 Redis 发生故障恢复后不能保证消息至少被消费一次。

综上,和专业的消息队列相比,使用 Redis 来实现消息队列还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。因此,我们通常建议不要使用 Redis 来做消息队列,你完全可以选择市面上比较成熟的一些消息队列比如 RocketMQ、Kafka。不过,如果你就是想要用 Redis 来做消息队列的话,那我建议你优先考虑 Stream,这是目前相对最优的 Redis 消息队列实现。

相关阅读:Redis 消息队列发展历程 - 阿里开发者 - 2022open in new window

延时任务

【中级】如何基于 Redis 实现延时任务?

要点

基于 Redis 实现延时任务的功能无非就下面两种方案:

  1. Redis 过期事件监听
  2. Redisson 内置的延时队列

Redis 过期事件监听的存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。

Redisson 内置的延时队列具备下面这些优势:

  1. 减少了丢消息的可能:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。当然了,你也可以使用扫描数据库的方法作为补偿机制。
  2. 消息不存在重复消费问题:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。

参考资料

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.7