type
status
date
slug
summary
tags
category
icon
password

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

Redis 一般和其他数据库搭配使用,Redis 会把 MySQL 中经常被查询的数据缓存起来,用户可以直接访问Redis中的缓存数据,用来减轻后端数据库的压力。出于容错考虑,一般在在 Redis 中找不到的数据,就会去数据库里面查找。如果 Redis 缓存层失效,大量的访问直接到达数据库,对数据库造成压力过大,这就是缓存失效问题。一般有三种情况:
问题
问题原因
解决方案
缓存雪崩
缓存中大批量的 key 同时过期,导致大量并发访问直接到达数据库
• 缓存数据设置随机过期时间,防止同一时间大量数据过期现象发生。 • 互斥锁:保证单个key只有一个线程去数据库查询,其他线程负责等待 • 双 key 策略:主key缓存数据(key-data),备key缓存过期时间(key-time),其中(key-time)的缓存失效时间设置为短期(比如5min),(key-data)的缓存失效时间设置为长期(比如1天)。当第一个线程发现 key-time 过期不存在时,则先更新key-time,然后去查询数据库并更新key-data 的值;当其他线程来获取数据时,虽然第一个线程还没有从数据库查询完毕并更新缓存,但发现key-time存在,会直接读取缓存的旧数据返回。
Redis 故障宕机,导致大量并发访问直接到达数据库
• 服务熔断和限流 • 构建Redis高可用集群
缓存击穿
缓存中某些高并发的热点 key 过期,针对该key的大量并发访问直接到达数据库
• 设置热点数据永远不过期。 • 互斥锁,保证单个key只有一个线程去数据库查询,其他线程负责等待
缓存穿透
大量访问缓存和数据库中都没有的数据,相当于进行了两次无用的查询,造成大量的访问直达数据库。 例如用户故意使用空值或者其他不存在的值进行频繁请求攻击数据库。
• 请求入口对请求的合理性进行检查 • 缓存空值或者缺省值 • 布隆过滤器。布隆过滤器由「初始值都为 0 的位图数组」和「 N 个哈希函数」两部分组成。当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据。
 
三者的区别:
  • 缓存雪崩和缓存击穿都是访问数据库中存在,但是redis中不存在的数据;而缓存穿透是访问redis和数据库中都没有的数据。
  • 缓存击穿指少量热点数据过期,缓存雪崩是大量不同的数据都过期,两者都会导致大量访问直达数据库。

2、缓存一致性问题

在实际业务中,经常会使用redis缓存减轻数据库的压力。当有数据更新时,因为更新DB和操作缓存两个动作不是原子操作,那们就需要考虑是先更新数据库还是先操作缓存?操作缓存的话是删除缓存还是更新缓存?这些操作会导致缓存中的数据就会与DB数据出现短暂的不一致,这就是缓存一致性问题。
缓存一致性问题有四种解决方案:
解决方案
潜在问题
应对方案
先更新数据库,再更新缓存
更新数据库成功,但更新缓存失败,导致从缓存中读到的仍然是旧值
消息队列保证最终一致性:先更新DB数据,更新缓存失败后发送更新缓存的消息到消息队列,进行异步更新。 这里还需要的一个操作就是利用消息队列的重试机制,保证缓存能够更新成功,如果多次消费失败,可能是由于网络原因或者redis服务挂了,此处可以添加告警处理。
先更新数据库,再删除缓存
更新数据库成功,但删除缓存失败,导致后面更新缓存中的仍然是旧值
消息队列保证最终一致性,先更新DB数据,删除缓存失败后发送删除缓存的消息到消息队列,进行异步删除。
先更新缓存,再更新数据库
更新缓存成功,但更新数据库失败,导致数据库中仍然是旧值,但缓存中的是新值,最严重的问题
不推荐使用
先删除缓存,再更新数据库
删除缓存成功,但更新数据库失败,导致数据库中仍然是旧值,而且后面更新到缓存的也是旧值
延迟双删策略:适合于对实时性要求不高的场景,保证最终一致性。 1. 删除缓存 2. 更新数据库 3. 休眠500毫秒(根据具体的业务时间来定) 4. 再次删除缓存
优先采用先更新数据库,再删除缓存的策略。因为在实际生产中,我们一般以数据库的数据为准,缓存数据只是作为一个缓冲层,所以我么应该优先保证更新数据的动作的成功。至于是删除缓存还是更新缓存,我们可以分情况讨论:
  • 更新考虑问题:采用更新缓存的方式,需要考虑在操作完DB之后,后续的更新缓存操作,是否需要比较复杂的操作才能得到应该set进缓存中值?例如需要复杂计算或者DB交互查询。如果是的话,那么不建议使用更新缓存的方式。
  • 并发写问题。假如两个写请求A和B同时进行写DB操作,A先于B,B基于A做更新,此时假如是使用传值进行更新缓存,可能出现的问题就是B的写缓存操作,先于A的写缓存操作,从而导致缓存被A的更新数据覆盖,缓存中的数据变成旧的数据。出现缓存不一致。
  • 性能问题:如果某个数据一个小时内只读1次,采用更新缓存的方式数据库1小时内更新了1000次,那么缓存也要更新1000次;采用删除缓存的方式,只需要删除1次数据,只有当缓存真正被读取的时候才去数据库加载。
  • 由上可见,更新缓存的方式出现缓存不一致的可能性更大,删除缓存相对还节约内存空间,如果是读多写少的场景优先考虑删除缓存。
