CSDN话题挑战赛第2期
参赛话题:一起学Java
♨️本篇文章记录的为Redis知识中
缓存穿透,缓存击穿,缓存雪崩,缓存预热
中秒杀相关内容,适合在学Java的小白,帮助新手快速上手,也适合复习中,面试中的大佬🙉🙉🙉。
♨️如果文章有什么需要改进的地方还请大佬不吝赐教❤️🧡💛
最近在工作之余重新学习了redis的持久化机制,有了比之前更深刻的看法与见解,并对此进行了分析与总结,供各位朋友参考也希望与各位大佬探讨
@[toc]
一、redis 持久化
1. 简介
1.1. 为什么需要持久化?
为了防止数据丢失以及服务重启时能够恢复数据,Redis 支持数据的持久化,主要分为两种方式,分别是 RDB 和 AOF;
那么实际场景该如何实现呢?
二、持久化方式
2. RDB 持久化
RDB 就是 Redis DataBase 的缩写,中文名为 快照 / 内存快照 ,RDB
持久化是把当前进程数据生成快照保存到磁盘上的过程,由于是某一时刻的快照,那么快照中的值要早于或者等于内存中的值。2.1. 触发方式
2.1.1.触发 rdb 持久化的方式有 2 种,分别是 手动触发 和 自动触发
2.1.1. 手动触发
手动触发分别对应 save 和 bgsave 命令
save 命令:阻塞当前 Redis 服务器,直到 RDB 过程完成为止,对于内存 比较大的实例会造成长时间阻塞,线上环境不建议使用
bgsave 命令:Redis 进程执行 fork 操作创建子进程,RDB 持久化过程由子进程负责,完成后自动结束。阻塞只发生在 fork 阶段,一般时间很短
2.2. bgsave 的基本流程?
- redis 客户端执行 bgsave 命令或者自动触发 bgsave 命令;
- 主进程判断当前是否已经存在正在执行的子进程,如果存在,那么主进程直接返回;
- 如果不存在正在执行的子进程,那么就 fork 一个新的子进程进行持久化数据,fork 过程是阻塞的,fork 操作完成后主进程即可执行其他操作,子进程与主进程共享内存空间;
- 子进程先将数据写入到临时的 rdb 文件中,待快照数据写入完成后再原子替换旧的 rdb 文件;
- 同时发送信号给主进程,通知主进程 rdb 持久化完成,主进程更新相关的统计信息(info Persitence 下的 rdb_* 相关选项)。
2.2.1 自动触发
在以下 4 种情况时会自动触发
- redis.conf 中配置 save m n,即在 m 秒内有 n 次修改时,自动触发 bgsave 生成 rdb 文件;
- 主从复制时,从节点要从主节点进行 全量复制时 也会触发 bgsave 操作,生成当时的快照发送到从节点;
- 执行 debug reload 命令重新加载 redis 时也会触发 bgsave 操作;
- 默认情况下执行 shutdown 命令时,如果没有开启 aof 持久化,那么也会触发 bgsave 操作;
2.3 redis.conf 中配置 RDB
快照周期 :内存快照虽然可以通过技术人员手动执行 SAVE 或 BGSAVE 命令来进行,但生产环境下多数情况都会设置其周期性执行条件。
2.3.1 Redis 中默认的周期性设置
# Save the DB to disk.
#
# save <seconds> <changes>
#
# Redis will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
#
# Snapshotting can be completely disabled with a single empty string argument
# as in following example:
#
# 关闭 RDB
# save ""
#
# Unless specified otherwise, by default Redis will save the DB:
# * After 3600 seconds (an hour) if at least 1 key changed
# * After 300 seconds (5 minutes) if at least 100 keys changed
# * After 60 seconds if at least 10000 keys changed
#
# You can set these explicitly by uncommenting the three following lines.
# 如果 900秒 内有 1 条Key信息发生变化,则进行快照;
save 3600 1
# 如果 300 秒内有 10 条Key信息发生变化,则进行快照;
save 300 100
# 如果 60 秒内有 10000 条 Key 信息发生变化,则进行快照。
save 60 10000
# save 5 1
其它相关配置
# 文件名称
dbfilename dump.rdb
# 文件保存路径
dir /home/work/app/redis/data/
# 如果持久化出错,主进程是否停止写入
stop-writes-on-bgsave-error yes
# 是否压缩
rdbcompression yes
# 导入时是否检查
rdbchecksum yes
stop-writes-on-bgsave-error:上文提到的在快照进行过程中,主进程照样可以接受客户端的任何写操作的特性,是指在快照操作正常的情况下。如果快照操作出现异常(例如操作系统用户权限不够、磁盘空间写满等等)时,Redis 就会禁止写操作。这个特性的主要目的是使运维人员在第一时间就发现 Redis的运行错误,并进行解决。一些特定的场景下,您可能需要对这个特性进行配置,这时就可以调整这个参数项。该参数项默认情况下值为yes,如果要关闭这个特性,指定即使出现快照错误 Redis 一样允许写操作,则可以将该值更改为 no
rdbcompression:该属性将在字符串类型的数据被快照到磁盘文件时,启用 LZF 压缩算法。Redis 官方的建议是请保持该选项设置为 yes,因为 “it’s almost always a win”。
rdbchecksum:从 RDB 快照功能的 version 5 版本开始,一个 64 位的 CRC 冗余校验编码会被放置在 RDB文件的末尾,以便对整个 RDB 文件的完整性进行验证。这个功能大概会多损失 10% 左右的性能,但获得了更高的数据可靠性。所以如果您的Redis 服务需要追求极致的性能,就可以将这个选项设置为 no。
2.4. RDB 更深入理解
由于生产环境中我们为 Redis 开辟的内存区域都比较大(例如6GB),那么将内存中的数据同步到硬盘的过程可能就会持续比较长的时间,而实际情况是这段时间 Redis 服务一般都会收到数据写操作请求。那么如何保证数据一致性呢?
RDB 中的核心思路是 Copy-on-Write,来保证在进行快照操作的这段时间,需要压缩写入磁盘上的数据在内存中不会发生变化。在正常的快照操作中,一方面 Redis 主进程会 fork 一个新的快照进程专门来做这个事情,这样保证了 Redis 服务不会停止对客户端包括写请求在内的任何响应。另一方面这段时间发生的数据变化会以副本的方式存放在另一个新的内存区域,待快照操作结束后才会同步到原来的内存区域 。
在进行快照操作的这段时间,如果发生服务崩溃怎么办?
很简单,在没有将数据全部写入到磁盘前,这次快照操作都不算成功。如果出现了服务崩溃的情况,将以上一次完整的 RDB 快照文件作为恢复内存数据的参考。 在快照操作过程中不能影响上一次的备份数据。Redis 服务会在磁盘上创建一个临时文件进行数据操作,待操作成功后才会用这个临时文件替换掉上一次的备份。
可以每秒做一次快照吗?
如果频繁地执行全量快照,也会带来两方面的开销:
- 一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。
- 另一方面,bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了。
那么,有什么其他好方法吗?此时,我们可以做增量快照,也就是 AOF持久化,是指做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。这个比较好理解。
2.5. RDB 优缺点
- 优点
- RDB 文件是某个时间节点的快照,默认使用 LZF 算法进行压缩,压缩后的文件体积远远小于内存大小,适用于备份、全量复制等场景;(体积小)
- Redis 加载 RDB 文件恢复数据要远远快于 AOF 方式;(速度快)
- RDB 文件是某个时间节点的快照,默认使用 LZF 算法进行压缩,压缩后的文件体积远远小于内存大小,适用于备份、全量复制等场景;(体积小)
- 缺点
- RDB 方式实时性不够,无法做到秒级的持久化;(实时性不够)
- 每次调用 bgsave 都需要 fork 子进程,fork 子进程属于重量级操作,频繁执行成本较高;(fork 成本高)
- RDB 文件是二进制的,没有可读性,AOF 文件在了解其结构的情况下可以手动修改或者补全;(可读性不高)
- 版本兼容 RDB 文件问题;(不兼容)
针对 RDB 不适合实时持久化的问题,Redis 提供了 AOF 持久化方式来解决
3. AOF 持久化
AOF 全称为 Append Only File(追加文件)。Redis 处理的每一个写命令都会记录在 AOF 文件,可以看做是命令日志文件。
AOF 日志采用写后日志,即先写内存,后写日志。
- 避免额外的检查开销:Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。
- 不会阻塞当前的写操作
但这种方式存在潜在风险:
如果命令执行完成,写日志之前宕机了,会丢失数据。
主线程写磁盘压力大,导致写盘慢,阻塞后续操作。
3.1. 如何实现 AOF
AOF 日志记录 Redis 的每个写命令,步骤分为:命令追加(append)、文件写入(write)和文件同步(sync)。
命令追加 当 AOF 持久化功能打开了,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器的 aof_buf 缓冲区。
文件写入和同步 关于何时将 aof_buf 缓冲区的内容写入 AOF 文件中,Redis 提供了三种写回策略:
# Redis supports three different modes:
#
# no: don't fsync, just let the OS flush the data when it wants. Faster.
# always: fsync after every write to the append only log. Slow, Safest.
# everysec: fsync only one time every second. Compromise.
#
# The default is "everysec", as that's usually the right compromise between
# speed and data safety. It's up to you to understand if you can relax this to
# "no" that will let the operating system flush the output buffer when
# it wants, for better performances (but if you can live with the idea of
# some data loss consider the default persistence mode that's snapshotting),
# or on the contrary, use "always" that's very slow but a bit safer than
# everysec.
#
# More details please check the following article:
# http://antirez.com/post/redis-persistence-demystified.html
#
# If unsure, use "everysec".
# appendfsync always
appendfsync everysec
# appendfsync no
Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
3.2. redis.conf 中 配置 AOF
默认情况下,Redis 是没有开启 AOF 的,可以通过配置 redis.conf 文件来开启 AOF 持久化,关于 AOF 的配置如下
# appendonly参数开启AOF持久化
appendonly no
# AOF持久化的文件名,默认是appendonly.aof
appendfilename "appendonly.aof"
# AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的
dir ./
# 同步策略
# appendfsync always
appendfsync everysec
# appendfsync no
# aof重写期间是否同步
no-appendfsync-on-rewrite no
# 重写触发配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 加载 aof出错如何处理
aof-load-truncated yes
# 文件重写策略
aof-rewrite-incremental-fsync yes
no-appendfsync-on-rewrite:always 和 everysec 的设置会使真正的 I/O 操作高频度的出现,甚至会出现长时间的卡顿情况,这个问题出现在操作系统层面上,所有靠工作在操作系统之上的 Redis 是没法解决的。为了尽量缓解这个情况,Redis 提供了这个设置项,保证在完成 fsync 函数调用时,不会将这段时间内发生的命令操作放入操作系统的 Page Cache(这段时间 Redis 还在接受客户端的各种写操作命令)。
aof-load-truncated :与载入 RDB 文件类似,Redis 载入 AOF 文件时,会对 AOF 文件进行校验,如果文件损坏,则日志中会打印错误,Redis 启动失败。但如果是 AOF 文件结尾不完整 (机器突然宕机等容易导致文件尾部不完整),且 aof-load-truncated 参数开启,则日志中会输出警告,Redis 忽略掉 AOF 文件的尾部,启动成功
3.3. 深入理解 AOF 重写
AOF 会记录每个写命令到 AOF 文件,随着时间越来越长,AOF 文件会变得越来越大。如果不加以控制,会对 Redis服务器,甚至对操作系统造成影响,而且 AOF 文件越大,数据恢复也越慢。为了解决 AOF 文件体积膨胀的问题,Redis 提供 AOF 文件重写机制来对 AOF 文件进行 “瘦身”。
Redis 通过创建一个新的 AOF 文件来替换现有的 AOF,新旧两个 AOF 文件保存的数据相同,但新 AOF 文件没有了冗余命令。
AOF 重写会阻塞吗?
AOF 重写过程是由后台进程 bgrewriteaof 来完成的。主线程 fork 出后台的 bgrewriteaof 子进程,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
所以 aof 在重写时,在 fork 进程时是会阻塞住主线程的。
AOF 日志何时会重写?
有两个配置项控制 AOF 重写的触发:
auto-aof-rewrite-min-size: 表示运行 AOF 重写时文件的最小大小,默认为 64MB。
auto-aof-rewrite-percentage: 这个值的计算方式是,当前 aof 文件大小和上一次重写后 aof文件大小的差值,再除以上一次重写后 aof 文件大小。也就是当前 aof 文件比上一次重写后 aof 文件的增量大小,和上一次重写后 aof 文件大小的比值。
重写日志时,有新数据写入,主进程是怎么做的?
重写过程总结为:“一个拷贝,两处日志”。在 fork 出子进程时的拷贝,以及在重写时,如果有新数据写入,主线程就会将命令记录到两个 aof 日志内存缓冲区中。如果 AOF 同步策略配置的是 always,则直接将命令写回 旧的日志文件 ,并且保存一份命令至 AOF重写缓冲区 ,这些操作对新的日志文件是不存在影响的。(旧的日志文件:主线程使用的日志文件,新的日志文件:bgrewriteaof 进程使用的日志文件)
而在 bgrewriteaof 子进程完成会日志文件的重写操作后,会提示主线程已经完成重写操作,主线程会将 AOF重写缓冲中的命令追加到新的日志文件后面。这时候在高并发的情况下,AOF 重写缓冲区积累可能会很大,这样就会造成阻塞,Redis 后来通过 Linux 管道技术让 aof 重写期间就能同时进行回放,这样 aof 重写结束后只需回放少量剩余的数据即可。
最后通过修改文件名的方式,保证文件切换的原子性。
在重写日志整个过程时,主线程有哪些地方会被阻塞?
- fork 子进程时,需要拷贝虚拟页表,会对主线程阻塞 。
- 主进程有 bigkey 写入时,操作系统会创建页面的副本,并拷贝原有的数据,会对主线程阻塞。
- 子进程重写日志完成后,主进程追加 aof 重写缓冲区时可能会对主线程阻塞。
4. 从持久化中恢复数据
其实想要从这些文件中恢复数据,只需要重新启动 Redis 即可。
- redis 重启时判断是否开启 aof,如果开启了 aof,那么就优先加载 aof 文件;
- 如果 aof 存在,那么就去加载 aof 文件,加载成功的话 redis 重启成功,如果 aof 文件加载失败,那么会打印日志表示启动失败,此时可以去修复 aof 文件后重新启动;
- 若 aof 文件不存在,那么 redis 就会转而去加载 rdb 文件,如果 rdb 文件不存在,redis 直接启动成功;
- 如果 rdb 文件存在就会去加载 rdb 文件恢复数据,如加载失败则打印日志提示启动失败,如加载成功,那么 redis 重启成功,且使用 rdb 文件恢复数据;
那么为什么会优先加载 AOF 呢?因为 AOF 保存的数据更完整,通过上面的分析我们知道 AOF 基本上最多损失 1s 的数据。
三、redis性能与实践
通过上面的分析,我们都知道 RDB 的快照、AOF 的重写都需要 fork,这是一个重量级操作,会对 Redis 造成阻塞。因此为了不影响 Redis 主进程响应,我们需要尽可能降低阻塞。
- 降低 fork 的频率,比如可以手动来触发 RDB 生成快照、与 AOF 重写;
- 控制 Redis 最大使用内存,防止 fork 耗时过长;
- 使用 更强的硬件;
- 合理配置 Linux 的内存分配策略,避免因为物理内存不足导致 fork 失败。
在线上我们到底该怎么做?这里提供了一些的实践经验。
- 如果 Redis 中的数据并不是特别敏感或者可以通过其它方式重写生成数据, 可以关闭持久化,如果丢失数据可以通过其它途径补回;
- 自己制定策略定期检查 Redis 的情况,然后可以手动触发备份、重写数据;
- 单机如果部署多个实例,要防止多个机器同时运行持久化、重写操作,防止出现内存、CPU、IO 资源竞争,让持久化变为串行;
- 可以加入 主从机器,利用一台从机器进行备份处理,其它机器正常响应客户端的命令;
- RDB 持久化与 AOF 持久化可以同时存在,配合使用
总结:
以上就是对这次redis持久化机制再次学习的总结与分享,希望能够帮助大家解决一些实际开发中的问题,如果各位大佬有其他更好的看法与观点,欢迎评论区留言和私信
如果这篇【文章】有帮助到你💖,希望可以给我点个赞👍,创作不易,如果有对Java后端或者对redis感兴趣的朋友,请多多关注💖💖💖
👨🔧个人主页:阿千弟
如果大家对redis相关知识感兴趣请点击这里👉👉👉redis专栏学习