状态
Redis是一个内存数据库,运行在内存中,其中可能包含多个数据库以及数据库中包含多个键值对,将Redis中这些非空的数据库和其中的键值对称为状态。
Redis的数据都存放在内存中,如果不将内存中的数据库状态保存到硬盘上,那么Redis进程结束的时候,本次运行期间数据库中所有的内容都不回被保存,下次启动Redis的时候又只是一个空数据库。
为了解决这个问题,Redis
使用了持久化功能,这个功能可以将Redis中的数据保存到硬盘上进行持久存储,防止数据的意外丢失。
RDB持久化
RDB持久化可以使用save
命令来进行手动存储,也可以配置服务器自动定期执行存储。
RDB读取Redis
的状态来得到数据库当前存储的信息,然后将其压缩为一个RDB文件;在需要恢复Redis数据库数据的时候,在从RDB文件中读取保存的信息从而恢复数据。
创建RDB文件
有两个命令可以来创建RDB文件,一个是SAVE
、一个是BGSAVE
。
- Save命令会阻塞
Redis
进程,所以从Save
执行直到结束,用户所有的命令都不能执行。
127.0.0.1:6379> save
OK
BGSave
命令会**fork**
一个子进程,然后子进程来完成RDB文件的创建,服务器进程继续执行命令请求。
127.0.0.1:6379> bgsave
Background saving started
实际上,创建RDB的工作是由rdbSave()
函数来执行的,SAVE
与BGSAVE
命令只是在不同的时候调用了这个函数。
伪代码如下:
function SAVE(){
rdbSave(); // 直接进行调用,导致阻塞
}
function BGSAVE(){
// 创建子进程
pid = fork();
if(pid==0) {
// pid=0代表使用子进程来执行
// 所以主进程不阻塞
rdbSave();
} else if (pid > 0) {
# 父进程继续执行用户发送的命令
....
}
}
自动保存RDB文件
由于BGSAVE
在执行期间不阻塞用户的请求,所以可以在conf
文件中设置相应的命令来使Redis
自动的保存当前数据库的状态,生成RDB文件。
在conf
中默认的运行BGSAVE
的参数如下:
save 900 1 # 代表如果900秒以内,数据库有最少一个修改,就执行BGSAVE
save 300 10 # 如果300秒以内,数据库有最少十个修改,就执行BGSAVE
save 60 10000 # 如果60秒以内,数据库有最少10000个修改,就执行BGSAVE
可以在conf
文件中配置多个save
,满足其中一个则自动执行BGSAVE
。
设置保存条件
服务器启动期间,读取conf
文件中的save
配置,生成一个保存所有save
条件的数组,
struct saveparam *saveparams;
struct saveparam{
// save配置的秒数
time_t seconds;
// save配置的改变的次数
int changes;
}
假如配置的是:
save 900 1 # 代表如果900秒以内,数据库有最少一个修改,就执行BGSAVE
save 300 10 # 如果300秒以内,数据库有最少十个修改,就执行BGSAVE
save 60 10000 # 如果60秒以内,数据库有最少10000个修改,就执行BGSAVE
那么最终会生成:
dirty计数器+lastsave属性
dirty
计数器记录了自上次成功执行SAVE
或者BGSAVE
以后,服务器对数据库状态修改的次数,包括添加、删除、更新等操作。
lastsave
则记录了上一次执行保存操作时候的UNIX
时间戳。
检查是否满足执行BGSAVE的条件
Redis
有一个周期性函数serverCron
默认每100ms执行一次,它的其中一个工作就是检查conf
文件中save
选项的条件是不是已经满足,如果满足,执行BGSAVE
。
function serverCron(){
.....
// 遍历执行检查操作
for(saveparam param:server.saveparams){
int diff = now() - lastsave();// 距离上次更新的间隔时间
if(diff >= param.seconds && dirty >= param.changes) {
BGSAVE();
}
}
....
}
程序遍历所有的save
参数,如果有一个满足,就执行BGSAVE
操作。
RDB总结
RDB
文件可以使服务器恢复Redis
数据库的所有键值对数据。SAVE
命令执行保存操作会阻塞服务器。BGSAVE
命令在后台执行,不会阻塞服务器。- 在
conf
中设置save
参数来配置BGSAVE
的执行条件。
优势
整个redis
中只有这一个备份文件,不用经常进行备份。
适合大规模的数据恢复。性能最大化。
通过fork
子进程来持久化,同时主进程又能继续处理客户端的请求。
相较于AOF
机制,如果数据集很大,启动时数据恢复效率更高更快。
劣势
如果服务器突然宕机,还未来得及持久化的数据将会丢失。
如果对数据完整性要求较高,不建议采用这种方式。
由于是fork
了一个与当前进程一样的进程,包含当前进程的所有数据,所以,内存中的数据增加了一倍,性能会有所影响。
AOF
RDB保存的是数据库的状态,而AOF保存的是Redis执行的所有写操作(读操作对数据库状态没有影响,不需要进行记录)。
当服务器执行:
127.0.0.1:6379> set msg vms
OK
127.0.0.1:6379> sadd schools qinghua beida ustc
(integer) 3
127.0.0.1:6379> rpush numbers 1 2 3
(integer) 3
AOF文件中保存的是:
*2
$6
SELECT
$1
0
*3
$3
set
$3
msg
$3
vms
*5
$4
sadd
$7
schools
$7
qinghua
$5
beida
$4
ustc
*5
$5
rpush
$7
numbers
$1
1
$1
2
$1
3
这些命令都是刚才执行过的写命令,当Redis服务器再次启动的时候,就可以从AOF文件中复原数据库的状态。
AOF追加实现方式
- 命令追加
当AOF命令处于打开的时候,服务器执行完一个写命令后,会将执行的写命令按照所规定的格式加入到AOF_BUF缓冲区的末尾。 - 写入与同步
Redis
服务器就是一个事件的循环,其中
- 文件事件:接受客户端的命令请求
- 时间事件:执行需要定时执行的命令
每次服务器结束一个文件事件的时候,由于可能执行写命令,这会让一些内容被追加到AOF_BUF
缓冲区中(设置缓冲区的目的是为了提高文件的写入效率,将需要写入的数据暂存到内存缓冲区,缓冲区满或者超过一定时间时候才将数据写入硬盘),所以每次在文件事件结束的时候,都会调用flushAppendOnlyFile
函数,来决定时都需要将AOF_BUF缓冲区的内容加入到AOF文件中。
伪代码:
function eventLoop(){
while(true){
// 处理文件事件
processFileEvents();
// 处理时间事件
processTimeEvents();
// 决定是否将aof缓冲区内容写入aof文件
flushAppendOnlyFile();
}
}
flushAppendOnlyFile
根据conf
的配置来决定何时来执行AOF。
# appendfsync always # AOF_BUF的所有内容都加入AOF文件
appendfsync everysec # 每一秒钟将AOF_BUF的文件加入AOF文件
# appendfsync no # 何时加入来由操作系统决定
appendfsync always
如果突然断电,最多会丢失一次事件循环已经执行的命令,因为每次执行完毕都会写入到AOF,但是因为每次都写入AOF文件,所以效率稍低。appendfsync everysec
最多丢失一秒内执行的命令appendfsync no
丢失的命令数量=上次写入到AOF以后执行命令的次数。
数据还原
Redis
读取AOF
文件的步骤如下:
- 创建一个伪客户端,因为
Redis
命令只能在客户端上下文中执行,而载入AOF
文件中的命令来源于AOF
而不是来自客户端,所以创建一个伪客户端来执行AOF
中的命令。 - 从AOF读取一条写入命令
- 伪客户端执行这个命令
- 重复2-3直到AOF文件中的命令执行完毕。
AOF重写
如果AOF文件体积过大,那么通常会影响服务器性能,可以通过一些操作来缓解这种情况。
例如如下命令:
127.0.0.1:6379> RPUSH list A B # [A,B]
(integer) 2
127.0.0.1:6379> RPUSH list C # [A,B,C]
(integer) 3
127.0.0.1:6379> RPUSH list D E # [A,B,C,D,E]
(integer) 5
127.0.0.1:6379> LPOP list # [B,C,D,E]
"A"
127.0.0.1:6379> LPOP list # [C,D,E]
"B"
127.0.0.1:6379> RPUSH list F G # [C,D,E,F,G]
(integer) 5
其实这些都可以用一条命令
RPUSH list C,D,E,F,G
AOF实现这个功能的操作就是重写。
重写的实现
AOF重写并不需要对现有的AOF文件进行读取分析,而是直接读取服务器的状态来实现。
当执行这些指令来以后:
127.0.0.1:6379> RPUSH list A B # [A,B]
(integer) 2
127.0.0.1:6379> RPUSH list C # [A,B,C]
(integer) 3
127.0.0.1:6379> RPUSH list D E # [A,B,C,D,E]
(integer) 5
127.0.0.1:6379> LPOP list # [B,C,D,E]
"A"
127.0.0.1:6379> LPOP list # [C,D,E]
"B"
127.0.0.1:6379> RPUSH list F G # [C,D,E,F,G]
(integer) 5
如果想要实现AOF文件体积的最小化,那么就可以直接从数据库读取list
键对应的值,可以得到:C,D,E,F,G
。
所以AOF重写可以概括为:
- 从数据库读取键现在的值
- 用一条命令(例如
RPUSH list C,D,E,F,G
)去记录这个键值对来代替之前的多条的命令。
整个过程:
function aof_rewrite(newfilename){
f = create_file(newfilename); // 新的aof文件
for(Database db: server.Databases){
if(db.empty) continue; // 当前数据库空,跳过
for(Key k:keys){ // 遍历所有的key
if key.is_expired() continue;
f.write(k); // 写入key以及key对应的数据
}
}
f.close();
}
- AOF重写是通过子进程实现的
因为使用的是子进程,那么子进程对AOF重写的时候,服务器可能处理新的数据,改变AOF开始重写时刻以后的数据库状态,那么可能会导致当前数据库状态和重写后AOF记录的服务器状态不一致。
为了解决这个问题,Redis
设置了一个AOF重写缓冲区,这个缓冲区在子进程之后开始使用,Redis执行完一个写命令以后,同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区,这样可以保障:从子进程创建开始,所有的写命令都会被记录到AOF重写缓冲区。
当子进程完成后,给父进程发送信号,父进程调用信号处理函数,并执行:
- 将AOF重写缓冲区的内容写到写的AOF中,保证数据的一致性。
- 对新的AOF文件换名,原子性的覆盖现有的AOF文件,完成新旧AOF文件的替换。
优劣势
优势
更高的数据安全性和完整性。
默认情况下每秒同步,最多丢失一秒的数据。
后台异步的,效率非常高。
提供Redis-check-aof --fix
机制,确保数据正确性。
劣势
AOF文件相较于RDB文件大得多,数据恢复效率低。
AOF虽然是后台异步fsync追加日志文件,无论是每秒同步还是每修改同步,都是消耗一部分性能。
如果同时开启两种持久化方式,redis重启时会采用AOF文件来恢复数据。因为AOF文件保存的数据比RDB要完整,RDB丢失数据的风险要大一些。