redis相关面试题

发布于:2025-06-23 ⋅ 阅读:(13) ⋅ 点赞:(0)

1.缓存穿透(Cache Penetration)

  • 问题描述: 查询一个数据库中根本不存在的数据。由于缓存中不会有该数据(未命中),导致每次请求都直接访问数据库,给数据库造成巨大压力,甚至压垮数据库。

  • 原因:

    • 恶意攻击:攻击者故意构造大量不存在的ID进行查询。

    • 业务逻辑错误:程序BUG导致产生大量无效请求。

  • 解决方案:

    • 缓存空对象(Null Caching / Bloom Filter Pre-caching): 即使数据库查询为空,也将这个空结果(比如 null)进行短暂的缓存(设置一个较短的过期时间,如 1-5 分钟)。这样后续相同的无效请求在缓存过期前会命中空对象,保护数据库。

      • 优点: 实现简单。

      • 缺点: 1) 消耗额外的内存存储大量空值。2) 存在短期的数据不一致(如果这个key在数据库里被创建了,在空对象过期前可能查不到)

    • 布隆过滤器(Bloom Filter): 在访问缓存之前,先经过一个布隆过滤器。

      • 原理: 布隆过滤器是一个概率型数据结构,可以高效地判断一个元素一定不存在可能存在于某个集合中。

      • 工作流程:

        1. 将所有可能存在的有效键(或键的哈希值)预先加载到布隆过滤器中。

        2. 请求到来时,先用布隆过滤器判断请求的Key:

          • 如果布隆过滤器说不存在 -> 则直接返回空或错误,无需查询缓存和数据库(拦截无效请求)。

          • 如果布隆过滤器说可能存在 -> 则继续走正常的缓存查询逻辑(查询缓存,未命中则查数据库)。

      • 优点: 内存占用远小于缓存空对象,能有效拦截绝大部分无效请求。

      • 缺点: 1) 存在一定的误判率(False Positive - 判断为可能存在,但实际上不存在,这种情况仍然会走到数据库查询,但概率可控)。2) 删除数据困难(需要维护布隆过滤器的更新,通常结合空对象缓存使用)。3) 需要预加载有效键集合。

    • 接口层校验: 对请求参数进行基础校验(如ID范围、格式校验),拦截明显无效的请求。

 2.缓存击穿(Cache Breakdown)

  • 问题描述: 某个热点数据Key在缓存中恰好过期的瞬间,有大量的并发请求同时涌来。这些请求发现缓存失效,都会去后端数据库加载数据,导致数据库瞬间压力剧增甚至崩溃。

  • 原因: 热点Key + 缓存同时失效 + 高并发。

  • 与穿透的区别: 击穿针对的是存在但暂时失效的热点数据;穿透是针对根本不存在的数据。

  • 解决方案:

    • 设置热点数据永不过期: 对于一些极热点的数据,可以设置永不过期(或物理上不设置过期时间)。通过逻辑过期(在Value中存储一个过期时间字段)或后台异步更新策略来保证数据的更新。

      • 工作流程:

        1. 缓存永不过期,Value中包含逻辑过期时间 expireTime

        2. 查询时,先返回缓存数据。

        3. 程序判断 expireTime 是否已过:

          • 未过:直接返回数据。

          • 已过:触发异步线程去更新缓存,当前请求仍返回旧数据(或稍作等待)。

      • 优点: 避免缓存同时失效。

      • 缺点: 需要额外字段和异步更新逻辑,可能返回短暂旧数据。

    • 互斥锁(Mutex Lock):

      • 工作流程:

        1. 当缓存失效时,不是所有线程都去查数据库。

        2. 第一个发现失效的线程尝试获取一个分布式锁(例如使用Redis的 SET key value NX PX milliseconds 命令)。

        3. 获取锁成功的线程负责查询数据库并更新缓存。

        4. 其他线程等待(轮询或订阅通知)或者短暂休眠后重试读取缓存。

      • 优点: 保证只有一个线程去查数据库,保护数据库。

      • 缺点: 1) 未获取锁的线程需要等待,增加延迟。2) 分布式锁的实现复杂度。3) 锁失效时间设置不当可能导致死锁或重复查询。

    • 缓存预热(Cache Warm-up): 在系统启动或低峰期,提前将热点数据加载到缓存中,尽量避免在高峰期出现缓存失效。

 3.缓存雪崩(Cache Avalanche)

  • 问题描述: 在某一时刻,大量的缓存Key同时失效(或Redis服务不可用),导致所有原本应该命中缓存的请求都涌向后端数据库,造成数据库压力骤增甚至崩溃。

  • 原因:

    • 大量Key设置了相同的过期时间(例如系统初始化时批量加载数据,设置了相同的TTL)。

    • Redis服务宕机集群故障

  • 与击穿的区别: 雪崩是大量Key同时失效;击穿是单个热点Key失效。

  • 解决方案:

    • 设置不同的过期时间: 这是最常用且有效的方法。给缓存数据的过期时间加上一个随机值(例如基础过期时间 + 随机几分钟)。确保数据不会在同一时间大面积失效,而是分散失效。

    • 构建高可用缓存集群:

      • Redis Sentinel(哨兵): 实现主从切换和故障转移,提高可用性。

      • Redis Cluster(集群): 提供数据分片(Sharding)和高可用,即使部分节点宕机,集群整体仍能提供服务(部分数据可能暂时不可用)。

    • 服务熔断与降级:

      • 熔断(Circuit Breaker): 当检测到数据库访问失败率高或响应过慢时,暂时“熔断”对数据库的访问,直接返回预设的默认值(或错误信息),给数据库恢复的时间。

      • 降级(Degradation): 在系统压力过大时,暂时关闭一些非核心功能或返回简化数据,优先保证核心功能的可用性(可能核心功能也会用到缓存,但压力会小些)。

    • 多级缓存: 在应用层(如本地缓存Guava Cache/Caffeine)和分布式缓存(Redis)之间再加一层缓存。即使Redis挂了,本地缓存还能抵挡一部分流量,为恢复争取时间。需要注意本地缓存的一致性管理。

    • 缓存永不过期 + 后台更新: 类似击穿的解决方案,对关键数据使用。

 4.内存淘汰策略决策

