Redis 为什么不会每次修改都写磁盘?
上一篇我们聊了《Redis 为什么要同时支持 RDB 和 AOF?》,了解了 Redis 在持久化设计上的取舍。
RDB 通过保存某个时间点的数据快照,让 Redis 可以快速恢复数据,但是两次快照之间的数据可能丢失。AOF 通过记录每一次写操作,让 Redis 尽可能减少数据丢失,但是随着运行时间增加,日志文件会越来越大。
所以 Redis 最终选择了两种方案结合。
但是 AOF 又带来了一个新的问题:
既然 AOF 可以记录每一次修改,那为什么 Redis 不直接在每一次修改之后,把数据同步到磁盘?
这样不是最安全的吗?
例如:
客户端执行 SET
↓
修改内存数据
↓
写入 AOF
↓
同步磁盘
↓
返回成功
如果按照这个流程执行,Redis 返回成功时,数据已经真正保存到了磁盘。
看起来这是一个完美方案。
但是 Redis 并没有这么设计。
原因是:
Redis 追求的并不是绝对的数据安全,而是在接近内存速度的情况下,提供足够可靠的数据恢复能力。
如果每一次修改都等待磁盘完成,Redis 最核心的优势反而会消失。
Redis 为什么不能每次修改都写磁盘?
Redis 快的原因,大家通常会想到:
"因为 Redis 使用内存。"
这个答案没有错,但是不完整。
真正决定 Redis 性能的,是它的大部分操作都发生在内存中。
例如执行:
SET user:name redis
Redis 需要做的事情非常简单:
接收命令
↓
解析命令
↓
修改内存数据结构
↓
返回结果
整个过程没有磁盘参与。
但是如果增加持久化:
接收命令
↓
修改内存数据结构
↓
写入磁盘
↓
等待磁盘完成
↓
返回结果
问题就出现了。
内存和磁盘的速度差距非常大。
CPU 访问内存可能只需要几十纳秒,而一次磁盘 IO,即使 SSD,也需要微秒甚至更长时间。
如果 Redis 每执行一次写操作,都必须等待磁盘完成,那么 Redis 的性能上限就不再由 CPU 和内存决定,而是由磁盘决定。
对于一个追求高并发的内存数据库来说,这是无法接受的。
那为什么不用 write 代替磁盘同步?
很多人可能会想到:
既然同步磁盘太慢,那只写文件不等待不就行了吗?
实际上,Redis 的 AOF 写入确实不会每次都直接等待磁盘。
因为 Linux 文件写入本身也有缓存机制。
整个过程大致如下:
Redis
|
| write()
↓
操作系统 Page Cache
|
| fsync()
↓
磁盘
Redis 调用 write 后,数据首先进入操作系统缓存。
例如:write(fd, buffer, size);
这个操作只是把数据交给操作系统。
此时数据并不一定已经保存到了磁盘。
如果机器突然断电:
Redis 内存
↓
丢失
操作系统缓存
↓
可能丢失
磁盘已保存数据
↓
保留
所以真正保证数据落盘,需要:
fsync(fd);
fsync 会要求操作系统把缓存中的数据同步到磁盘。
因此:
write 解决的是"写入文件"。
fsync 解决的是"真正保存"。
fsync 为什么不能频繁执行?
既然 fsync 才安全,那 Redis 为什么不每次执行?
因为 fsync 太慢。
假设 Redis 每秒处理 10 万次写请求。
如果每次请求:
write()
fsync()
那么意味着磁盘需要每秒完成大量同步写操作。
更严重的是,fsync 会阻塞当前线程。
Redis 早期核心命令执行模型是单线程:
客户端请求
↓
Redis 主线程
↓
执行命令
↓
返回结果
如果加入:
客户端请求
↓
Redis 主线程
↓
执行命令
↓
等待 fsync
↓
返回结果
那么一个磁盘慢操作,就会阻塞整个 Redis。
这也是 Redis 设计中非常重要的一点:
Redis 可以接受后台任务慢,但不能接受主线程慢。
因为主线程负责处理所有客户端请求。
Redis 如何解决这个问题?
Redis 的方案是:
不要让写磁盘阻塞命令执行。
也就是说:
数据修改和数据刷盘分开。
客户端请求:
执行命令
↓
修改内存
↓
写入 AOF 缓冲区
↓
返回成功
后台:
AOF 缓冲区
↓
后台任务
↓
fsync
↓
磁盘
这样 Redis 主线程可以继续处理新的请求。
这就是 Redis 持久化设计中的一个核心思想:
把影响性能的慢操作,从核心请求链路中移出去。
Redis 为什么选择 everysec?
既然刷盘放到了后台,那么新的问题来了:
多久刷一次?
Redis 提供了三个策略:
appendfsync always
appendfsync everysec
appendfsync no
它们代表不同的数据安全选择。
always:最高安全,但是牺牲性能
always 表示:
每一次写操作都立即 fsync。
流程:
执行命令
↓
写 AOF
↓
fsync
↓
返回成功
这种方式数据安全最高。
因为客户端收到成功响应时,数据已经保存到了磁盘。
但是性能最低。
因为每一个请求都可能等待磁盘。
对于高并发业务,这种方式基本无法发挥 Redis 的优势。
everysec:性能和安全的平衡
everysec 是 Redis 默认策略。
流程:
客户端请求
↓
修改内存
↓
写入 AOF
↓
返回成功
↓
后台每秒 fsync
它的核心思想:
允许极少量数据丢失,换取更高性能。
例如:
10:00:00
写入数据
10:00:01
后台刷盘
如果 Redis 在中间发生异常,最多丢失最近一秒的数据。
对于大部分互联网业务:
用户登录状态
热点数据
缓存信息
非核心计数
这种程度的数据损失通常可以接受。
所以 Redis 选择:
不是保证每一次操作绝对安全,而是保证大多数情况下数据可靠。
no:完全交给操作系统
no 模式:
Redis 只负责:
write()
什么时候同步磁盘,由操作系统决定。
这种方式性能最高。
但是风险也最高。
因为 Redis 无法控制:
操作系统什么时候刷盘
缓存中积累了多少未保存数据
所以生产环境通常不会使用。
AOF Rewrite:日志为什么不会无限增长?
解决刷盘问题之后,AOF 又出现了新的问题。
AOF 记录的是操作过程。
例如:
SET count 1
SET count 2
SET count 3
SET count 4
这些命令都会保存。
但是 Redis 最终状态:
count = 4
前面的:
SET count 1
SET count 2
SET count 3
实际上已经没有意义。
如果 Redis 长时间运行:
大量历史命令
↓
AOF 文件不断增长
↓
恢复时间越来越长
所以 Redis 引入了:
AOF Rewrite
Rewrite 并不是简单压缩文件,而是根据当前内存状态重新生成 AOF。
例如:
原来的:
SET count 1
SET count 2
SET count 3
SET count 4
重新生成:
SET count 4
恢复时只需要执行最终状态。
为什么 Rewrite 不会影响 Redis?
如果 Rewrite 在主线程执行:
Redis 主线程
↓
生成 AOF 文件
↓
无法处理请求
这显然不可接受。
所以 Redis 使用子进程完成 Rewrite。
流程:
Redis 主进程
继续处理客户端请求
↓
子进程生成新的 AOF
↓
期间新的写操作
进入重写缓冲区
↓
合并数据
↓
替换旧文件
Redis 再一次体现了自己的设计原则:
耗时操作异步化,核心路径保持简单。
Redis 为什么最终选择这种持久化方式?
如果 Redis 每次修改都立即写磁盘:
数据安全最高。
但是性能会明显下降。
如果 Redis 完全不考虑磁盘:
性能最高。
但是数据无法恢复。
所以 Redis 最终选择:
内存快速执行
↓
后台异步刷盘
↓
保证数据恢复能力
RDB 解决的是:
如何快速保存某个时间点的数据。
AOF 解决的是:
如何记录数据变化过程。
everysec 解决的是:
如何避免磁盘影响 Redis 性能。
Rewrite 解决的是:
如何控制 AOF 文件规模。
Redis 从来没有追求一个绝对完美的方案,而是在性能、可靠性和复杂度之间寻找一个工程上的平衡。
这也是 Redis 能够长期保持高性能,同时又具备持久化能力的原因。
上一篇:《Redis 为什么要同时支持 RDB 和 AOF?》
下一篇:《Redis 为什么会发生缓存穿透、击穿、雪崩?》