4.redis持久化(上)

137 阅读9分钟

Redis 是一个内存数据库,所以其运行效率非常高。但也存在一个问题:内存中的数据是不持久的,若主机宕机或 Redis 关机重启,则内存中的数据全部丢失。当然,这是不允许的。Redis 具有持久化功能,其会按照设置以快照操作日志的形式将数据持久化到磁盘。

根据持久化使用技术的不同,Redis 的持久化分为两种:RDB 与 AOF。

4.1 持久化基本原理

image.png Redis 持久化也称为钝化,是指将内存中数据库的状态描述信息保存到磁盘中。只不过不同的持久化技术,对数据的状态描述信息是不同的,生成的持久化文件也是不同的。但 它们的作用都是相同的:避免数据意外丢失。

通过手动/自动定时/自动条件触发方式,将内存中数据库的状态描述信息写入到指定的持久化文件中。当系统重启时,自动加载持久化文件,并根据文件中数据库状态描述信息将数据恢复到内存中,这个过程也称为激活。钝化与激活的过程就是 Redis 持久化的基本原理。

不过从以上分析可知,对于 Redis 单机状态下,无论哪种方式,都存在数据丢失问题:在尚未手动/自动保存时发生了 Redis 宕机状况,那么从上次保存到宕机期间产生的数据就会丢失。不同的持久化方式,其数据的丢失率也是不同的。 image.png RDB 是默认持久化方式,但 Redis 允许 RDB 与 AOF 两种持久化技术同时开启,此时系统会使用 AOF 方式做持久化,即 AOF 持久化技术的优先级要更高。同样的道理,两种技术同时开启状态下,系统启动时若两种持久化文件同时存在,则优先加载 AOF 持久化文件。

4.2 RDB 持久化

RDB,Redis DataBase,是指将内存中某一时刻的数据快照全量写入到指定的 rdb 文件的持久化技术。RDB 持久化默认是开启的。当 Redis 启动时会自动读取 RDB 快照文件,将数据从硬盘载入到内存,以恢复 Redis 关机前的数据库状态。

4.2.1 持久化的执行

(1) 手动 save 命令

通过在 redis-cli 客户端中执行 save 命令可立即进行一次持久化保存。save 命令在执行期间会阻塞 redis-server 进程,直至持久化过程完毕。而在 redis-server 进程阻塞期间,Redis 不能处理任何读写请求,无法对外提供服务。

(2) 手动 bgsave 命令

通过在 redis-cli 客户端中执行 bgsave 命令可立即进行一次持久化保存。不同于 save 命令的是,该命令后台运行 save。bgsave 会使服务器 进程 redis-server 生成一个子进程,由该子进程负责完成保存过程。在子进程进行保存过程中,不会阻塞 redis-server 进程对客户端读写请求的处理

(3) 自动条件触发

本质仍是 bgsave 命令的执行。只不过是用户通过在配置文件中做相应的设置后,Redis 会根据设置信息自动调用 bgsave 命令执行。具体配置方式,后面会详解。

(4) 查看持久化时间

通过 lastsave 命令可以查看最近一次执行持久化的时间,其返回的是一个 Unix 时间戳。

4.2.2 RDB 优化配置

RDB 相关的配置在 redis.conf 文件的 SNAPSHOTTING 部分。 image.png

(1) save image.png 该配置用于设置快照的自动保存触发条件,即 save point。触发条件是在指定时间段内发生了指定次数的写操作。除非另有规定,默认情况下持久化条件为 save 3600 1 300 100 60 10000。其等价于以下三条:

  • save 3600 1 # 在 3600 秒(1 小时)内发生 1 次写操作
  • save 300 100 # 在 300 秒(5 分钟)内发生 100 次写操作
  • save 60 10000 # 在 60 秒(1 分钟)内发生 1 万次写操作

如果不启用 RDB 持久化,只需设置 save 的参数为空串即可:save “”。

(2) stop-write-on-bgsave-error image.png 默认情况下,如果 RDB 快照已启用(至少一个保存点),且最近的 bgsave 命令失败,Redis 将停止接受写入。这样设置是为了让用户意识到数据没有正确地保存到磁盘上,否则很可能发生一些灾难。如果 bgsave 命令后来可以正常工作了,Redis 将自动允许再次写入。

(3) rdbcompression

当进行持久化时启用 LZF 压缩字符串对象。虽然压缩 RDB 文件会消耗系统资源,降低性能,但可大幅降低文件的大小,方便保存到磁盘,加速主从集群中从节点的数据同步。

(4) rdbchecksum

从 RDB5 开始,RDB 文件的 CRC64 校验和被放置在文件末尾。这使格式更能抵抗 RDB 文件的损坏,但在保存和加载 RDB 文件时,性能会受到影响(约 10%),可以设置为 no 禁用校验和以获得最大性能。在禁用校验和的情况下创建的 RDB 文件的校验和为零,这将告诉加载代码跳过校验检查。默认为 yes,开启校验功能。

