大厂面试:聊聊Redis大Key对持久化的影响?

张开发
2026/4/15 15:55:43 15 分钟阅读

分享文章

大厂面试:聊聊Redis大Key对持久化的影响?
上周有位读者字节一二面时被问到Redis 的大 Key 对持久化有什么影响Redis 的持久化方式有两种AOF 日志和 RDB 快照。所以接下来针对这两种持久化方式具体分析分析。大 Key 对 AOF 日志的影响先说说 AOF 日志三种写回磁盘的策略Redis 提供了 3 种 AOF 日志写回硬盘的策略分别是Always这个单词的意思是「总是」所以它的意思是每次写操作命令执行完后同步将 AOF 日志数据写回硬盘Everysec这个单词的意思是「每秒」所以它的意思是每次写操作命令执行完后先将命令写入到 AOF 文件的内核缓冲区然后每隔一秒将缓冲区里的内容写回到硬盘No意味着不由 Redis 控制写回硬盘的时机转交给操作系统控制写回的时机也就是每次写操作命令执行完后先将命令写入到 AOF 文件的内核缓冲区再由操作系统决定何时将缓冲区内容写回硬盘。这三种策略只是在控制 fsync() 函数的调用时机。当应用程序向文件写入数据时内核通常先将数据复制到内核缓冲区中然后排入队列然后由内核决定何时写入硬盘。如果想要应用程序向文件写入数据后能立马将数据同步到硬盘就可以调用 fsync() 函数这样内核就会将内核缓冲区的数据直接写入到硬盘等到硬盘写操作完成后该函数才会返回。Always 策略就是每次写入 AOF 文件数据后就执行 fsync() 函数Everysec 策略就会创建一个异步任务来执行 fsync() 函数No 策略就是永不执行 fsync() 函数;分别说说这三种策略在持久化大 Key 的时候会影响什么在使用 Always 策略的时候主线程在执行完命令后会把数据写入到 AOF 日志文件然后会调用 fsync() 函数将内核缓冲区的数据直接写入到硬盘等到硬盘写操作完成后该函数才会返回。当使用 Always 策略的时候如果写入是一个大 Key主线程在执行 fsync() 函数的时候阻塞的时间会比较久因为当写入的数据量很大的时候数据同步到硬盘这个过程是很耗时的。当使用 Everysec 策略的时候由于是异步执行 fsync() 函数所以大 Key 持久化的过程数据同步磁盘不会影响主线程。当使用 No 策略的时候由于永不执行 fsync() 函数所以大 Key 持久化的过程不会影响主线程。大 Key 对 AOF 重写和 RDB 的影响当 AOF 日志写入了很多的大 KeyAOF 日志文件的大小会很大那么很快就会触发AOF 重写机制。AOF 重写机制和 RDB 快照bgsave 命令的过程都会分别通过 fork() 函数创建一个子进程来处理任务。在创建子进程的过程中操作系统会把父进程的「页表」复制一份给子进程这个页表记录着虚拟地址和物理地址映射关系而不会复制物理内存也就是说两者的虚拟空间不同但其对应的物理空间是同一个。这样一来子进程就共享了父进程的物理内存数据了这样能够节约物理内存资源页表对应的页表项的属性会标记该物理内存的权限为只读。随着 Redis 存在越来越多的大 Key那么 Redis 就会占用很多内存对应的页表就会越大。在通过 fork() 函数创建子进程的时候虽然不会复制父进程的物理内存但是内核会把父进程的页表复制一份给子进程如果页表很大那么这个复制过程是会很耗时的那么在执行 fork 函数的时候就会发生阻塞现象。而且fork 函数是由 Redis 主线程调用的如果 fork 函数发生阻塞那么意味着就会阻塞 Redis 主线程。由于 Redis 执行命令是在主线程处理的所以当 Redis 主线程发生阻塞就无法处理后续客户端发来的命令。我们可以执行 info 命令获取到 latest_fork_usec 指标表示 Redis 最近一次 fork 操作耗时。# 最近一次 fork 操作耗时 latest_fork_usec:315如果 fork 耗时很大比如超过1秒则需要做出优化调整单个实例的内存占用控制在 10 GB 以下这样 fork 函数就能很快返回。如果 Redis 只是当作纯缓存使用不关心 Redis 数据安全性问题可以考虑关闭 AOF 和 AOF 重写这样就不会调用 fork 函数了。在主从架构中要适当调大 repl-backlog-size避免因为 repl_backlog_buffer 不够大导致主节点频繁地使用全量同步的方式全量同步的时候是会创建 RDB 文件的也就是会调用 fork 函数。那什么时候会发生物理内存的复制呢当父进程或者子进程在向共享内存发起写操作时CPU 就会触发缺页中断这个缺页中断是由于违反权限导致的然后操作系统会在「缺页异常处理函数」里进行物理内存的复制并重新设置其内存映射关系将父子进程的内存读写权限设置为可读写最后才会对内存进行写操作这个过程被称为「**写时复制(Copy On Write)**」。写时复制顾名思义在发生写操作的时候操作系统才会去复制物理内存这样是为了防止 fork 创建子进程时由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。如果创建完子进程后父进程对共享内存中的大 Key 进行了修改那么内核就会发生写时复制会把物理内存复制一份由于大 Key 占用的物理内存是比较大的那么在复制物理内存这一过程中也是比较耗时的于是父进程主线程就会发生阻塞。所以有两个阶段会导致阻塞父进程创建子进程的途中由于要复制父进程的页表等数据结构阻塞的时间跟页表的大小有关页表越大阻塞的时间也越长创建完子进程后如果子进程或者父进程修改了共享数据就会发生写时复制这期间会拷贝物理内存如果内存越大自然阻塞的时间也越长这里额外提一下 如果Linux 开启了内存大页会影响 Redis 的性能的。Linux 内核从 2.6.38 开始支持内存大页机制该机制支持 2MB 大小的内存页分配而常规的内存页分配是按 4KB 的粒度来执行的。如果采用了内存大页那么即使客户端请求只修改 100B 的数据在发生写时复制后Redis 也需要拷贝 2MB 的大页。相反如果是常规内存页机制只用拷贝 4KB。两者相比你可以看到每次写命令引起的复制内存页单位放大了 512 倍会拖慢写操作的执行时间最终导致 Redis 性能变慢。那该怎么办呢很简单关闭内存大页默认是关闭的。禁用方法如下echo never /sys/kernel/mm/transparent_hugepage/enabled总结当 AOF 写回策略配置了 Always 策略如果写入是一个大 Key主线程在执行 fsync() 函数的时候阻塞的时间会比较久因为当写入的数据量很大的时候数据同步到硬盘这个过程是很耗时的。AOF 重写机制和 RDB 快照bgsave 命令的过程都会分别通过 fork() 函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程主线程创建子进程的途中由于要复制父进程的页表等数据结构阻塞的时间跟页表的大小有关页表越大阻塞的时间也越长创建完子进程后如果父进程修改了共享数据中的大 Key就会发生写时复制这期间会拷贝物理内存由于大 Key 占用的物理内存会很大那么在复制物理内存这一过程就会比较耗时所以有可能会阻塞父进程。大 key 除了会影响持久化之外还会有以下的影响。客户端超时阻塞。由于 Redis 执行命令是单线程处理然后在操作大 key 时会比较耗时那么就会阻塞 Redis从客户端这一视角看就是很久很久都没有响应。引发网络阻塞。每次获取大 key 产生的网络流量较大如果一个 key 的大小是 1 MB每秒访问量为 1000那么每秒会产生 1000MB 的流量这对于普通千兆网卡的服务器来说是灾难性的。阻塞工作线程。如果使用 del 删除大 key 时会阻塞工作线程这样就没办法处理后续的命令。内存分布不均。集群模型在 slot 分片均匀情况下会出现数据和查询倾斜情况部分有大 key 的 Redis 节点占用内存多QPS 也会比较大。如何避免大 Key 呢最好在设计阶段就把大 key 拆分成一个一个小 key。或者定时检查 Redis 是否存在大 key 如果该大 key 是可以删除的不要使用 DEL 命令删除因为该命令删除过程会阻塞主线程而是用 unlink 命令Redis 4.0删除大 key因为该命令的删除过程是异步的不会阻塞主线程。

更多文章