策略 作用范围 淘汰依据 适用场景
allkeys-lru 所有Key 最近最少使用 通用缓存(保留热点数据)
volatile-lru 带过期时间的Key 最近最少使用 区分永久/临时数据
allkeys-lfu 所有Key 访问频率最低 防扫描访问导致缓存污染
volatile-ttl 带过期时间的Key TTL最短 优先清理即将过期数据
noeviction - 拒绝写入 数据绝对不可丢失场景

5. 缓存的数据结构

常用结构及典型场景:

  • String:

    • 场景:简单KV缓存(用户信息序列化JSON存储)、计数器(INCR/DECR)、分布式锁(SETNX)、位操作(签到统计)。

  • Hash:

    • 场景:存储对象(如用户信息,每个field对应一个属性)。适合需要部分修改HSET单个field)或部分读取HGET单个field)的场景。比String序列化整个对象更节省网络流量和内存(Redis内部优化)。

  • List:

    • 场景:消息队列(LPUSH/RPOPBRPOP阻塞版本)、最新消息/动态列表(LPUSH+LTRIM保持固定长度)、栈(LPUSH/LPOP)。

  • Set:

    • 场景:无序集合(标签Tag、用户关注列表)、去重(UV统计初步去重)、集合运算(交集SINTER/并集SUNION/差集SDIFF - 共同好友、推荐)。

  • Sorted Set (ZSet):

    • 场景:排行榜(ZADD+ZREVRANGE)、带权重的队列(延迟队列 - 时间戳作为score)、范围查找(按分数区间ZRANGEBYSCORE)。

6.Redis为什么快?

  • 内存操作: 数据存储在内存,读写速度极快。

  • 单线程模型:

    • 避免多线程上下文切换和锁竞争开销。

    • I/O多路复用 (epoll/kqueue): 单线程高效处理大量并发连接。

  • 高效的数据结构: 精心设计的底层数据结构(SDS, HashTable, SkipList, ZipList等)。

 7.缓存与数据库的数据一致性

问题本质: 如何保证缓存中的数据和数据库中的数据在更新操作后保持一致

  • 延迟双删(Double Delete): (Cache-Aside的增强)

    1. 更新数据库。

    2. 删除缓存。

    3. 等待一小段时间 (比如几百毫秒,根据业务主从延迟估算)。

    4. 再次删除缓存。

    • 目的:清除在步骤1-2期间可能被其他读请求写入缓存的旧数据。

    • 步骤 操作 目的 注意事项
      1 更新数据库 确保数据持久化 更新主库
      2 首次删除缓存 清除旧缓存 立即执行
      3 等待一段时间 等待主从同步完成 时间需根据业务主从延迟设置
      4 再次删除缓存 清除期间可能写入的旧数据 确保最终一致性
  • 为什么需要二次删除:避免脏读回填

  • 脏读回填问题图示

  • 订阅数据库变更日志(Binlog): (如Canal, Debezium)

    • 通过监听数据库的变更日志(如MySQL的Binlog),将变更事件发布到消息队列(如Kafka)。

    • 独立的消费者程序消费消息,更新或删除Redis中的缓存。

    • 优点: 解耦应用,通用性强,能保证最终一致性。

    • 缺点: 架构复杂,引入新组件,延迟比Cache-Aside高。

  • 关键点:

    • 强一致性(CP)代价很高,通常选择最终一致性(AP)。

    • 选择删除缓存而不是更新缓存,能有效避免复杂的并发更新时序问题(如两个并发写操作更新DB和缓存的顺序错乱导致脏数据)。

    • 在高并发写场景下,任何策略都可能存在短暂不一致,需要业务容忍度或结合其他机制。

 


网站公告

今日签到

点亮在社区的每一天
去签到