1 介绍

从上一篇的 《深刻理解高性能Redis的本质》 中可以知道, 我们经常在数据库层上加一层缓存(如Redis),来保证数据的访问效率。

这样性能确实也有了大幅度的提升,但是本身Redis也是一层服务,也存在宕机、故障的可能性。

一旦服务挂起,可能生产的后果包括如下几方面:

1、Redis的数据是存在内存中的,所以一旦挂起,内存中的数据会全部丢失。

2、I/O从内存层级迁移到磁盘层级,性能极速下降。

3、原本访问缓存的请求会透过缓存层直接投向数据库,给数据库带来极大的压力,甚至导致雪崩。

所以,缓存层崩溃产生的后果是灾难的。为了避免宕机和宕机后的数据丢失, 为了保证数据的快速恢复,Redis提供了两个持久化数据的能力, AOF(Append Only FIle)日志 和 RDB 快照。

2 关于RDB 内存快照

大规模高并发的分布式场景,经常会遇到问题就是Redis挂起,导致访问失败,而所有的请求透过缓存层投向数据库,给数据库造成极大的压力。

而Redis的数据是存储在高速缓存中,即使我们重启并且恢复使用,缓存池依旧是空的,因为内存被释放了。

重新建立缓存的过程,对数据库也是一个暴击的过程,很可能会导致整个系统调用链的雪崩。参考我的这篇《架构与思维:一次缓存雪崩的灾难复盘

我们知道,Redis 数据都是保存在内存中,能不能将内存中的数据进一步写到磁盘上,Redis 重启的时候就可以把磁盘上的数据快速恢复到内存中。这样,即使Redis宕机重启之后,依然能够正常的提供服务。

但是不能忽略一个问题,Redis和MySQL最大的区别之一就是一个存储在内存,一个持久化在磁盘。但是如果每次数据的变化(新增、修改、删除缓存)都要写内存并同时写磁盘,这样成本太高,内存+磁盘,会让 Redis 性能大大降低。而且还要保证原子性操作,避免内存和磁盘的数据不一致。

2.1 使用内存快照

为了避免实时写入高频操作磁盘带来的负面效应。Redis提供了内存快照策略。

我们知道,Redis 在 执行写(增、删、改)指令过程中,内存中数据会持续的在变化。而内存快照,指的是 Redis 内存中的数据在某一刻的状态。就好比如是拍照一样,你把那一刻的数据都定格下来,持久化到磁盘上。打游戏的同学可以想象存盘。

快照文件我们称之为 RDB 文件,即 Redis DataBase 的缩写。

Redis 通过定时执行 RDB 内存快照,这样就不必每次执行写指令都存盘,只需要在执行内存快照的时候写磁盘。这样既保证Redis的高效读写,还实现了定时持久化,宕机后可快速恢复数据。



如上图,在做数据恢复时,直接将 RDB 文件读入内存完成恢复。

2.2 生成RDB策略

Redis 提供了两种模式来生成 RDB 文件:

  • save: 由主线程来执行,同步阻塞,只有等save完成后,才能进行新操作;
  • bgsave:执行后,会立刻返回OK,同时调用 glibc 的函数fork产生一个子进程用于写入 RDB 文件,快照持久化完全交给子进程来处理。主进程继续执行他自己的工作,非阻塞。

2.2.1 save模式

save模式是主进程执行,非常不建议使用主进程执行的方式,在 《深刻理解高性能Redis的本质》 中,

我们知道他的主操作都是在单线程模型上完成的。所以尽量避免 RDB 文件生成影响主线程的网络I/O和键值对读写。

2.2.2 bgsave模式

上面提到的另外一种方式,fork一个子进程来写RDB文件。

Redis 使用操作系统的多进程写时复制技术 COW(Copy On Write) 来实现快照持久化,这个很重要,具体可以了解下这篇《Copy On Write机制》,写的不错。

Redis 在持久化时会调用 glibc 的函数fork产生一个子进程,由这个子进程来处理快照持久化的动作,子进程可以共享主进程的所有内存数据,所以它读取到主进程的数据之后写入到 RDB 文件。而父进程继续处理客户端的写操作,不受影响。

在创建 RDB 文件时,程序会对数据库中的键进行检查,仅仅将未过期的键保存到新创建的 RDB 文件中。

