type
status
date
slug
summary
tags
category
password

1、缓存失效问题(穿透、击穿、雪崩)

Redis 一般和其他数据库搭配使用,Redis 会把 MySQL 中经常被查询的数据缓存起来,用户可以直接访问 Redis 中的缓存数据,用来减轻后端数据库的压力。出于容错考虑,一般在在 Redis 中找不到的数据,就会去数据库里面查找。如果 Redis 缓存层失效,大量的访问直接到达数据库,对数据库造成压力过大,这就是缓存失效问题。一般有三种情况:
问题
问题原因
解决方案
缓存雪崩
缓存中大批量的 key 同时过期或者 Redis 故障宕机,导致大量并发访问直接到达数据库
1、设置随机过期时间,在基础值上加上一个随机值(如1-5分钟的随机数),这样可以避免大量 key 集体失效。 2、互斥锁:当缓存失效时,不是所有线程都去访问数据库。保证只有一个线程(如使用 setnx 或 Redisson 分布式锁)能获得锁,这个线程负责去数据库加载数据并重建缓存。其他未获得锁的线程则等待一段时间(如100ms)后重试。 3、服务熔断和限流。当检测到数据库压力过大时,在应用层使用 HystrixSentinel 等工具进行熔断降级,直接返回预定义的值(如“系统繁忙”)或默认值,保护数据库不被拖垮。 4、搭建 Redis 高可用集群。实现主从切换和故障转移,防止因单点故障导致整个缓存服务不可用。
缓存击穿
缓存中某些高并发的热点 key 过期,针对该 key 的大量并发访问直接到达数据库
1、永不过期策略:设置热点数据永远不过期。 2、多级缓存:设置本地缓存作为备份,当分布式缓存失效时,先检查本地缓存。 3、互斥锁:同缓存雪崩
缓存穿透
大量访问缓存和数据库中都不存在的数据,导致每次请求都绕过缓存直接访问数据库。 例如用户故意使用空值或者其他不存在的值进行频繁请求攻击数据库。
1、参数校验:请求入口对请求的合理性进行检查,过滤非法请求 2、缓存空对象:即使从数据库没查到,也把一个空值(如 null""#)写入缓存,并设置一个较短的过期时间(如1-5分钟)。后续同样的 key 请求过来时,缓存直接返回空值,而不会访问数据库。 3、布隆过滤器:布隆过滤器用来判断一个元素“一定不存在”或者“可能存在”于一个集合中。我们先把所有合法的 key 值写入布隆过滤器,当请求来临时,先通过布隆过滤器判断 key 是否存在。如果“不存在”,那么这个 key 一定不存在于数据库中,直接返回空,无需查询缓存和数据库;如果“存在”,那么这个 key 可能存在,再继续后续的缓存和数据库查询流程。
布隆过滤器是一种空间效率高概率型数据结构。它的核心作用是:用来判断一个元素“一定不存在”或者“可能存在”于一个集合中。将所有已注册的 ID 放入一个布隆过滤器(通常放在内存中)。当用户输入一个新 ID 时,通过布隆过滤器判断 ID 是否存在:
  • 如果布隆过滤器说“不存在”,那么这个ID肯定不存在。
  • 如果布隆过滤器说“存在”,那么它可能存在,但也可能是误判,需要走一次数据库进行最终确认。
三者的区别:
  • 缓存击穿指少量热点数据过期,缓存雪崩是大量不同的数据过期,两者都会导致大量访问直达数据库。
  • 缓存雪崩和缓存击穿都是访问数据库中存在,但是 Redis 中不存在的数据;而缓存穿透是访问 Redis 和数据库中都不存在的数据

2、缓存一致性问题

缓存一致性问题是指当系统中存在缓存层和数据库两层数据存储时,如何保证两者的数据一致性。引发缓存一致性问题的原因:
  • 数据多副本:同一份数据同时存在于缓存和数据库中。
  • 更新非原子操作:当有数据更新时,因为更新数据库和操作缓存两个动作不是原子操作,那们就需要考虑是先更新数据库还是先操作缓存?操作缓存的话是删除缓存还是更新缓存?这些操作会导致缓存中的数据和数据库中的数据出现短暂的不一致。
根据业务需求不同,可以采用强一致性最终一致性两种不同的解决方案。
  • 强一致性:要求任何时刻所有节点看到的数据都是相同的,一般需要采用事务或锁保证更新数据库和更新缓存两个操作是原子性的。适用于严格要求数据实时一致的场景。
    • 最终一致性:允许短暂的不一致,但保证最终会达到一致状态。实际应用中,大多数场景采用最终一致性方案即可。
    为了达到最终一致性,业界逐步形成了下面这几种缓存更新策略:
    更新策略
    内容
    说明
    Cache Aside(旁路缓存)
    读:先读缓存,未命中则读数据库并更新缓存 写:直接更新数据库,然后使缓存失效
    是应用最为广泛的一种缓存策略。需要搭配补偿机制,例如删除缓存失败后发送删除缓存的消息到消息队列,进行异步删除。
    Write Through(直写)
    读:与 Cache-Aside 的读流程相同 写:所有写操作同时更新缓存和数据库
    适用于对一致性要求高的场景,但是写延迟较高。
    Write Behind(回写)
    读:与 Cache-Aside 的读流程相同 写:先更新缓存,异步批量更新数据库
    写的性能高,但是存在数据丢失风险。
    Write-Around(写绕过)
    读:在 Cache Aside 的读流程的基础上 ,增加一个缓存过期时间 写:仅仅更新数据库,不做任何删除或更新缓存的操作,缓存仅能通过过期时间失效。
    实现简单,但缓存中的数据和数据库数据一致性较差,应慎重选择。
    Read-Through(读穿透)
    读:先读缓存,缓存未命中时,由缓存系统负责从数据库加载数据 写:同 Cache-Aside 策略
    由缓存系统控制与数据库的交互,避免出现缓存穿透等问题
    Binlog订阅
    通过中间件(例如 Canal)监听数据库的 Binlog 数据,解析出发生变更的数据,异步删除 Redis 对应的缓存。
    业务代码无侵入,但是系统复杂度高,运维成本高
    为什么是删除缓存,而不是更新缓存?
    • 简化设计,降低复杂度:直接更新缓存需要处理:数据库和缓存的双写一致性(需事务或分布式锁)、缓存更新失败时的回滚逻辑。删除缓存只需删除缓存键,后续读取时自动回填,逻辑更简单,且天然兼容最终一致性。
    • 避免并发写导致的缓存脏数据:当多个线程/服务同时更新同一数据时,直接更新缓存可能导致数据不一致。例如线程A更新数据库(新值:X)→ 线程B更新数据库(新值:Y)→ 线程B更新缓存(Y)→ 线程A更新缓存(X),最终缓存中存储的是过时的X(尽管数据库中是Y)。而删除缓存的优势在于无论线程 A 和线程 B 的操作顺序如何,删除缓存会强制后续读取时重新加载最新数据,避免脏写。
    • 自动淘汰低频访问数据:某些数据可能更新后长时间不被访问,删除缓存可以自然淘汰这些数据。
    延时双删策略
    先删除缓存,再更新数据库这一方案在读并发时可能导致旧数据回填,产生缓存脏数据问题。如果一定要这种方案,可以考虑延时双删的策略,在更新数据库之后,延迟一段时间再次删除缓存。
    1. 先删除缓存
    1. 更新数据库
    1. 延迟一段时间(如 500ms 或 1s)后再次删除缓存,作用是为了保证第二次删除缓存的时间点在读请求更新缓存,这个延迟时间需根据业务读取耗时评估,经验值通常应稍大于业务中读请求的耗时。
    最佳实践:
    1. 首选旁路缓存模式:对于90%的业务,使用“先更新数据库,再删除缓存” + 设置缓存过期时间(TTL) 就足够了。即使出现不一致,缓存最终也会过期,然后从数据库加载最新数据,达到最终一致。
    1. 必须设置缓存过期时间:这是一个安全网,可以防止在极端情况下(如删缓存失败且重试机制也失效)数据永远不一致。
    1. 优先保证数据库正确:任何操作失败,都不能影响数据库主流程。删除缓存失败可以通过重试机制补偿,但更新数据库失败必须回滚或抛出异常。

    3、Redis的脑裂问题

    Redis 脑裂问题是指在 Redis 的主从复制模式或哨兵模式中,由于网络分区导致集群被分割成多个独立的部分,每个部分都有一个主节点,导致多个主节点同时提供写入服务,产生数据不一致、冲突和混乱等问题。所以脑裂问题产生的根本原因是网络分区
    假设我们有一个一主两从三哨兵的经典架构:
    • 主节点M1
    • 从节点S1S2
    • 哨兵节点Sentinel1Sentinel2Sentinel3
    脑裂发生过程:
    1. 网络分区:某个时刻,主节点 M1 和从节点 S1 与整个系统的其他部分(哨兵 Sentinel2Sentinel3 和从节点 S2)之间的网络突然中断。系统被分割成两个部分:
        • 小组AM1 + S1 + Sentinel1 (少数派,因为只有1个哨兵能联系到M1)
        • 小组BS2 + Sentinel2 + Sentinel3 (多数派,有2个哨兵)
    1. 哨兵选举
        • 小组B的 Sentinel2 和 Sentinel3 检测到与 M1 的连接超时(超过 down-after-milliseconds 配置的时间)。经过协商,它们以多数票(2/3)认为主节点 M1 客观下线(ODOWN)。
        • 小组B的哨兵会自动发起一次故障转移(Failover),从 S2 中选举出一个新的主节点(假设是 S2 -> M2)。
        • 此时,系统里出现了两个主节点:原来的 M1(在小组A) 和 新的 M2(在小组B)
    1. 客户端写入
        • 如果客户端 C1 恰好连接在小组A,它仍然会向旧的 M1 写入数据。
        • 如果客户端 C2 连接在小组B,它会向新的 M2 写入数据。
        • 这就导致了最严重的问题:数据分裂。 两边的客户端都在向不同的主节点写入不同的数据,且彼此不知。
    1. 网络恢复
        • 当网络分区修复后,M1S1 重新连接到集群。
        • 哨兵会发现有两个主节点。根据 Redis 的规则,旧主 M1 会被哨兵降级为新主 M2 的从节点
        • M1 在降级前,会清空自己的数据,然后从 M2 进行全量同步(SYNC),以 M2 的数据为准。
    1. 数据丢失
        • 在网络分区期间,客户端 C1 写入旧主 M1 的所有数据,在 M1 被降级并清空数据的过程中,全部永久丢失了
        • 客户端 C2 写入 M2 的数据得以保留。
    notion image
    脑裂带来的问题
    1. 数据不一致:在脑裂期间,两个主节点独立接收写请求,导致数据分叉。
    1. 数据丢失:这是最核心的问题。网络恢复后,旧主节点的数据会被覆盖,导致部分写操作永久丢失。
    1. 系统混乱:客户端可能被分配到不同的主节点,导致应用逻辑错误。
    脑裂问题解决方案
    • 对于主从复制/哨兵模式:Redis 提供了一些关键的配置参数来极大程度地降低脑裂发生概率和影响(但无法100%绝对避免,这是分布式系统的CAP理论决定的)Redis 提供了两个配置项来限制主库的请求处理,这两个配置项需要同时满足,主库才会接受客户端的请求
      • min-slaves-to-write:主节点必须至少有多少个健康(连接正常)的从节点,才能接受写请求。
      • min-slaves-max-lag:从节点最后一次复制数据的延迟时间(lag)必须小于这个值(单位:秒),才被认为是健康的。
      • 在上面的例子中,当发生网络分区后,M1 只剩下 S1 一个从节点。如果 S1 也失联了(或者在小组A里),M1 就发现健康从节点数量(0) < min-slaves-to-write(1),于是它会停止接受所有写请求。这样,即使它自己还是主节点,也不会再接收新数据,从而避免了数据分叉。网络恢复后,它同步新数据即可,不会造成数据丢失。
    • 对于集群模式:Redis Cluster 通过分片和 Gossip 协议能更好地处理网络分区问题。
      • Redis Cluster 要求主节点需要得到大多数(N/2+1)节点的认可才能继续工作
      • 如果一个主节点发现自己无法与集群中大多数节点通信,它会自动停止接受写请求。这本质上和配置 min-slaves-to-write 的效果一样。
      • 因此,在 Redis Cluster 中,脑裂导致数据写入两个主节点的情况更难发生,但依然需要合理设置集群大小(建议至少 3 个主节点)。

    4、Redis的慢查询问题

    Redis 慢查询是指执行时间超过预设阈值的命令请求。Redis 提供了慢查询日志功能用于记录执行时间超过给定时长的命令请求,可以通过查看慢查询日志来监控和优化查询速度。慢查询的两个相关参数:
    • slowlog-log-slower-than:慢查询日志对执行时间大于多少微秒的命令进行记录。
    • slowlog-max-len:慢查询日志最多能记录多少条命令记录(默认是 128)。慢查询日志的底层实现是 一个具有预定大小的先进先出队列,一旦记录的命令数量超过了队列⻓度,最先记录的命令操作就会被删除。一般建议设置为 1000 左右。
    慢查询的相关命令:
    notion image
    注意:
    • 慢查询只记录命令执行时间,并不包括命令排队和网络传输时间。因此客户端执行命令的时间会大于命令实际执行的时间。
    • 由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令。解决方案有:
      • 线上建议调大 slowlog-max-len 参数。
      • 可以定期执行 slowlog get 命令将慢查询日志持久化到其他存储中(例如 MySQL)。
    延迟监控(Latency Monitoring)
    Redis 2.8.13 引入延迟监控(Latency Monitoring),可以帮助我们检查和排查引起延迟的原因。Latency monitor 监控的 latency spikes 则范围广一点,不仅包括命令执行,也包括fork(2)系统调用,key过期等操作的耗时。 Latecny Monitoring 由如下组成:
    • Latency hooks: 采样不同敏感度延迟的代码路径(也称作事件),事件类型有:
    事件
    事件内容
    命令
    详解
    command
    慢命令
    latency history command
    执行时长超过 latency-monitor-threshold阈值的慢命令
    fast-command
    时间复杂度为O(1)和O(logN)的命令
    latency history fast-command
    时间复杂度为O(1)和O(logN)的命令
    fork
    系统调用fork(2)
    latency history fork
    AOF 或RDB 子进程
    • 时间序列:记录不同事件的延迟峰值(也叫延迟尖峰),指运行时间超过latency-monitor-threshold 配置的阈值的事件。
    • 报表引擎:从时间序列获取原始数据。
    • 分析引擎:根据测量提供可读的报告和提示。
    设置/开启latency monitor
    读取latency monitor配置
    获取最近的latency,返回事件名、发生的时间戳、最近的延时(毫秒)、最大的延时(毫秒)
    查看事件延时图
    诊断建议

    5、Redis的性能问题

    Redis 虽然具备高性能的特性,但是也有很多因素会影响 Redis 的性能,导致 Redis 变慢。
    怎样判断 Redis 是否变慢:可以通过延迟基线测量进行判断。Redis 基线性能是指一个系统在低压力、无干扰情况下的基本性能,这个性能只由当前的软硬件配置决定。。一般可以监测 120s 作为最大延迟,然后用单次访问 Redis 的延迟和基线性能做对比,如果观察到的 Redis 运行时延迟是其基线性能的2倍以上,就可以认定Redis变慢了。
    redis-cli 命令提供了 –intrinsic-latency 选项,用来监测和统计测试期间内的最大延迟(以毫秒为单位),这个延迟可以作为 Redis 的基线性能。
    运行的最大延迟是 3079 微秒,所以基线性能是 3079 (3 毫秒)微秒。注意:
    • 要在 Redis 的服务端运行,而不是客户端。这样可以避免网络对基线性能的影响。
    • 如果想监测网络对 Redis 的性能影响,可以使用 Iperf 测量客户端到服务端的网络延迟。如果网络延迟几百毫秒,说明网络可能有其他大流量的程序在运行导致网络拥塞,需要找运维协调网络的流量分配。
    Redis 变慢的可能原因:
    原因
    描述
    排查方式
    解决方案
    慢查询命令
    涉及到集合操作的复杂度一般为O(N),比如: • 集合全量查询:HGETALL、SMEMBERS • 集合聚合操作:SORT、LREM、 SUNION等 • 所有key查询:KEYS • 数据库操作:FLUSHDB、FLUSHALL • bigkey操作:如果一个 key 写入的 value 非常大,那么 Redis 在分配内存时就会比较耗时;当删除这个 key 时,释放内存也会比较耗时
    • 慢日志功能 • latency-monitor(延迟监控)工具 • redis-cli 自带 bigkey 扫描或者 redis-rdb-tools工具
    • 尽可能使用O(1) 和 O(log N)命令。例如需要返回一个SET中的所有成员时,使用SSCAN多次迭代返回,代替SMEMBERS命令一次性返回。 • 需要执行排序、交集、并集操作时,可以在客户端完成 • 线上环境禁用KEYS、FLUSHDB、FLUSHALL等命令 • 尽量避免用bigkey,用 unlink 命令代替 del 来删除,释放内存也会放到后台线程中执行
    RDB
    • 生成RDB快照,fork 操作导致主线程延迟 • 加载RDB快照,从库加载 RDB 期间无法提供读写服务
    latency-monitor(延迟监控)工具
    • 增加机器内存 • 增加 Cluster 集群的数量分担数据量,减少每个实例所需的内存。 • 控制实例的内存尽量在 10G 以下,执行 fork 的耗时与实例大小有关,实例越大,耗时越久
    AOF
    • AOF日志刷盘,如果 AOF 配置为 appendfsync always,那么 Redis 每处理一次写操作都会把这个命令写入到磁盘中才返回,导致主线程组册 • AOF日志重写,fork 操作导致主线程延迟
    latency-monitor(延迟监控)工具
    • AOF刷盘配置 appendfsync everysec • 控制实例的内存尽量在 10G 以下,执行 fork 的耗时与实例大小有关,实例越大,耗时越久
    内存大页机制
    应用程序向操作系统申请内存时,是按内存页进行申请的,而常规的内存页大小是 4KB。Linux 内核从 2.6.38 开始,支持了内存大页机制,该机制允许应用程序以 2MB 大小为单位,向操作系统申请内存。 开启内存大页会导致申请内存的耗时变长,进而导致每个写请求的延迟增加,影响到 Redis 性能。
    $ cat /sys/kernel/mm/transparent_hugepage/enabled 如果,执行结果是always,表示内存大页机制启动了;如果是never,表示禁止了。
    • 关闭内存大页,echo never /sys/kernel/mm/transparent_hugepage/enabled
    使用SWAP内存
    swap 区涉及到磁盘IO
    # 先找到 Redis 的进程 ID $ ps -aux | grep redis-server # 查看 Redis Swap 使用情况 $ cat /proc/$pid/**aps | egrep '^(Swap|Size)’ 如果 Swap 一切都是 0 kb,或者零星的 4k ,那么一切正常。当出现百 MB,甚至 GB 级别的 swap 大小时,就表明,此时,Redis 实例的内存压力很大
    • 增加机器内存 • 增加 Cluster 集群的数量分担数据量,减少每个实例所需的内存。
    淘汰过期数据
    Redis 有两种方式淘汰过期数据: • 惰性删除:当接收请求的时候发现 key 已经过期,才执行删除; • 定时删除:每 100 毫秒删除一些过期的 key。 定时删除的算法如下: 1. 随机采样 20 个数的 key,删除所有过期的 key; 2. 如果发现还有超过 25% 的 key 已过期,则执行步骤一。 大量的 key 设置了相同的时间参数。同一秒内,大量 key 过期,需要重复删除多次才能降低到 25% 以下,而删除过程主线程是阻塞的。
    • 在key过期时间参数上,加上一个一定大小范围内的随机数,避免大量 key 同时实效。
    绑定CPU
    如果把 Redis 进程只绑定了一个 CPU 逻辑核心上,那么当 Redis 在进行数据持久化时,fork 出的子进程会继承父进程的 CPU 使用偏好。子进程会与主进程发生 CPU 争抢,进而影响到主进程服务客户端请求,访问延迟变大。
    • 让redis绑定同一个物理核心的多个逻辑核心 • redis6.0以后,可以对主线程、后台线程、后台 RDB 进程、AOF rewrite 进程,绑定固定的 CPU 逻辑核心
    排查思路:
    1. 获取 Redis 实例在当前环境下的基线性能。
    1. 是否用了慢查询命令?如果是的话,就使用其他命令替代慢查询命令,或者把聚合计算命令放在客戶端做。
    1. 是否对过期key设置了相同的过期时间?对于批量删除的key,可以在每个key的过期时间上加一个随机数,避免同时删除。
    1. 是否存在bigkey?对于bigkey的删除操作,如果你的Redis是4.0及以上的版本,可以直接利用异步线程 机制减少主线程阻塞;如果是Redis 4.0以前的版本,可以使用SCAN命令迭代删除;对于bigkey的集合查询和聚合操作,可以使用SCAN命令在客戶端完成。
    1. RedisAOF配置级别是什么?业务层面是否的确需要这一可靠性级别?如果我们需要高性能,同时也允许数据丢失,可以将配置项no-appendfsync-on-rewrite设置为yes,避免AOF重写和fsync竞争磁盘IO资源,导致Redis延迟增加。当然,如果既需要高性能又需要高可靠性,最好使用高速固态盘作为AOF日志的写入盘。
    1. Redis实例的内存使用是否过大?发生swap了吗?如果是的话,就增加机器内存,或者是使用Redis集 群,分摊单机Redis的键值对数量和内存压力。同时,要避免出现Redis和其他内存需求大的应用共享机 器的情况。
    1. 是否启用了透明大⻚机制?如果是的话,直接关闭内存大⻚机制就行了。
    1. 是否运行了Redis主从集群?如果是的话,把主库实例的数据量大小控制在2~4GB,以免主从复制时,从库因加载大的RDB文件而阻塞。
    1. 是否使用了多核CPU或NUMA架构的机器运行Redis实例?使用多核CPU时,可以给Redis实例绑定物理核;使用NUMA架构时,注意把Redis实例和网络中断处理程序运行在同一个CPU Socket上。
    Redis 实践建议
    notion image
     

    6、Redis的内存碎片问题

    内存分配策略:Redis 使用 jemalloc 内存分配器,每次按照一系列固定的大小划分内存空间,例如8字节、16字节、32字节、48字 节,..., 2KB、4KB、8KB,按照所需要的内存最接近的最小规格进行分配。
    产生内存碎片的原因:
    • 内存分配器只能按照固定大小分配内存,所以,分配的内存空间一般都会比申请的空间大一些,在该内存规格里面剩下的内存空间即为内存碎片。
    • 键值被修改和删除,会导致空间的扩容和释放。
    notion image
    判断是否有内存碎片
    • used_memory:Redis为了保存数据实际申请使用的空间。
    • used_memory_rss:操作系统实际分配给Redis的物理内存空间,里面就包含了碎片。
    • mem_fragmentation_ratio:内存碎片率, mem_fragmentation_ratio = used_memory_rss / used_memory
      • mem_fragmentation_ratio 大于 1 但小于 1.5。这种情况是合理的。
      • mem_fragmentation_ratio 大于 1.5 。这表明内存碎片率已经超过了50%。一般情况下,这个时候, 我们就需要采取一些措施来降低内存碎片率了。
    解决内存碎片问题:开启内存碎片清理功能
    然后,设置触发内存清理的条件:
    • active-defrag-ignore-bytes 100mb:表示内存碎片的字节数达到 100MB 时,开始清理;
    • active-defrag-threshold-lower 10:表示内存碎片空间占操作系统分配给 Redis 的总空间比例达到 10%时,开始清理。
    最后,控制清理操作占用CPU时间比例的上、下限:
    • active-defrag-cycle-min 25:表示自动清理过程所用 CPU 时间的比例不低于25%,保证清理能正常开展;
    • active-defrag-cycle-max 75:表示自动清理过程所用 CPU 时间的比例不高于75%,一旦超过,就停止清理,从而避免在清理时,大量的内存拷⻉阻塞 Redis,导致响应延迟升高。

    7、Redis的BigKey问题

    BigKey 是指 Key 对应的 Value 值很大。业界常见的 Big Key 标准:
    • String 类型的 value 大于 10 KB。
    • 非 String 类型(Hash、List、Set、ZSet)的元素的个数超过 5000个。
    BigKey 的危害:
    1. 阻塞风险:对 Big Key 执行 GETHGETALLLRANGEZRANGE 等命令会非常耗时,因为数据量巨大。而且由于 Redis 是单线程的。一个耗时的操作会阻塞后续的所有命令,导致整体服务延迟增高,甚至超时。
    1. 网络拥塞:每次读取 Big Key 都会产生巨大的网络流量,可能占满带宽,影响其他服务的网络通信。
    1. 内存不均衡:在 Redis Cluster 集群模式下,数据会分片存储。Big Key 会导致某个分片的内存使用率远高于其他节点,无法平衡集群的内存和负载压力。
    1. 数据迁移困难:在集群扩容或缩容时,需要进行数据迁移。Big Key 的迁移过程同样会非常耗时,并且可能导致迁移失败。
    1. 主从复制和持久化问题:主从复制、AOF 重写和 RDB 保存时,如果存在 Big Key,会导致整个流程耗时很长,可能导致 Redis 服务暂停数毫秒甚至数百毫秒。
    如何检测 BigKey:有三种方法:
    • 使用 redis-cli 自带命令。这是 Redis 官方自带的命令,可以对整个 Redis 实例进行扫描,找出每种数据类型中最大的 key。但是只能返回每种类型中最大的那个 bigkey,无法得到大小排在前 N 位的 bigkey,而且最好在从节点上执行。
      • 使用MEMORY USAGE 命令。
        • 使用STRLEN 命令。对于 String 类型,可以直接使用 STRLEN 命令获取字符串的长度,也就是占用的内存空间字节数。
        • 使用 SCAN 命令扫描
          • 使用第三方工具:
            • Redis-Rdb-Tools:用来解析 Redis 快照(RDB)文件,找到其中的大 key。
            • Redis Memory Analyzer
          如何删除 BigKey
          • 渐进式删除:对于 Hash/List/Set/ZSet 类型,使用渐进式分批删除策略,避免直接使用DEL命令
            • Hash:使用 hscan 命令,每次获取 100 个字段,再用 hdel 命令,每次删除 1 个字段。
            • List:通过 ltrim 命令,每次删除少量元素。
            • Set:使用 sscan 命令,每次扫描集合中 100 个元素,再用 srem 命令每次删除一个键。
            • ZSet:使用 zremrangebyrank 命令,每次删除 top 100个元素。
          • 异步删除:从 Redis 4.0 版本开始,可以采用异步删除法,用 UNLINK 命令代替 DEL 来删除。这样 Redis 会将这个 key 放入到一个异步线程中进行删除,这样不会阻塞主线程。

          8、Redis的热点Key问题

          热点 Key 是指在 Redis 中的某个 Key 被超高频率地访问,大量的 CPU 或者带宽集中在同一个 Key。这个 Key 通常对应着一个 Value 较大的数据,大量的客户端请求(例如每秒数十万次)都集中访问这一个 Key。
          我们可以参考阿里云对 Redis 热点 Key 的定义:
          • 访问频率高度集中:例如,某个 Redis 实例的总QPS(每秒查询率)为 10,000,而其中一个 Key 的每秒访问量就达到了 7,000,占比极高。
          • 带宽占用显著:例如,对一个拥有上千个成员且总大小为 1MB 的 HASH Key 每秒发送大量的 HGETALL 操作请求。
          • CPU时间占用突出:例如,对一个拥有数万个成员的 ZSET Key 每秒发送大量的 ZRANGE 操作请求。
          热点 Key 存在的问题:
          • 单点压力过大:在 Redis 集群模式下,由于某个 Key 特别热,会导致它所处的那个分片实例负载极高,而其他实例非常空闲,无法充分利用集群资源。这个“忙碌”的实例就成了整个系统的瓶颈。
          • 缓存击穿风险:如果这个热点 Key 恰好过期,海量的请求会瞬间穿透 Redis,直接打到后端数据库(如 MySQL)上,可能导致数据库瞬间压力过大而宕机。
          • 性能瓶颈:如果该 Key 对应的 Value 很大(例如一个包含几万元素的 Hash,或一个巨大的字符串),频繁的读取会占满服务器网卡带宽,频繁处理同一个大 Key 的序列化/反序列化、压缩等操作也会带来一定的 CPU 压力。
          如何发现/监控热点 Key?
          1. Redis 自带的命令
              • redis-cli --hotkeys:Redis 4.0 及以上版本支持。执行该命令可以扫描出当前实例的热点 Key。注意:此命令会全程扫描所有 Key,在线使用可能对性能有影响,建议在低峰期使用。
              • MONITOR 命令:这是一个调试命令,会实时打印出 Redis 处理的所有命令。可以短时间运行,然后通过脚本分析输出,找出高频 Key。绝对不要在生产环境长时间使用,会严重拖慢性能。
          1. 基于代理或中间件:一些开源的 Redis 代理(如 Codis)或集群解决方案(如 Twemproxy)会在中间件层面统计每个 Key 的访问频率,从而上报热点 Key。
          1. 客户端统计:在业务代码的客户端层进行埋点,对访问的 Key 进行聚合统计,然后上报给监控系统。这种方式对服务性能有一定侵入性,但最为精准。
          1. 网络流量监控:监控 Redis 服务器网卡流量,如果发现某个实例的入/出流量远高于其他实例,很可能存在热点 Key 或大 Key。
          1. 第三方工具:一些 APM(应用性能管理)工具,如阿里云的 Redis 控制台,提供了热点 Key 的发现功能。
          如何优化热点Key?
          1、热点 Key 拆分:将热点数据分散到多个子 Key 中。
          • 实现:
            • 原始 Key 是 hotkey:123,可以将其数据分散到子 Key hotkey:123:1hotkey:123:2, ... hotkey:123:n
            • 客户端在访问时,然后使用 HMGET 或 HGETALL 分批获取。
          • 优点:实现简单,能有效分散压力。
          • 缺点:查询时候需要分批获取再组装,存在性能消耗。
          2、多级缓存:在 Redis 之前引入本地缓存(如 Guava Cache、Caffeine、Ehcache),存储热点 Key 数据。
          • 实现:应用先读本地缓存 -> 没有则读 Redis -> 将数据回填到本地缓存并设置一个较短的过期时间(例如几秒)。
          • 优点:极大减轻 Redis 压力。
          • 缺点:存在数据一致性问题。可以通过 Redis 的 Pub/Sub 机制更新本地缓存,也可以考虑使用成熟的多级缓存框架(如阿里的 JetCache)。
          3、读写分离:通过 Redis 的主从复制机制,建立多个从节点。
          • 实现:客户端将读请求分散到不同的从节点上去,写请求仍然发往主节点。
          • 优点:简单,利用 Redis 自带功能。
          • 缺点:只适用于读多写少的场景。
          方案选择:
          • 如果是读写混合热点,优先拆分成多个子 Key。
          • 如果是纯读热点,优先考虑本地缓存读写分离

          9、为什么Redis实现使用跳表而不是B+树

          这是一个经典的面试题。Redis 作者 Salvatore Sanfilippo (antirez) 曾给出过明确回答,主要原因包括:
          1. 实现简单性:跳跃表的实现比平衡树(如红黑树、AVL树)要简单得多,代码更易读、调试和维护。这符合 Redis 追求简单稳定的哲学。
          1. 范围查询高效:跳跃表本质上是链表结构,因此在执行范围查询(如 ZRANGEZREVRANGE)时,一旦找到范围内的第一个节点,后续只需要沿着最底层的链表遍历即可,非常高效。而在平衡树中实现范围查询可能需要复杂的中序遍历。
          1. 并发友好:在早期版本中,跳跃表更易于实现并发控制。虽然现在的平衡树也有并发版本,但跳跃表的简单性使其在并发环境下更容易处理。
          1. 性能相当:对于插入、删除、查找和有序遍历等操作,跳跃表的平均时间复杂度也是 O(log N),与平衡树相当,性能上并不逊色。

          10、为什么Redis这么快

          • 基于内存的数据存储:这是最根本的原因。Redis 将所有数据主要存储在内存中,读写操作直接与内存交互,而内存的访问速度比磁盘(即使是 SSD)要快几个数量级(纳秒 vs 毫秒级)。
          • 单线程的事件驱动机制:Redis 设计了事件驱动机制,把所有的读写操作抽象成事件,然后通过单线程顺序地处理事件,这样可以避免上下文切换和锁机制开销,提高了性能。
          • 高效的数据结构:Redis 的每种数据结构都针对其常用操作进行了极致的优化,例如 ZSet 结构,无论是单个查询,还是顺序范围查询,查询效率都极高;对于小数据量的列表和哈希,使用压缩列表(ziplist)或者 紧凑列表(listpack)来实现,减少了内存碎片和内存占用。
           
          Mysql基础篇:整体架构和存储引擎Redis系列:高可用架构(主从复制模式、哨兵模式、集群模式)
          Loading...