1. 持久化流程
Redis是一个内存数据库,数据保存在内存中,但是我们都知道内存的数据变化是很快的,也容易发生丢失。幸好Redis还为我们提供了持久化的机制,分别是RDB(Redis DataBase)和AOF(Append Only File)。
既然redis的数据可以保存在磁盘上,那么这个流程是什么样的呢?
主要有下面五个过程:
(1)客户端向服务端发送写操作(数据在客户端的内存中)。
(2)数据库服务端接收到写请求的数据(数据在服务端的内存中)。
(3)服务端调用write这个系统调用,将数据往磁盘上写(数据在系统内存的缓冲区中)。
(4)操作系统将缓冲区中的数据转移到磁盘控制器上(数据在磁盘缓存中)。
(5)磁盘控制器将数据写到磁盘的物理介质中(数据真正落到磁盘上)。
这5个过程是在理想条件下一个正常的保存流程,但是在大多数情况下,我们的机器等等都会有各种各样的故障,这里划分了两种情况:
(1)Redis数据库发生故障,只要在上面的第三步执行完毕,那么就可以持久化保存,剩下的两步由操作系统替我们完成。
(2)操作系统发生故障,必须上面5步都完成才可以。
在这里只考虑了保存的过程可能发生的故障,其实保存的数据也有可能发生损坏,需要一定的恢复机制,不过在这里就不再延伸了。现在主要考虑的是redis如何来实现上面5个保存磁盘的步骤。它提供了两种策略机制,也就是RDB和AOF。
2. RDB方式
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。
在我们安装了redis之后,所有的配置都是在redis.conf文件中,里面保存了RDB和AOF两种持久化机制的各种配置。
当符合一定条件时Redis会自动将内存中的数据进行快照并持久化到硬盘。
2.1 触发快照的时机
2.1.1 符合自定义配置的快照规则(redis.conf)
save:这里是用来配置触发 Redis的 RDB 持久化条件,也就是什么时候将内存中的数据保存到硬盘。比如"save m n"。表示m秒内数据集存在n次修改时,自动触发bgsave。
默认如下配置:
save 900 1 :表示900秒钟内至少1个键被更改则进行快照。
save 300 10 :表示300秒内至少10个键被更改则进行快照。
save 60 10000 :表示60秒内至少10000个键被更改则进行快照。
如果不需要持久化,那么你可以注释掉所有的 save 行来停用保存功能或设置为save "" 。
2.1.2 执行save或者bgsave命令
2.1.2.1 save触发方式
执行该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。
<center>具体流程</center>
001.jpeg
执行完成时候,如果存在老的RDB文件,就把新的替代掉旧的。我们的客户端可能都有几万或者是几十万,这种方式显然不可取。
2.1.2.2 bgsave触发方式
执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。
<center>具体流程</center>
002.jpeg
具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令。
003.jpeg2.1.3 执行flushall命令
2.1.4 执行主从复制操作(第一次)
<center>RDB原理图</center>
001.png
2.2 注意事项
Redis在进行快照的过程中不会修改RDB文件,只有快照结束后才会将旧的文件替换成新的,也就是说任何时候RDB文件都是完整的。
这就使得我们可以通过定时备份RDB文件来实现Redis数据库的备份,RDB文件是经过压缩的二进制文件,占用的空间会小于内存中的数据,更加利于传输。
2.3 RDB优缺点
2.3.1 优点:
RDB可以最大化Redis的性能。
父进程在保存RDB文件时唯一要做的就是fork出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无需执行任何磁盘I/O操作。
同时这个也是一个缺点,如果数据集比较大的时候,fork可以能比较耗时,造成服务器在一段时间内停止处理客户端的请求。
RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
2.3.2 缺点:
RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。一旦Redis异常退出,就会丢失最后一次快照以后更改的所有数据。
这个时候我们就需要根据具体的应用场景,通过组合设置自动快照条件的方式来将可能发生的数据损失控制在能够接受范围。
如果数据相对来说比较重要,希望将损失降到最小,则可以使用AOF方式进行持久化。
3. AOF方式
全量备份总是耗时的,Redis为我们提供了一种更加高效的持久化方式,即AOF(appendonlyfile)。此方式工作机制很简单,redis会将每一个收到的写命令都通过write函数追加到文件中。默认情况下Redis没有开启AOF方式的持久化。
开启AOF持久化后,每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件,这一过程显然会降低Redis的性能,但大部分情况下这个影响是能够接受的,另外使用较快的硬盘可以提高AOF的性能。
3.1 AOF方式的开启
# 可以通过修改redis.conf配置文件中的appendonly参数开启
appendonly yes
# AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的。
dir ./
# 默认的文件名是appendonly.aof,可以通过appendfilename参数修改
appendfilename appendonly.aof
3.2 原理
Redis将所有对数据库进行过写入的命令(及其参数)记录到AOF文件,以此达到记录数据库状态的目的,为了方便起见,我们称呼这种记录过程为同步。
当一个 Redis 客户端需要执行命令时,它通过网络连接,将协议文本发送给 Redis 服务器。为了处理的方便, AOF 文件使用网络通讯协议的格式来保存这些命令。
Redis客户端使用RESP(Redis的序列化协议)协议与Redis的服务器端进行通信。协议如下:
1.间隔符号在Linux下是\r\n,在Windows下是\n
2.简单字符串SimpleStrings,以"+"加号开头
3.错误Errors,以"-"减号开头
4.整数型Integer,以":"冒号开头
5.大字符串类型BulkStrings,以"$"美元符号开头,长度限制512M
6.数组类型Arrays,以"*"星号开头
用SET命令来举例说明RESP协议的格式。
redis> SET mykey "Hello"
"OK"
实际发送的请求数据:
*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nHello\r\n
*3
$3
SET
$5
mykey
$5
Hello
实际收到的响应数据:
+OK\r\n
002.png
3.3 同步命令到AOF文件的整个过程可以分为三个阶段:
命令传播:Redis将执行完的命令、命令的参数、命令的参数个数等信息发送到AOF程序中。
缓存追加:AOF程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的AOF缓存中。
文件写入和保存:AOF缓存中的内容被写入到AOF文件末尾,如果设定的AOF保存条件被满足的话,fsync函数或者fdatasync函数会被调用,将写入的内容真正地保存到磁盘中。
3.3.1 命令传播
当一个Redis客户端需要执行命令时,它通过网络连接将协议文本发送给Redis服务器。比如说, 要执行命令 SET KEY VALUE
, 客户端将向服务器发送文本 "*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n"
。
服务器在接到客户端的请求之后,它会根据协议文本的内容选择适当的命令函数来执行,并将各个参数从字符串文本转换为Redis字符串对象(StringObject)。比如说, 针对上面的 SET 命令例子, Redis 将客户端的命令指针指向实现 SET命令的 setCommand函数, 并创建三个 Redis 字符串对象, 分别保存 SET、 KEY 和 VALUE 三个参数(命令也算作参数)。
每当命令函数成功执行之后,命令、命令的参数、命令的参数个数等信息都会被传播到AOF程序。
3.3.2 缓存追加
当命令被传播到AOF程序之后,程序会根据命令以及命令的参数,将命令从字符串对象转换回原来的协议文本。比如说, 如果 AOF 程序接受到的三个参数分别保存着 SET 、 KEY和 VALUE 三个字符串, 那么它将生成协议文本 "*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n"
。
协议文本生成之后,它会被追加到 redis.h/redisServer结构的aof_buf末尾。redisServer 结构维持着Redis服务器的状态,aof_buf域则保存着所有等待写入到AOF文件的协议文本。
struct redisServer {
// 其他域...
sds aof_buf;
// 其他域...
};
至此, 追加命令到缓存的步骤执行完毕。
综合起来,整个缓存追加过程可以分为以下三步:
1.接受命令、命令的参数、以及参数的个数、所使用的数据库等信息。
2.将命令还原成 Redis 网络通讯协议。
3.将协议文本追加到 aof_buf
末尾。
3.3.3 文件写入和保存
每当服务器常规任务函数被执行或者事件处理器被执行时,aof.c/flushAppendOnlyFile函数都会被调用,这个函数执行以下两个工作:
1.WRITE:根据条件,将aof_buf中的缓存写入到AOF文件。
2.SAVE:根据条件,调用fsync或fdatasync函数,将AOF文件保存到磁盘中。
两个步骤都需要根据一定的条件来执行, 而这些条件由 AOF 所使用的保存模式来决定。 下面就来介绍 AOF 所使用的三种保存模式 以及在这些模式下步骤 WRITE 和 SAVE 的调用条件。
3.4 AOF保存模式
Redis目前支持三种AOF保存模式,它们分别是:不保存、每秒钟保存一次、每执行一个命令保存一次。
AOF_FSYNC_NO(不保存):在这种模式下,每次调用flushAppendOnlyFile函数,WRITE都会被执行,但SAVE会被略过。在这种模式下,SAVE只会在以下情况中被执行:1.Redis被关闭;2.AOF功能被关闭;3.系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行)。这三种情况下的SAVE操作都会引起Redis主进程阻塞。
AOF_FSYNC_EVERYSEC(每一秒钟保存一次):在这种模式中,SAVE原则上每隔一秒钟就会执行一次,因为SAVE操作是由后台子线程调用的,所以它不会引起服务器主进程阻塞。
AOF_FSYNC_ALWAYS(每执行一个命令保存一次(不推荐)):在这种模式下,每次执行完一个命令之后,WRITE和SAVE都会被执行。另外,因为SAVE是由Redis主进程执行的,所以在SAVE执行期间,主进程会被阻塞,不能接受命令请求。
3.4.1 AOF保存模式对性能和安全性的影响
对于三种AOF保存模式,它们对服务器主进程的阻塞情况如下:
AOF_FSYNC_NO:写入和保存都由主进程执行,两个操作都会阻塞主进程。
AOF_FSYNC_EVERYSEC:写入操作由主进程执行,阻塞主进程。保存操作由子线程执行,不直接阻塞主进程,但保存操作完成的快慢会影响写入操作的阻塞时长。
AOF_FSYNC_ALWAYS:和AOF_FSYNC_NO模式一样。
因为阻塞操作会让Redis主进程无法持续处理请求,所以一般说来,阻塞操作执行得越少,完成得越快,Redis的性能就越好。
003.png
3.4.1 三种保存模式对比
模式1的保存操作只会在AOF关闭或Redis关闭时执行,或者由操作系统触发,在一般情况下,这种模式只需要为写入阻塞,因此它的写入性能要比后面两种模式要高。当然,这种性能的提高是以降低安全性为代价的。在这种模式下,如果运行的中途发生停机,那么丢失数据的数量由操作系统的缓存冲洗策略决定。
模式2在性能方面要优于模式3,并且在通常情况下,这种模式最多丢失不多于2秒的数据,所以它的安全性要高于模式1,这是一种兼顾性能和安全性的保存方案。
模式3的安全性是最高的,但性能也是最差的,因为服务器必须阻塞直到命令信息被写入并保存到磁盘之后才能继续处理请求。
综合起来,三种 AOF 模式的操作特性可以总结如下:
模式 | WRITE 是否阻塞 | SAVE 是否阻塞 | 停机时丢失的数据量 |
---|---|---|---|
AOF_FSYNC_NO | 阻塞 | 阻塞 | 操作系统最后一次对 AOF 文件触发 SAVE 操作之后的数据。 |
AOF_FSYNC_EVERYSEC | 阻塞 | 不阻塞 | 一般情况下不超过 2 秒钟的数据。 |
AOF_FSYNC_ALWAYS | 阻塞 | 阻塞 | 最多只丢失一个命令的数据。 |
3.5 AOF 重写
AOF 持久化是通过保存被执行的写命令来记录数据库状态的,所以AOF文件的大小随着时间的流逝一定会越来越大。影响包括但不限于:对于Redis服务器,计算机的存储压力;AOF还原出数据库状态的时间增加。
为了解决AOF文件体积膨胀的问题,Redis提供了AOF重写功能:Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个文件所保存的数据库状态是相同的,但是新的AOF文件不会包含任何浪费空间的冗余命令,通常体积会较旧AOF文件小很多。
3.5.1 AOF 文件重写的实现
AOF重写并不需要对原有AOF文件进行任何的读取、写入、分析等操作,这个功能是通过读取服务器当前的数据库状态来实现的。
假设服务器对键list执行了以下命令
127.0.0.1:6379> RPUSH list "A" "B"
(integer) 2
127.0.0.1:6379> RPUSH list "C"
(integer) 3
127.0.0.1:6379> RPUSH list "D" "E"
(integer) 5
127.0.0.1:6379> LPOP list
"A"
127.0.0.1:6379> LPOP list
"B"
127.0.0.1:6379> RPUSH list "F" "G"
(integer) 5
127.0.0.1:6379> LRANGE list 0 -1
1) "C"
2) "D"
3) "E"
4) "F"
5) "G"
127.0.0.1:6379>
当前列表键list在数据库中的值就为["C", "D", "E", "F", "G"]。要使用尽量少的命令来记录list键的状态,最简单的方式不是去读取和分析现有AOF文件的内容,而是直接读取list键在数据库中的当前值,然后用一条RPUSH list "C" "D" "E" "F" "G"代替前面的6条命令。
3.5.2 AOF重写功能的实现原理
首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录该键值对的多个命令,伪代码表示如下:
def AOF_REWRITE(tmp_tile_name):
f = create(tmp_tile_name)
# 遍历所有数据库
for db in redisServer.db:
# 如果数据库为空,那么跳过这个数据库
if db.is_empty(): continue
# 写入 SELECT 命令,用于切换数据库
f.write_command("SELECT " + db.number)
# 遍历所有键
for key in db:
# 如果键带有过期时间,并且已经过期,那么跳过这个键
if key.have_expire_time() and key.is_expired(): continue
if key.type == String:
# 用 SET key value 命令来保存字符串键
value = get_value_from_string(key)
f.write_command("SET " + key + value)
elif key.type == List:
# 用 RPUSH key item1 item2 ... itemN 命令来保存列表键
item1, item2, ..., itemN = get_item_from_list(key)
f.write_command("RPUSH " + key + item1 + item2 + ... + itemN)
elif key.type == Set:
# 用 SADD key member1 member2 ... memberN 命令来保存集合键
member1, member2, ..., memberN = get_member_from_set(key)
f.write_command("SADD " + key + member1 + member2 + ... + memberN)
elif key.type == Hash:
# 用 HMSET key field1 value1 field2 value2 ... fieldN valueN 命令来保存哈希键
field1, value1, field2, value2, ..., fieldN, valueN =\
get_field_and_value_from_hash(key)
f.write_command("HMSET " + key + field1 + value1 + field2 + value2 +\
... + fieldN + valueN)
elif key.type == SortedSet:
# 用 ZADD key score1 member1 score2 member2 ... scoreN memberN
# 命令来保存有序集键
score1, member1, score2, member2, ..., scoreN, memberN = \
get_score_and_member_from_sorted_set(key)
f.write_command("ZADD " + key + score1 + member1 + score2 + member2 +\
... + scoreN + memberN)
else:
raise_type_error()
# 如果键带有过期时间,那么用 EXPIREAT key time 命令来保存键的过期时间
if key.have_expire_time():
f.write_command("EXPIREAT " + key + key.expire_time_in_unix_timestamp())
# 关闭文件
f.close()
实际为了避免执行命令时造成客户端输入缓冲区溢出,重写程序在处理list hash set zset时,会检查键所包含的元素的个数。如果元素的数量超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那么重写程序会使用多条命令来记录键的值,而不是单使用一条命令。该常量默认值是64,即每条命令设置的元素的个数是最多64个,重写程序使用多条命令实现集合键中元素数量超过64个的键。
3.5.3 AOF后台重写
aof_rewrite函数可以创建新的AOF文件,但是这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间的阻塞,因为Redis服务器使用单线程来处理命令请求;如果直接是服务器进程调用AOF_REWRITE函数的话,那么重写AOF期间,服务器将无法处理客户端发送来的命令请求。Redis不希望AOF重写会造成服务器无法处理请求,所以Redis决定将AOF重写程序放到子进程(后台)里执行。这样处理的最大好处是:
①子进程进行AOF重写期间,主进程可以继续处理命令请求。
②子进程带有主进程的数据副本,使用子进程而不是线程,可以避免在锁的情况下保证数据的安全性。
3.5.3.1 子进程进行AOF重写的问题
子进程在进行AOF重写期间,服务器进程还要继续处理命令请求,而新的命令可能对现有的数据进行修改,这会让当前数据库的数据和重写后的AOF文件中的数据不一致。
3.5.3.2 子进程进行AOF重写问题的修正
为了解决这种数据不一致的问题,Redis增加了一个AOF重写缓存,这个缓存在fork出子进程之后开始启用,Redis服务器主进程在执行完写命令之后,会同时将这个写命令追加到AOF缓冲区和AOF重写缓冲区,即子进程在执行AOF重写时,主进程需要执行以下三个工作:
执行client发来的命令请求;
将写命令追加到现有的AOF文件中;
将写命令追加到AOF重写缓存中
这样一来,可以保证:
①AOF缓冲区的内容会定期被写入和同步到AOF文件中,对现有的AOF文件的处理工作会正常进行。
②从创建子进程开始,服务器执行的所有写操作都会被记录到AOF重写缓冲区中;
3.5.3.3 完成AOF重写之后
当子进程完成对AOF文件重写之后,它会向父进程发送一个完成信号,父进程接到该完成信号之后,会调用一个信号处理函数,该函数完成以下工作:
① 将AOF重写缓存中的内容全部写入到新的AOF文件中;这个时候新的AOF文件所保存的数据库状态和服务器当前的数据库状态一致;
② 对新的AOF文件进行改名,原子的覆盖原有的AOF文件;完成新旧两个AOF文件的替换。
当这个信号处理函数执行完毕之后,主进程就可以继续像往常一样接收命令请求了。在整个AOF后台重写过程中,只有最后的 “主进程写入命令到AOF缓存” 和 “对新的AOF文件进行改名,覆盖原有的AOF文件” 这两个步骤(信号处理函数执行期间)会造成主进程阻塞,在其他时候,AOF后台重写都不会对主进程造成阻塞,这将AOF重写对性能造成的影响降到最低。
以上,即AOF后台重写,也就是BGREWRITEAOF命令的工作原理。
3.4.3.4 触发AOF后台重写的条件
手动触发
AOF重写可以由用户通过调用BGREWRITEAOF手动触发。
自动触发
服务器在AOF功能开启的情况下,会维持以下三个变量:aof_current_size(记录当前AOF文件大小的变量)、aof_rewrite_base_size(记录最后一次AOF重写之后AOF文件大小的变量)、aof_rewrite_perc(增长百分比变量)。
每次当serverCron函数(服务器周期性操作函数)执行时,它会检查以下条件是否全部满足,如果全部满足的话,就触发自动的AOF重写操作:
①没有BGSAVE命令(RDB持久化)/AOF持久化在执行;
②没有BGREWRITEAOF在进行;
③当前AOF文件大小要大于server.aof_rewrite_min_size(默认为1MB),或者在redis.conf配置的auto-aof-rewrite-min-size;
④当前AOF文件大小和最后一次重写后的大小之间的比率等于或者等于指定的增长百分比(在配置文件设置了auto-aof-rewrite-percentage参数,不设置默认为100%)。
也就是说,如果前面三个条件已经满足,并且当前AOF文件大小比最后一次AOF重写的大小大一倍就会触发自动AOF重写。
网友评论