type
status
date
slug
summary
tags
category
password

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

Redis 一般和其他数据库搭配使用,Redis 会把 MySQL 中经常被查询的数据缓存起来,用户可以直接访问 Redis 中的缓存数据,用来减轻后端数据库的压力。出于容错考虑,一般在在 Redis 中找不到的数据,就会去数据库里面查找。如果 Redis 缓存层失效,大量的访问直接到达数据库,对数据库造成压力过大,这就是缓存失效问题。一般有三种情况:
问题
问题原因
解决方案
缓存雪崩
缓存中大批量的 key 同时过期或者 Redis 故障宕机,导致大量并发访问直接到达数据库
1、设置随机过期时间,防止同一时间大量数据过期现象发生。 2、互斥锁:保证单个 key 只有一个线程去数据库查询,其他线程负责等待 3、多级缓存:设置本地缓存作为备份,当分布式缓存失效时,先检查本地缓存。 4、数据库熔断和限流。 5、搭建 Redis 高可用集群
缓存击穿
缓存中某些高并发的热点 key 过期,针对该 key 的大量并发访问直接到达数据库
1、永不过期策略:设置热点数据永远不过期。 2、互斥锁:保证单个 key 只有一个线程去数据库查询,其他线程负责等待。 3、提前续期:在缓存即将过期前,异步刷新缓存。
缓存穿透
大量访问缓存和数据库中都不存在的数据,导致每次请求都绕过缓存直接访问数据库。 例如用户故意使用空值或者其他不存在的值进行频繁请求攻击数据库。
1、参数校验:请求入口对请求的合理性进行检查,过滤非法请求 2、缓存空对象:对查询结果为空或者 null 的数据也进行缓存,设置较短过期时间 3、布隆过滤器:使用布隆过滤器预先判断 key 是否存在。 4、限流措施:对频繁访问的 IP 进行限制
布隆过滤器由「初始值都为 0 的位图数组」和「 N 个哈希函数」两部分组成。当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,查询布隆过滤器显示数据存在,数据库中不一定存在这个数据,但是显示数据不存在,数据库中一定就不存在这个数据。
三者的区别:
  • 缓存雪崩和缓存击穿都是访问数据库中存在,但是 Redis 中不存在的数据;而缓存穿透是访问 Redis 和数据库中都没有的数据。
  • 缓存击穿指少量热点数据过期,缓存雪崩是大量不同的数据都过期,两者都会导致大量访问直达数据库。

2、缓存一致性问题

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

    3、Redis脑裂问题

    Redis 脑裂问题是指在 Redis 的主从复制模式或哨兵模式中,由于网络分区导致集群被分割成多个独立的部分,每个部分都有一个主节点,从而导致数据不一致的问题。
    Redis Cluster 通过分片和 Gossip 协议能更好地处理网络分区问题。
    脑裂问题导致的问题
    • 客户端连接到不同的主节点。多个客户端可能连接到不同的"主节点",导致数据写入不同节点。
    • 导致数据丢失。等到哨兵让原主库和新主库做全量同步后,原主库在切换期间保存的数据就丢失了。这个问题的发生过程:
        1. redis主库被阻塞一段时间,超过了哨兵检测心跳的的最大时长,哨兵判断主库客观下线,开始新一轮选主。
        1. 在新主库还没选出来之前,旧主库恢复正常,客户端继续向旧主库写入数据。
        1. 新主库选主成功,旧主库降级为从库,清空本地数据,和新主库全量同步数据,客户端在第2步写入的数据全部丢失。
        notion image
    脑裂问题解决方案:Redis 提供了两个配置项来限制主库的请求处理,这两个配置项需要同时满足,主库才会接受客户端的请求:
    • min-slaves-to-write:主库能进行数据同步的最少从库数量
    • min-slaves-max-lag:主从复制时 slave 连接到 master 的最大延迟时间
    假如 min-slaves-to-write 设置为 3, min-slaves-max-lag 设置为 10,要求至少 master 至少有 3 个 slave 节点,且数据复制和同步的延迟不能超过 10 秒,如果不满足条件 master 就会拒绝客户端的写请求。

    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 的值很大,而是 key 对应的 value 很大。下面这两种情况被称为 BigKey:
    • String 类型的 value 大于 10 KB。
    • 非 String 类型(Hash、List、Set、ZSet)的元素的个数超过 5000个。
    BigKey 的危害:
    1. 内存不均衡:导致集群中某些节点内存使用率过高
    1. 阻塞风险:删除或查询大key可能长时间阻塞Redis
    1. 网络拥塞:传输大key占用大量带宽
    1. 持久化问题:AOF重写和RDB保存时可能造成延迟
    1. 迁移困难:在集群环境下难以迁移
    如何检测 BigKey:有三种方法:
    • 使用 redis-cli 自带命令。使用最好在从节点上执行,只能返回每种类型中最大的那个 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 放入到一个异步线程中进行删除,这样不会阻塞主线程。
          Mysql基础篇:整体架构和存储引擎Redis系列:高可用架构(哨兵、集群)
          Loading...