当主进程执行写指令修改数据的时候,这个数据就会复制一份副本, bgsave 子进程读取这个副本数据写到 RDB 文件,所以主进程就可以直接修改原来的数据。



这既保证了快照的完整性,也允许主进程同时对数据进行修改,避免了对正常业务的影响。

2.2.3 避免过频全量照片

虽然说Redis 使用 bgsave 函数 fork 子进程在后台完成 内存中的数据做快照,没有影响父进程继续处理客户端的各种操作。

但是需注意一点,过于频繁的执行全量的数据快照,必然会导致严重的性能开销:

  • 频繁生成 RDB 文件写入磁盘,磁盘压力过大,效率降低。
  • fork 出来的 bgsave 子进程因为共享主线程的数据,一定程度上会阻塞主线程的运行,主线程的内存越大,阻塞时间越长。

2.3 总结

  • 快照的恢复速度快,但是生成 RDB 文件的频率需要把握一个度,频率过低快照间隔数据较大,丢失的数据就会比较多;频率太快,又会消耗额外开销,降低Redis性能。
  • RDB 建议采用二进制 + 数据压缩的方式写磁盘,文件体积小,数据恢复速度快。

3 AOF 日志

AOF 日志存储了 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的指令记录。

假设 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列,那么就可以通过对一个空的 Redis 实例顺序执行所有的指令。

也就是说,可以通过重放(replay),来建立 Redis 当前实例的内存数据结构。这种模式有没有很熟悉,有没有想到MySQL主从同步时候的relay log。

3.1 日志变更前后对比

AOF记录日志有两种模式,一种是预写式日志,也称写前日志(Write Ahead Log, WAL): 在实际写数据之前,将修改的数据写到日志文件中。

另外一种是写后日志: 先执行写操作,当数据存入内存后,再记录日志。

预写式日志类似 MySQL Innodb 引擎 中的 redo log,修改数据前先记录日志,再修改。

3.2 日志格式

Redis 接收到 set keyName someValue 命令的时候,会先将数据写到内存,Redis 会按照如下格式写入 AOF 文件。

*3:表示当前指令分为三个部分,每个部分都是 $ + 数字开头,后面是3部分的具体内容:指令、键、值。

数字:表示这部分的命令、键、值多占用的字节大小。比如 $3表示这部分包含 3 个字符,也就是 set 的长度。

推荐使用写后日志的模式,避免了额外的检查开销,不需要对执行的命令进行语法检查。如果使用写前日志的话,就需要先检查语法是否有误,否则日志记录了错误的命令,在使用日志恢复的时候就会出错。另外,写后才记录日志,不会阻塞当前的 指令执行。

# set keyName someValue
*3
$3
set
$7 #长度为7
keyName
$9 #长度为9
someValue # 执行 mset key1 1 ,key2 2 ,key33 3
# aof日志如下:
*7 # 本批命令需要往下读7行非 $ 开始的命令
$4 #接着读取4个字节宽度,‘mset’长度为4,记为 $4
mset
$4 #接着读取4个字节宽度,‘key1’长度为4,记为 $4
key1
$1 #接着读取1个字节宽度,‘1’长度为1,记为 $1
1
$4
key2
$1
2
$5 #接着读取的字节宽度,‘$key33’长度为5,记为 $5
key33
$1
3

3.3 可能存在的问题

  • 可能存在丢失:比如Redis 刚执行完指令,还没记录日志宕机了,命令数据就丢了。
  • AOF 避免了当前命令的阻塞,但是AOF 日志是主线程执行,将日志写入磁盘过程中,如果磁盘压力大就会导致执行变慢,降低后续的操作。

3.4 写回策略

上面的问题,在Redis高频读写的时候是必然存在的,想要解决,在写入的时候做一层缓冲就可以了,避免直塞。这时候Redis提供了一种执行策略叫写回策略。

3.4.1 写回策略说明

