Redis的持久化方式
通常数据库存在三种用于持久操作以防止数据损坏的常见策略:
- 是数据库不关心故障,而是在数据文件损坏后从数据备份或快照中恢复。RDB就是这种情况
- 该数据库使用操作日志记录每个操作的操作行为,以在失败后通过日志恢复一致性。由于操作日志是按顺序追加写入的,因此不会出现无法恢复操作日志的情况。类似于Mysql的重做和撤消日志。
- 数据库不修改旧数据,而仅通过追加进行写入,因此数据本身就是日志,因此永远不会出现数据无法恢复的情况。CouchDB是一个很好的例子。AOF类似这种情况
严格上讲Redis为持久化提供了三种方式:
- RDB:在指定的时间间隔能对数据进行快照存储,类似于MySQL的dump备份文件。
- AOF:记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据(MySQL的binlog)。
- RDB与AOF混合使用,这是Redis4.0开始的新特性。在混合使用中AOF读取RDB数据重建原始数据集,集二者优势为一体。
RDB持久化
初始化环境
创建配置/数据/日志目录
# 创建配置目录
mkdir -p /usr/local/redis/conf
# 创建数据目录
mkdir -p /usr/local/redis/data
# 创建日志目录
mkdir -p /usr/local/redis/log
代码块123456
配置文件
创建一份配置文件至 conf
目录。
vim /usr/local/redis/conf/redis.conf
代码块1
文件内容如下:
# 放行访问IP限制
bind 0.0.0.0
# 后台启动
daemonize yes
# 日志存储目录及日志文件名
logfile "/usr/local/redis/log/redis.log"
# rdb数据文件名
dbfilename dump.rdb
# rdb数据文件和aof数据文件的存储目录
dir /usr/local/redis/data
# 设置密码
requirepass 123456
代码块123456789101112
准备数据
在 /usr/local/redis/bin
目录下创建 initdata.py
,内容如下:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
class Token(object):
def __init__(self, value):
if isinstance(value, Token):
value = value.value
self.value = value
def __repr__(self):
return self.value
def __str__(self):
return self.value
def b(x):
return x
SYM_STAR = b('*')
SYM_DOLLAR = b('$')
SYM_CRLF = b('\r\n')
SYM_EMPTY = b('')
class RedisProto(object):
def __init__(self, encoding='utf-8', encoding_errors='strict'):
self.encoding = encoding
self.encoding_errors = encoding_errors
def pack_command(self, *args):
"""将redis命令安装redis的协议编码,返回编码后的数组,如果命令很大,返回的是编码后chunk的数组"""
output = []
command = args[0]
if ' ' in command:
args = tuple([Token(s) for s in command.split(' ')]) + args[1:]
else:
args = (Token(command),) + args[1:]
buff = SYM_EMPTY.join(
(SYM_STAR, b(str(len(args))), SYM_CRLF))
for arg in map(self.encode, args):
"""数据量特别大的时候,分成部分小的chunk"""
if len(buff) > 6000 or len(arg) > 6000:
buff = SYM_EMPTY.join((buff, SYM_DOLLAR, b(str(len(arg))), SYM_CRLF))
output.append(buff)
output.append(arg)
buff = SYM_CRLF
else:
buff = SYM_EMPTY.join((buff, SYM_DOLLAR, b(str(len(arg))), SYM_CRLF, arg, SYM_CRLF))
output.append(buff)
return output
def encode(self, value):
if isinstance(value, Token):
return b(value.value)
elif isinstance(value, bytes):
return value
elif isinstance(value, int):
value = b(str(value))
elif not isinstance(value, str):
value = str(value)
if isinstance(value, str):
value = value.encode(self.encoding, self.encoding_errors)
return value
if __name__ == '__main__':
for i in range(5000000):
commands_args = [('SET', 'key_' + str(i), 'value_' + str(i))]
commands = ''.join([RedisProto().pack_command(*args)[0] for args in commands_args])
print commands
代码块12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
在 /usr/local/redis/bin
目录下执行以下命令加载数据(根据机器不同可能需要30s~60s):
python initdata.py | ./redis-cli -a 123456 --pipe
代码块1
开启RBD
我们可以配置Redis在n秒内如果超过m个key被修改就自动做快照,下面是默认的快照保存配置(这3个选项都屏蔽,则RDB禁用):
# 900秒内如果超过1个key改动,则发起快照保存
save 900 1
# 300秒内如果超过10个key改动,则发起快照保存
save 300 10
# 60秒内如果超过1W个key改动,则发起快照保存
save 60 10000
代码块123456
什么叫Redis快照
快照,顾名思义可以理解为拍照一样,把整个内存数据映射到硬盘中,保存一份到硬盘,因此恢复数据起来比较快,把数据映射回去即可,不像AOF,一条条的执行操作命令。
快照是默认的持久化方式。这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。可以通过配置设置自动做快照持久化的方式。
产生快照的情况有以下几种:
- 执行bgsave命令(此时Redis会fork一个子进程,子进程负责生成硬盘文件,父进程负责继续接收命令)
- 或执行save命令(和bgsave命令不同,发送save命令后,系统创建快照完成之前系统不会再接收任何新的命令,换句话说save命令会阻塞后面的命令,而bgsave不会)
- 根据用户在配置文件中配置的快照触发时间执行
- 客户端发送shutdown,系统会先执行save命令阻塞客户端,然后关闭服务器
- 当有主从架构时,从服务器向主服务器发送sync命令来执行复制操作时,主服务器会执行bgsave操作
RDB工作原理
Redis默认会将快照文件存储在Redis当前进程的工作目录中的dump.rdb文件中,可以通过配置dir和dbfilename两个参数分别指定快照文件的存储路径和文件名。流程过程如下(rdb.c中)。
- Redis使用fork函数复制一份当前进程(父进程)的副本(子进程);
- 父进程继续接收并处理客户端发来的命令,而子进程开始将内存中的数据写入硬盘中的临时文件;
- 当子进程写入完所有数据后会用该临时文件替换旧的 RDB 文件,至此一次快照操作完成。
在执行 fork 的时候操作系统(类 Unix 操作系统)会使用写时复制(copy-on-write)策略,即fork函数发生的一刻父子进程共享同一内存数据,当父进程要更改其中某片数据时(如执行一个写命令),操作系统会将该片数据复制一份以保证子进程的数据不受影响,所以新的RDB文件存储的是执行fork一刻的内存数据。
写时复制策略也保证了在 fork 的时刻虽然看上去生成了两份内存副本,但实际上内存的占用量并不会增加一倍。这就意味着当系统内存只有2 GB,而Redis数据库的内存有1.5 GB时,执行 fork后内存使用量并不会增加到3 GB(超出物理内存)。为此需要确保 Linux 系统允许应用程序申请超过可用内存(物理内存和交换分区)的空间,方法是在/etc/sysctl.conf 文件加入 vm.overcommit_memory = 1,然后重启系统或者执行 sysctl vm.overcommit_memory=1 确保设置生效。
另外需要注意的是,当进行快照的过程中,如果写入操作较多,造成 fork 前后数据差异较大,是会使得内存使用量显著超过实际数据大小的,因为内存中不仅保存了当前的数据库数据,而且还保存着 fork 时刻的内存数据。进行内存用量估算时很容易忽略这一问题,造成内存用量超限。
通过上述过程可以发现Redis在进行快照的过程中不会修改RDB文件,只有快照结束后才会将旧的文件替换成新的,也就是说任何时候 RDB 文件都是完整的。这使得我们可以通过定时备份 RDB 文件来实现 Redis 数据库备份。RDB 文件是经过压缩(可以配置rdbcompression 参数以禁用压缩节省CPU占用)的二进制格式,所以占用的空间会小于内存中的数据大小,更加利于传输。
Redis启动后会读取RDB快照文件,将数据从硬盘载入到内存。根据数据量大小与结构和服务器性能不同,这个时间也不同。通常将一个记录1000万个字符串类型键、大小为1 GB 的快照文件载入到内存中需要花费20~30秒。
扩展:写入时复制(英语:Copy-on-write,简称COW)是一种计算程序设计]领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
RBD的优点
- 一旦采用RDB方式,那么你的整个Redis数据库将只包含一个紧凑压缩的二进制文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。
- 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。
- 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。
- 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。
RDB的缺点
- 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
- 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
AOF持久化
它也是Redis持久化的重要手段之一,AOF(Append Only File)只追加文件,也就是每次处理完请求命令后都会将此命令追加到aof文件的末尾。而RDB是压缩成二进制等时机开子进程去干这件事。
开启AOF
通过配置进行启动,默认是关闭的,以下是 AOF 常用配置:
# 默认appendonly为no
appendonly yes
appendfilename "appendonly.aof"
# RDB文件和AOF文件所在目录
dir /usr/local/redis/data
# fsync 持久化策略
appendfsync everysec
# AOF重写期间是否禁止fsync;如果开启该选项,可以减轻文件重写时CPU和硬盘的负载(尤其是硬盘),但是可能会丢失AOF重写期间的数据;需要在负载和安全性之间进行平衡
no-appendfsync-on-rewrite no
# 当前aof文件大于多少字节后才触发
auto-aof-rewrite-min-size 64mb
# 当前写入日志文件的大小超过上一次rewrite之后的文件大小的百分之100时,也就是2倍时触发Rewrite
auto-aof-rewrite-percentage 100
# 如果AOF文件结尾损坏,Redis启动时是否仍载入AOF文件
aof-load-truncated yes
代码块123456789101112131415
同步策略
Redis中提供了3种AOF同步策略:
- 每秒同步(默认,每秒调用一次 fsync,这种模式性能并不是很糟糕)
- 每修改同步(会极大消弱 Redis 的性能,因为这种模式下每次 write 后都会调用 fsync)
- 不主动同步(由操作系统自动调度刷磁盘,性能是最好的)
# 每秒钟同步一次,该策略为AOF的缺省策略
appendfsync everysec
# 每次有数据修改发生时都会写入AOF文件
appendfsync always
# 从不同步。高效但是数据不会主动被持久化
appendfsync no
代码块123456
事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。
而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。
至于无同步,Redis不会主动去将AOF日志内容同步到磁盘,所以这一切就完全依赖于操作系统的调试了。对大多数Linux操作系统,是每30秒进行一次fsync,将缓冲区中的数据写到磁盘上。
AOF工作原理
AOF的频率高的话肯定会对Redis带来性能影响,因为每次都是刷盘操作。跟mysql一样了。Redis每次都是先将命令放到缓冲区,然后根据具体策略(每秒/每条指令/缓冲区满)进行刷盘操作。如果配置的always,那么就是典型阻塞,如果是everysec每秒的话,那么会开一个同步线程去每秒进行刷盘操作,对主线程影响稍小。
写入文件与恢复
AOF 文件是一个只进行append操作的日志文件,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。假如一次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们修复问题。
AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。
导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL
命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL
命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
重写
Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行rewrite。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。
因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。
而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
为什么要重写
比如我有业务很简单,就来回delete set 同一个key。就这个业务运行了10年,那么aof文件将记录无数个delete k1, set k1 xxx。其实都是重复的,但是我aof每次都追加,文件变成了1T大小。这时候Redis宕机了,要恢复,你想想1TB大小的aof文件去恢复,累死了。最主要的是1TB大小只记录了两个命令,所以压缩其实就是来处理这件事的。
rewrite触发条件
- 客户端执行
bgrewriteaof
命令 - auto-aof-rewrite-min-size 64mb # 当前aof文件大于多少字节后才触发
- auto-aof-rewrite-percentage 100 # 当前写入日志文件的大小超过上一次rewrite之后的文件大小的百分之100时,也就是2倍时触发Rewrite
AOF 优点
- 使用AOF 会让你的Redis更加耐久: 你可以使用不同的fsync策略:无fsync,每秒fsync,每次写的时候fsync.使用默认的每秒fsync策略,Redis的性能依然很好(fsync是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,你最多丢失1秒的数据。
- AOF文件是一个只进行追加的日志文件,所以不需要写入seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用redis-check-aof工具修复这些问题。
- Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
- AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
AOF缺点
- 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
- 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。
如何选择RDB和AOF
同时开启
Redis先加载AOF文件来恢复原始数据,因为AOF数据比rdb更完整,但是aof存在潜在的bug,如把错误的操作记录写入了aof,会导出数据恢复失败,所以可以把RDB作为后备数据。
为了考虑性能,可以只在Slave上开启RDB,并且15min备份一次 ,如果为了避免AOF rewite的IO以及阻塞,可以在Redis集群中不开启AOF,靠集群的备份机制来保证可用性,在启动时选取较新的RDB文件,如果集群全部崩溃,会丢失15min前的数据。
混合模式
Redis4.0开始支持该模式。
解决的问题:Redis在重启时通常是加载AOF文件,但加载速度慢。因为RDB数据不完整,所以加载AOF。
开启方式: aof-use-rdb-preamble true
开启后,AOF在重写时会直接读取RDB中的内容。
运行过程:通过bgrwriteaof完成,不同的是当开启混合持久化后
- 子进程会把内存中的数据以RDB的方式写入aof中
- 把重写缓冲区中的增量命令以AOF方式写入到文件
- 将含有RDB个数和AOF格数的AOF数据覆盖旧的AOF文件
新的AOF文件中,一部分数据来自RDB文件,一部分来自Redis运行过程时的增量数据
数据恢复
当我们开启了混合持久化时,启动Redis依然优先加载aof文件,aof文件加载可能有两种情况如下:
- aof文件开头是rdb的格式, 先加载 rdb内容再加载剩余的 aof
- aof文件开头不是rdb的格式,直接以aof格式加载整个文件
优点:既能快速备份又能避免大量数据丢失
缺点:RDB是压缩格式,AOF在读取它时可读性较差
二者动态切换
在 Redis 2.2 或以上版本,可以在不重启的情况下,从 RDB 切换到 AOF :
- 为最新的 dump.rdb 文件创建一个备份
- 将备份放到一个安全的地方
cp dump.rdb dump.rdb.bak
代码块1
- 执行以下两条命令:
# 开启aof
redis-cli config set appendonly yes
# 关闭rdb
redis-cli config set save “”
代码块1234
- 确保写命令会被正确地追加到 AOF 文件的末尾
- 执行的第一条命令开启了 AOF 功能: Redis 会阻塞直到初始 AOF 文件创建完成为止, 之后 Redis 会继续处理命令请求, 并开始将写入命令追加到 AOF 文件末尾
Redis容灾备份
- 开启RDB持久化
save 900 1
save 300 10
save 60 10000
代码块123
- 开启AOF配置
# 开启aof
appendonly yes
appendfilename "appendony.aof"
# rewrite
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# appendfsync always
appendfsync everysec
# appendfsync no
代码块1234567891011
- RDB日志备份,编写脚本定时备份
vim一个redis-rdb-copy-per-hour.sh
#!bin/bash
cur_date=$(date "+%Y%m%d%H%M%S")
rm -rf /usr/local/redis/snapshotting/$cur_date
mkdir -p /usr/local/redis/snapshotting/$cur_date
cp /usr/local/redis/data/dump.rdb /usr/local/redis/snapshotting/$cur_date
del_date=$(date -d -48hour "+%Y%m%d%H%M")
rm -rf /usr/local/redis/snapshotting/$del_date
代码块12345678
使用crontab定时器执行备份脚本
# 打开crontab
crontab -e
代码块12
然后写入(以下为每10秒执行一次,生成环境可以调整一下每小时执行一次):
*/1 * * * * sh /usr/local/redis/bin/redis-rdb-copy-per-hour.sh
*/1 * * * * sleep 10; sh /usr/local/redis/bin/redis-rdb-copy-per-hour.sh
*/1 * * * * sleep 20; sh /usr/local/redis/bin/redis-rdb-copy-per-hour.sh
*/1 * * * * sleep 30; sh /usr/local/redis/bin/redis-rdb-copy-per-hour.sh
*/1 * * * * sleep 40; sh /usr/local/redis/bin/redis-rdb-copy-per-hour.sh
*/1 * * * * sleep 50; sh /usr/local/redis/bin/redis-rdb-copy-per-hour.sh
代码块123456
保存即可
- 恢复方法
-
如果是redis进程挂掉,那么重启redis进程即可,直接基于AOF日志文件恢复数据
-
如果是redis进程所在机器挂掉,那么重启机器后,尝试重启redis进程,尝试直接基于AOF日志文件进行数据恢复
-
如果redis当前最新的AOF和RDB文件出现了丢失/损坏,那么可以尝试基于该机器上当前的某个最新的RDB数据副本进行数据恢复,步骤如下:
① 停止redis(命令是redis-cli shutdown),
② 在配置文件中关闭aof: appendonly no
③ 拷贝rdb日志备份到/usr/local/redis/data目录下
④ 启动redis
⑤ 尝试get一个key,确认数据恢复
⑥ 命令热修改redis配置,使用redis-cli连接redis,使用命令redis config set appendonly yes打开aof方式,这样aof和rdb数据就一致了
⑦ 手动修改redis.conf配置文件中appendonly为yes,因为热修改暂时不会写到配置文件中,所以需要手动修改,然后启动redis,再次确认数据恢复
优化方案
独立部署,硬盘优化
独立部署其实是为了解决子进程开销和优化,不要将 Redis 和其他存储服务或者消息队列服务部署在一起,因为RBD和AOF的文件生成除了内存和硬盘的开销之外,都属于CPU密集型操作。
也可以根据写入量决定是否使用性能更高的 SSD 磁盘。
缓存禁用持久化
如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。
主从模式,从持久化
因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1
这条规则。
如果不Enable AOF ,仅靠 Master-Slave Replication 实现高可用性也可以。能省掉一大笔IO也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave中的RDB文件,载入较新的那个。新浪微博就选用了这种架构。
优化fork处理
如果Enalbe AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。代价一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。默认超过原大小100%大小时重写可以改到适当的数值。
还可以修改配置 no-appendfsync-on-rewrite no
为 yes,让AOF重写期间不要执行正常追加操作。