Redis持久化策略

Redis通常用来作为缓存服务器,其数据全部保存在内存里面。但是,是机器就会存在宕机的情况,一旦宕机,所有的内存数据就全部丢失了。因此,必须有一种机制来保证Redis的数据能能被持久化,以便在宕机时降低损失。

Redis为我们提供了两种持久化的策略,RDB和AOF。RDB是一种全量备份机制,其备份得到的是二进制内容,结构上十分紧凑;而AOF则是一种通过增量日志实现持续备份的机制,AOF记录的是对内存数据中进行了修改的操作指令,存储的文本内容。

RDB原理

SAVE和BGSAVE

Redis有两个命令可以生成RDB snapshot,一个是SAVE,一个是BGSAVE。SAVE命令是以同步的方式创建snapshot,也就是说会造成阻塞,Redis在SAVE命令运行期间将不会响应其他的命令。而BGSAVE则是通过fork一个子进程,将备份的任务交给子进程去做,而父进程可以继续响应客户端的请求。

SAVE和BGSAVE的备份方式其实是一样的,区别只在于是否会阻塞服务,假设Redis中创建RDB的函数抽象是rdb_save,通过下面的伪代码来描述SAVE和BGSAVE的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def SAVE():
# SAVE,同步创建RDB
rdb_save()

def BGSAVE():
# fork子进程
pid = fork()
if pid == 0:
# 子进程创建RDB文件
rdb_save()
# RDB备份完成,通知父进程
signal_parent()
else if pid > 0:
# 父进程继续处理请求
# 同时等待子进程通知
handle_reqs_and_wait_signal()
else:
# fork子进程失败处理函数
handle_fork_error()

可以看出SAVE和BGSAVE的区别仅仅在于执行rdb_save()的进程不一样。

由于BGSAVE的快照创建时在子进程中进行的,而父进程继续响应客户端的请求。如果在子进程备份过程中,父进程接收到了客户端的修改执行,如何来保证数据的一致性呢?Redis通过COW(Copy On Write)机制,对于读取请求,父进程直接读取共享内存就可以了,而在父进程要进行修改时,将要修改的数据页拷贝一份。当越来越多的页在BGSAVE备份过程中被修改,那么占用的内存会越来越多,但是理论上不会超过原数据内存的2倍(不考虑新数据的情况下)。由于子进程不会修改数据,所以子进程看到的内存在子进程被fork的时候就固定好了。

自动创建RDB快照

Redis可以根据配置,进行定时的快照创建。例如,我们可以在配置中加入如下配置:

1
2
3
save 900 1
save 300 10
save 60 10000

那么,Redis则会在满足下面任一条件的情况下,采用BGSAVE的方式创建RDB快照:

  • 900秒内发生至少一次写
  • 300秒内,至少发生10次写
  • 60秒内,至少发生1万次写

Redis会在周期性操作函数serverCron中进行是否需要创建RDB的检查。serverCron的作用是Redis中对正在运行中的服务器进行维护,其中一项任务便是检查是否需要进行RDB快照的创建。

Redis中通过saveparams数据结构来存储自动快照的配置,其结构如下:

序号 seconds changes
0 900 1
1 300 10
2 60 10000

同时,Redis中会保存上次创建备份的时间,以及上次备份之后对数据库写的次数:

lastsave dirty ops

通过下面的伪代码,我们来了解serverCron是如何生成RDB快照的:

1
2
3
4
5
6
7
8
9
10
11
12
def serverCron:
# ....
# 遍历所有条件
for saveparam in serer.saveparams:
# 计算间隔
interval = now() - server.lastsave
# 脏数据和时间间隔同时满足
if (server.dirty > saveparam.changes and \
interval > saveparam.seconds):
BGSAVE()

## ....

AOF原理

除了RDB,Redis还提供了AOF(Append Only File)持久化功能。和RDB将Redis的内存全量转存的快照方式不同,AOF的工作机制是将客户端的每个写数据库的请求保存到文件,是一个持续增量的过程。

image

AOF实现

AOF持久化分三步走,命令追加,文件写入,文件同步。
命令追加
当AOF功能打开是,Redis会维护一个aof_buffer的缓冲区,用来存放未保存到AOF文件中的写数据库的命令。比如客户端执行了如下的命令:

1
2
3
4
5
redis> set message "hello world"
OK

redis> rpush books "redis in action" "netty in action"
(integer) 3

Redis服务器在执行完指令后,会按照Redis的协议格式,将命令添加到服务器aof_buffer的尾部。