为了提高日志文件的写入效率,写回策略会做如下变化:

  • 当你调用 write 函数将数据写入到文件时,这时候不是真正的落盘,而是将写入数据暂存在操作系统的内存缓冲区里。
  • 待到缓冲区的空间被填满、或者超过了指定的阈值时候,才真正地将缓冲区中的数据写入到磁盘里面。

    这种做法显然提高了效率,但也为写入数据带来了安全性问题,如果服务器发生了单机,那么保存在内存缓冲区里面的写入数据就会丢失。

    为此,系统提供了fsyncfdatasync两个同步函数,它们可以强制让操作系统立即将缓冲区中的数据写入到硬盘里面,从而确保写入数据的安全性。

    Redis 提供的 AOF 配置项 appendfsync 写回策略直接决定 AOF 持久化功能的效率和安全性,以下是 appendfsync 的3个枚举:
  • always:同步写回,写指令执行完 即将缓冲区内容回写到 AOF 文件。
  • everysec:每秒写回,写指令执行完,日志写到 AOF 文件缓冲区,缓冲区每隔一秒再把内容同步到磁盘。
  • no: 操作系统控制,写执行执行完毕,把日志写到 AOF 文件内存缓冲区,由操作系统决定何时回写到磁盘。

写磁盘会带来性能上的损耗,所以写回的策略要根据实际情况做一个取舍,比如你是偏向性能还是可靠性。

always 同步写回可以做到数据不丢失,但是每次执行写指令都需要写入磁盘,性能最差。

everysec 每秒写回,避免了同步写回的性能开销,但是如果服务发生宕机,会有大约1s时间周期的数据丢失,这种模式是在性能和可靠性之间做了妥协。

no 操作系统控制,执行写指令后就写入 AOF 文件缓冲,再执行后续的写磁盘指令,性能最好,但有可能丢失更多的数据。

3.4.2 写回策略的选择

我们可以根据服务的实际情况来抉择策略,看是偏向高性能还是高可靠。

  • 高性能需求,选择 No 策略
  • 高可靠性保证,就选择 Always 策略
  • 如果能够接受数据存在少量丢失,又希望性能较好的话,就选择 Everysec 策略

4 混合RDF/AOF 方式模式

现实情况下,无论使用RDB或者AOF都差点意思。使用 rdb 来恢复内存状态,势必会丢失一部分数据。 使用 AOF 日志重放,重放对性能有一定的影响,而且在 Redis 实例很大的情况下,需要花费很长的时间。

Redis 4.0 解决了这个问题,才用了一个新的持久化模式——混合持久化,该 混合模式 默认是关闭状态的。

将 RDB 文件的内容和 rdb快照时间点之后的增量的 AOF 日志文件存在一起。这时候 AOF 日志不需要再是全量的日志,而是最近一次快照时间点之后到当下发生的增量 AOF 日志,通常这部分 AOF 日志很小。

所以执行有如下顺序:

  • 查找rdb内容,如果存在先加载 rdb内容再 重放剩余的 aof。
  • 没有rdb内容,直接以aof格式重放整个文件。

    这样快照就不用频繁的执行,同时由于 AOF 只需要记录最近一次快照之后的数据,不需要记录所有的操作,避免了出现单次重放文件过大的问题。

5 总结

  • RDB提供了快照模式,记录某个时间的Redis内存状态。RDB设计了 bgsave 和写时复制,尽可能避免执行快照期间对读写指令的影响,但是频繁快照会给磁盘带来压力以及 fork 阻塞主线程。需把握频率。
  • AOF 日志存储了 Redis 服务的顺序指令序列,通过重放(replay)指令来写入日志文件,并通过写回策略来避免高频读写给Redis带来压力。
  • RDB快照的照片时间间隔,必然会带来数据缺失,如果允许分钟级别的数据丢失,可以只使用 RDB。
  • 如果只用 AOF,写回策略优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个平衡。
  • 数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择。