消息队列一致性方案也有两种方式:
  • 发送更新缓存的消息到消息队列,读取消息直接更新缓存。
  • 模拟数据库master的slave节点,收到binlog后封装binlog消息发送到消息队列,然后解析binlog消息再更新缓存。

3、Redis脑裂问题

脑裂问题是指在主从集群中,同时有两个主节点,它们都能接收写请求。客户端不知道应该往哪个主节点写入数据,结果就是不同的客户端会往不同的主节点上写入数据。
严重的话,脑裂会进一步导致数据丢失。也就是说,等到哨兵让原主库和新主库做全量同步后,原主库在切换期间保存的数据就丢失了。这个问题的发生过程:
  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就会拒绝客户端请求。主库发生假故障,在假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然也就无法和从库进行 ACK 确认了。主库接收到客户端的写入请求会拒绝,就可以减少数据同步之后的数据丢失。

4、Redis慢查询问题

Redis慢查询日志功能是用于记录执行时间超过给定时长的命令请求,可以通过查看慢查询日志来监控和优化查询速度。慢查询的两个相关参数:
  • slowlog-log-slower-than:慢查询日志对执行时间大于多少微秒的命令进行记录。
  • slowlog-max-len:慢查询日志最多能记录多少条命令记录。慢查询日志的底层实现是 一个具有预定大小的先进先出队列,一旦记录的命令数量超过了队列⻓度,最先记录的命令操作就会被删除。这个值默认是128。但是,如果慢查询命令较多的话,日志里就存不下了;如果这个值太大了,又会占用一定的内存空间。所以,一般建议设置为1000左右,这样既可以多记录些慢查询命令,方便排查, 也可以避免内存开销。
慢查询的相关命令:
  • 获取满查询日志:slowlog get [n],后面的n表示可以获取最新n条日志,slowlog get本身就属于慢查询。
  • 获取慢查询日志列表当前的长度:slowlog len
  • 慢查询日志重置:slowlog reset
notion image
满查询日志注意事项:
  • 慢查询只记录命令执行时间,并不包括命令排队和网络传输时间。因此客户端执行命令的时间会大于命令实际执行的时间。
  • 由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令。解决方案有:1)线上建议调大slowlog-max-len参数;2)可以定期执行slowlog get命令将慢查询日志持久化到其他存储中(例如,MySQL),然后可以制作出可视化界面进行查询。
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 很大。一般而言,下面这两种情况被称为大 key:
  • String 类型的值大于 10 KB。
  • Hash、List、Set、ZSet 类型的元素的个数超过 5000个。
如何找到大 key?有三种方法:
  • redis-cli -h 127.0.0.1 -p6379 -a "password" — bigkeys。最好在从节点上执行,只能返回每种类型中最大的那个 bigkey,无法得到大小排在前 N 位的 bigkey。
  • 使用 SCAN 命令对数据库扫描,然后用 TYPE 命令获取返回的每一个 key 的类型。
    • 对于 String 类型,可以直接使用 STRLEN 命令获取字符串的长度,也就是占用的内存空间字节数。
    • 对于集合类型来说,有两种方法可以获得它占用的内存大小:如果能够预先从业务层知道集合元素的平均大小,那么,可以使用下面的命令获取集合元素的个数,然后乘以集合元素的平均大小,这样就能获得集合占用的内存大小了。List 类型:LLEN 命令;Hash 类型:HLEN 命令;Set 类型:SCARD 命令;Sorted Set 类型:ZCARD 命令;如果不能提前知道写入集合的元素大小,可以使用 MEMORY USAGE 命令(需要 Redis 4.0 及以上版本),查询一个键值对占用的内存空间。
  • 使用 RdbTools 工具:用来解析 Redis 快照(RDB)文件,找到其中的大 key。
删除大 key 的方法:
  • 分批删除:
    • 对于删除大 Hash,使用 hscan 命令,每次获取 100 个字段,再用 hdel 命令,每次删除 1 个字段。
    • 对于删除大 List,通过 ltrim 命令,每次删除少量元素。
    • 对于删除大 Set,使用 sscan 命令,每次扫描集合中 100 个元素,再用 srem 命令每次删除一个键。
    • 对于删除大 ZSet,使用 zremrangebyrank 命令,每次删除 top 100个元素。
  • 异步删除:从 Redis 4.0 版本开始,可以采用异步删除法,用 unlink 命令代替 del 来删除。这样 Redis 会将这个 key 放入到一个异步线程中进行删除,这样不会阻塞主线程。
Mysql基础篇:整体结构和SQL的执行过程Redis高可用
mcbilla
mcbilla
一个普通的干饭人🍚
Announcement
type
status
date
slug
summary
tags
category
icon
password
🎉欢迎来到飙戈的博客🎉
-- 感谢您的支持 ---
👏欢迎学习交流👏