(5) sanitize-dump-payload

该配置用于设置在加载 RDB 文件或进行持久化时是否开启对 zipList、listPack 等数据的全面安全检测。该检测可以降低命令处理时发生系统崩溃的可能。其可设置的值有三种选择:

  • clients:只有当客户端连接时检测。排除了加载 RDB 文件与进行持久化时的检测。
  • yes/no:总是/不检测

默认值本应该是 clients,但其会影响 Redis 集群的工作,所以默认值为 no,不检测。

(6) dbfilename

指定 RDB 文件的默认名称,默认为 dump.rdb。

(7) rdb-del-sync-files

主从复制时,是否删除用于同步的从机上的 RDB 文件。默认是 no。只有当从机的 RDB 和 AOF 持久化功能都未开启时才生效。

(8) dir

指定 RDB 与 AOF 文件的生成目录。默认为 Redis 安装根目录。

4.2.3 RDB 文件结构

RDB 持久化文件 dump.rdb 整体上有五部分构成: image.png (1) SOF

常量,一个字符串,"REDIS"。用于标识 RDB 文件的开始,以便在加载 RDB 文件时可以迅速判断出文件是否是 RDB 文件。

(2) rdb_version

整数,4 字节,表示 RDB 文件的版本号。

(3) EOF

常量,1 个字节,标识 RDB 数据的结束,校验和的开始。

(4) check_sum

校验和 check_sum 用于判断 RDB 文件中的内容是否出现数据异常。采用 CRC 校验算法。

CRC 校验算法: 在持久化时,先将 SOF、rdb_version 及内存数据库中的数据快照这三者的二进制数据拼接起来,形成一个二进制数a,再使用这个 a 除以校验和 check_sum,此时可获取到一个余数 b,再将 b 拼接到 a 的后面,形成 databases。 在加载时,需要先使用 check_sum 对 RDB 文件进行数据损坏验证。验证过程:只需将 RDB 文件中除 EOF 与 check_sum 外的数据除以 check_sum。只要除得的余数不是 0,就说明文件发生损坏。如果余数是 0,也不能肯定文件没有损坏。

这种验证算法,是数据损坏校验,而不是数据没有损坏的校验。

(5) databases

数据部分,其可以包含任意多个非空数据库。每个 database 由三部分构成:

  • SODB:常量,1 字节,标识一个数据库的开始。
  • db_number:数据库编号。
  • key_value_pairs:当前数据库中的键值对数据。

image.png 每个 key_value_pairs 又由多个用于描述键值对的数据构成。

  • VALUE_TYPE:常量,1 字节,标识该键值对中 value 的类型。
  • EXPIRETIME_UNIT:常量,1字节,标识过期时间的单位是秒还是毫秒。
  • time:当前 key-value 的过期时间。
4.2.4 RDB 持久化过程

image.png

对于 Redis 默认的 RDB 持久化,在进行 bgsave 持久化时,redis-server 进程会 fork 一个 bgsave 子进程,由该子进程以异步方式负责完成持久化。而在持久化过程中,redis-server 进程会继续接收并处理用户的读写请求。

bgsave 子进程的详细工作原理如下:

由于子进程可以继承父进程的所有资源。bgsave 子进程有权读取到 redis-server 进程写入到内存中的用户数据,使得将内存数据持久化到 dump.rdb 成为可能。 bgsave 子进程在持久化时首先会将内存中的全量数据 copy 到磁盘中的一个 RDB 临时文件,copy 结束后再将该文件 rename 为 dump.rdb,替换掉原来的同名文件。 image.png 在进行持久化过程中,如果 redis-server 进程接收到了写请求,则系统会将内存中发生数据修改的物理块 copy 出一个副本。等内存中的全量数据 copy 结束后,再将副本中的数据 copy 到 RDB 临时文件。这个副本的生成是由于 Linux 系统的写时复制技术 (Copy-On-Write)实现的。

写时复制技术是 Linux 系统的一种进程管理技术。

原本在 Unix 系统中,当一个主进程通过 fork()系统调用创建子进程后,内核进程会复制主进程的整个内存空间中的数据,然后分配给子进程。这种方式存在的问题有以下几点:

  • 这个过程非常耗时
  • 这个过程降低了系统性能
  • 如果主进程修改了其内存数据,子进程副本中的数据是没有修改的。即出现了数据冗余,冗余数据最大的问题是数据一致性无法保证。

现代的 Linux 则采用了更为有效的方式:写时复制。子进程会继承父进程的所有资源,包括父进程的内存空间。即子进程与父进程共享内存。只要内存被共享,那么该内存就是只读的。而写时复制则是在任何一方需要写入数据到共享内存时都会出现异常, 此时内核进程就会将需要写入的数据 copy 出一个副本写入到另外一块非共享内存区域。