Redis系列2:数据持久化提高可用性的更多相关文章

  1. Redis进阶:数据持久化,安全,在PHP中使用

    一.redis数据持久化 由于redis是一个内存数据库,如果系统遇到致命问题需要关机或重启,内存中的数据就会丢失,这是生产环境所不能允许的.所以redis提供了数据持久化的能力. redis提供了两 ...

  2. Docker深入浅出系列 | 容器数据持久化

    Docker深入浅出系列 | 容器数据持久化 Docker已经上市很多年,不是什么新鲜事物了,很多企业或者开发同学以前也不多不少有所接触,但是有实操经验的人不多,本系列教程主要偏重实战,尽量讲干货,会 ...

  3. Redis系列文章-数据结构篇

    Redis系列文章 前言: 工作原因,在学习mybatis知识后,2个月没有补充新的知识了,最近拿起书本开始学习.打算写下这个Redis系列的文章. 目录结构如下: Redis内置数据结构 Redis ...

  4. Redis系列三之持久化

    一.Redis持久化 Redis是一个支持持久化的内存数据库,redis需要经常将内存中的数据同步到磁盘来保证持久化. redis提供了不同级别的持久化方法: Snapshotting(快照,默认方式 ...

  5. Redis 中的数据持久化策略(RDB)

    Redis 是一个内存数据库,所有的数据都直接保存在内存中,那么,一旦 Redis 进程异常退出,或服务器本身异常宕机,我们存储在 Redis 中的数据就凭空消失,再也找不到了. Redis 作为一个 ...

  6. Redis 中的数据持久化策略(AOF)

    上一篇文章,我们讲的是 Redis 的一种基于内存快照的持久化存储策略 RDB,本质上他就是让 redis fork 出一个子进程遍历我们所有数据库中的字典,进行磁盘文件的写入. 但其实这种方式是有缺 ...

  7. docker 系列 - 容器数据持久化和数据共享

    docker 主要有两种数据存储形式, 一种是storage driver(也叫做 Graph driver), 另一种是 volume driver. stroage driver主要是存储那些无状 ...

  8. IdentityServer4系列 | 支持数据持久化

    一.前言 在前面的篇章介绍中,一些基础配置如API资源.客户端资源等数据以及使用过程中发放的令牌等操作数据,我们都是通过将操作数据和配置数据存储在内存中进行实现的,而在实际开发生产中,我们需要考虑如何 ...

  9. redis系列:RDB持久化与AOF持久化

    前言 什么是持久化? 持久化(Persistence),即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘).持久化的主要应用是将内存中的对象存储在数据库中,或者存储在磁盘文件中.XML数 ...

随机推荐

  1. BUCK电路和BL8033使用记录

    我开始时用的是MPS的MP1584EN,这个芯片各方面都挺不错的.有次老大让我画个降压电路,看到用的是MP1584就说国外的芯片太贵,让我用上海贝岭的BL8033.BL8033确实很好用,虽然它没有1 ...

  2. 原生微信小程序里类似于计算属性写法

    可直接在wxml文件里直接写入直接调用.变量只支持var命名,不支持let const     </view>     <view class="wx_bgc"  ...

  3. Codeforces Round #741 (Div. 2), problem: (D1) Two Hundred Twenty One (easy version), 1700

    Problem - D1 - Codeforces 题意: 给n个符号(+或-), +代表+1, -代表-1, 求最少删去几个点, 使得     题解(仅此个人理解): 1. 这题打眼一看, 肯定和奇 ...

  4. 用于激光雷达的 APD,SPAD 和 SiPM 分析

    1. 术语及定义 1.1 激光雷达,Light Detection And Range, LiDAR  发射激光光束,并接收回波以获取目标三维和/或速度信息的系统: 1.2 机械旋转激光雷达,Mech ...

  5. Blazor使用区域(Area)

    在MVC中,我们经常使用区域(Area)来区分各个模块,比如后台我们可以写一个Admin的Area. 到了Blazor时代,已经不推荐这么做了,现在推荐的做法是通过Url来区分,比如Admin可以配置 ...

  6. 听说Integer有bug?1000不等于1000?

    bug? 前几天有位朋友找我,说:"老哥,老哥,我好像发现了Integer一个bug,你帮我看看什么情况?",说完给了我两个很简单的demo,上代码. 100 == 100 100 ...

  7. 国产芯片DP9637-K总线收发器替代L9637D芯片和SI9241

    DP9637可以替代L9637D,低成本解决方案,只需要做简单硬件修改,感兴趣可以留言或者联系小编了解详细资料.   主要特性    电压工作范围 6V≤VBAT≤36V    具有超低休眠电流 ...

  8. 大一/初学者学C语言前必看!!!(建议收藏)

    目录 数据类型 常量.变量 数组 字符串.转义字符 选择语句 循环语句 函数 操作符 结构体 指针 神秘的学习资料基地​jq.qq.com/?_wv=1027&k=5kWJsY1z 一.数据类 ...

  9. vue package.json 详解

    示例: { "name": "scrm", "version": "0.1.0", "private" ...

  10. Flume 详解&实战

    Flume 1. 概述 Flume是一个高可用,高可靠,分布式的海量日志采集.聚合和传输的系统.Flume基于流式架构,灵活简单. Flume的作用 Flume最主要的作用就是,实时读取服务器本地磁盘 ...