文件写入与同步
Redis服务器进程其实就是一个时间循环,在一次循环中会处理文件事件以及时间事件。文件事件指的是接收网络请求和响应。而时间事件处理的是serverCron之类的定时任务。由于在文件事件处理时会将执行的写命令追加到aof_buffer缓冲区的尾部,所以服务器在结束时间循环之前,调用flushAppendOnlyFile函数来检查是否需要将缓冲区中的内容写入和保存到AOF文件中。

下面是时间循环的伪代码,在一次循环结束之前,flushAppendOnlyFile会被调用:

1
2
3
4
5
6
7
8
9
def evenloop():
# 处理文件事件,将写数据库的请求写入
# aof缓冲区
processFileEvents()
processTimeEvents()

# 调用flushAppendOnlyFile检查是否需要
# 将缓冲区文件写入aof文件
flushAppendOnlyFile()

Redis可以通过配置appendfsync来选择flushAppendOnlyFile的行为:

appendfsync flushAppendOnlyFile的行为
always 将aof缓冲区中的文件都刷入aof文件
everysec 将缓冲区中的内容写入到AOF文件,每秒钟执行一次同步文件操作,同步的操作由一个线程负责
no 将AOF缓冲区的内容写入AOF文件,但是不去进行同步操作,让操作系统来选择合适进行文件同步

在不配置appendfsync的情况下,Redis会使用everysec作为aof写入和同步的选项。至于三种选项的优劣,结合具体的场景进行选择:

策略 Pros Cons
always aof和内存内容一致,数据安全性最高 每次都要同步,效率最低
everysec 效率足够高,宕机丢失的最多1秒钟数据 宕机有可能丢一秒的数据
no 不会有额外的系统调用和开销,写入效率最高 缓冲区会积累一段时间才同步文件,宕机时会丢数据的概率较大

AOF重写

由于AOF是持续写日志的方式来进行备份的,当时间久了之后,AOF文件内容会越来越大。同时,在AOF中会存在很多无用的操作,比如过期了的key,元素全部被删除了的key等。

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> rpush queue A
(integer) 1
127.0.0.1:6379> rpush queue hello
(integer) 2
127.0.0.1:6379> rpush queue day two one
(integer) 5
127.0.0.1:6379> lpop queue
"A"
127.0.0.1:6379> lpop queue
"hello"

AOF在保存上面这段指令执行需要保存5条记录。

为了解决Redis AOF空间膨胀的问题,Redis为AOF设计了AOF重写机制。在AOF重写会创建一个新的AOF文件来代替老的AOF文件,新的AOF保存的Redis状态和老的Redis一样,但是存储空间比老的AOF要小得多。因为AOF中不会保存任何冗余的命令。

虽然名字叫做AOF重写,但是整个重写过程中并不会对老的文件有任何的读操作。其实AOF重写做的事情是将内存中的key-value遍历一遍,将value读取出来,然后用一条等效的命令来记录对应的键值对,将这个心的命令写入新的AOF文件。

就比如刚才代码中的命令,这时候内存中的队列queue中存储的内容是”day”,”two”,”one”, 等效的命令就是:

1
rpush day two one

多以,在新的AOF文件中只需要这一条命令。

实际上,并不是所有的键值对重写之后都只有一条命令。如果一个键对应的值很多,如果只存一条命令,就需要把所有的Value都读到缓冲区,这很可能造成缓冲区溢出。所以Redis对list,hash,set,zset这些数据结构重写时,为了防止缓冲区溢出,如果数量超过一定的阈值,会使用多条记录来进行重写。

Redis提供了berewriteaof命令来调度后台AOF重写。后台AOF重写和后台RDB一样,都是通过子进程来实现的。在子进程进行AOF重写的过程中,父进程可以继续响应客户端的请求。那么如果在子进程重写AOF的过程中,父进程收到了写数据库的请求,就会导致AOF重写后的文件落后于父进程中的数据状态。为了解决这个问题,Redis设计重写AOF重写缓冲区,这个缓冲区在后台AOF重写启动的时候被激活,在子进程重写过程中父进程对内存的修改会将对应的命令写到AOF重写缓冲区。

子进程重写完成后,会发信号给父进程,父进程收到该信号后,会调用一个信号函数,这个信号函数会阻塞父进程,并将AOF重写缓冲区中的内容写到新的AOF文件中去,这样重写过的AOF文件就能包含重写过程中的修改了。整个重写过程之后最后的信号函数会阻塞,这样就将重写对服务器的影响降到了最低。

用JMH来验证伪共享 从LRU原理与实现到Redis的LRU策略

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×