概述
- Redis 有一个高质量的课题:数据安全性与数据可靠性
- Redis 是一个内存型数据库,数据大部分都是存在内存里面
- 当信息在内存中流通时,Redis 节点突然就故障挂掉
- 当重新启动的时候,内存中的数据肯定是全部丢失了
- 如果在这种情况下,Redis 提供了对应的持久化方案,它有两种持久化方案
- RDB和AOF可以把内存中的数据保存到磁盘,避免数据的流失
- 关于RDB,我们需要了解到
- 什么是持久化
- 什么是快照
- RDB的工作原理,包括它的优点和缺点
- 关于AOF,需要知道
- 为什么需要有AOF
- 存储上线后重写,为什么要重写,以及其触发条件
- 文件如何写入,文件损坏了怎么办
- 它是一个备选方案,或者说一种增强方案
- Redis 既然有了RDB又提供了AOF, 肯定两者之间是会有一个互补的关系
- 这里的话, 我们要去讲他们两者该如何选择, 如何互补?
- 该开启RDB还是开启AOF, 还是说同时都开启
- 同时都开启的情况下,数据冗移是需要考虑
- 从 RDB 动态切换到 AOF 在 Redis 不重启的情况下并保证数据不丢失,如何实现
- 关于 Redis 的备份容灾
- 比如说我们写一个脚本
- 再来一个定时任务,定时的去执行脚本
- 把我们的数据给它备份起来
- 备份了之后,怎么去恢复呢?如何更好的去恢复
- 关于Redis 的优化方案
- 更好的提升性能,从硬盘角度,从fork进程角度
- 以及从主从的角度
Redis 持久化方式
1 )通常数据库存在三种用于持久操作以防止数据损坏的常见策略:
- 是数据库不关心故障,而是在数据文件损坏后从数据备份或快照中恢复,RDB就是这种情况
- 该数据库使用操作日志记录每个操作的操作行为,以在失败后通过日志恢复一致性。由于操作日志是按顺序追加写入的,因此不会出现无法恢复操作日志的情况。类似于Mysql的重做(redolog)和撤消日志(undolog)
- 数据库不修改旧数据,而仅通过追加进行写入,因此数据本身就是日志,因此永远不会出现数据无法恢复的情况。CouchDB是一个很好的例子,AOF类似这种情况
2 )严格上讲Redis为持久化提供了三种方式:
- RDB:在指定的时间间隔能对数据进行快照存储,类似于MySQL的dump备份文件
- AOF:记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据(MySQL的binlog)
- RDB与AOF混合使用,这是Redis4.0开始的新特性,在混合使用中AOF读取RDB数据重建原始数据集,集二者优势为一体
RDB持久化
1 )初始化环境
1.1 创建配置/数据/日志目录
# 创建配置目录
mkdir -p /usr/local/redis/conf
# 创建数据目录
mkdir -p /usr/local/redis/data
# 创建日志目录
mkdir -p /usr/local/redis/log
1.2 配置文件
-
创建一份配置文件至 conf 目录
vim /usr/local/redis/conf/redis.conf
-
文件内容如下
# 放行访问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
-
通过这个配置文件,启动: bin/redis-server conf/redis.conf
-
验证:ps -ef | grep redis
-
客户端连接 bin/redis-cli -a 123456
- 这里密码明文不推荐,仅供参考
1.3 数据准备
- 查看数据库数据大小:$
DBSIZE
目前新环境没有任何数据显示 0 - 插入一些数据来演示RDB中一些阻塞的命令,若数据集较小则看不到任何效果
- 我们通过一个python文件,循环插入 500 w 数据,使用 redis 管道的方式来插入大概30s-1min
- 不用管道,则可能要 30min-1h, 非常慢
- $
cd bin/ && vim 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))
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
- 执行 python initdata.py | ./redis-cli -a 123456 --pipe
- 过个几十秒,查看 $
DBSIZE
- 之后,查看内存使用情况:$
info memory
可看到,大约 438M 的内存占用 - 进入 data 目录查看 dump.rdb 是否生成,发现并没有,说明RDB持久化并没有去做
- 如果强制被 kill 掉 redis 模拟故障,内存数据将会全部丢失,所以需要对内存数据做持久化处理
- 在一些业务场景下,内存数据是需要被保存下来的,就需要设置 RDB
- 现在修改配置文件 $
vim conf/redis.conf
, 添加如下,并重启 redis# 5秒有一个key的改动,就执行快照生成 save 5 1
- 这么这个配置用来模拟我们的故障,实际上缺省配置如下
# 900秒内如果超过1个key改动,则发起快照保存 save 900 1 # 300秒内如果超过10个key改动,则发起快照保存 save 300 10 # 60秒内如果超过1W个key改动,则发起快照保存 save 60 10000
- 还是通过模拟故障的配置来看
- 启动后,重新执行py脚本插入数据
- 数据插入完成后,进入 data 目录,发现已经有 dump.rdb 文件了
- 而且还多了 temp-1428.rdb 类似的文件
- 这个 temp 文件是其内部的生成工作原理
- 再次模拟故障,可发现数据仍存在
- 因为我们配置中写的是 5s, 那如果配置成 15min 的时间,那没有生成快照数据还会丢失
- AOF的出现,弥补了这一个缺陷
Redis 快照
- 快照,顾名思义可以理解为拍照一样,把整个内存数据映射到硬盘中,保存一份到硬盘,因此恢复数据起来比较快,把数据映射回去即可,不像AOF,一条条的执行操作命令
- 快照是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb,可以通过配置设置自动做快照持久化的方式
- 产生快照的情况有以下几种:
- 手动 bgsave 执行
- 手动 save 执行
- 注意:
- BGSAVE 命令是非阻塞的
- 本质上是folk出一个子进程在后台做快照生成, 仍然能对外提供服务
- 对外提供的是 get ,但是 save 这种写操作就不一样了
- SAVE 命令是阻塞的
- 主进程来做这件事,你来访问我的时候
- 我在忙的时候肯定提供不了服务给你
- 同时,SHUTDOWN 命令也是阻塞的
- 要先保存好数据
- BGSAVE 命令是非阻塞的
- 根据配置文件自动执行
- 客户端发送 shutdown, 系统会先执行 save 命令阻塞客户端,然后关闭服务器
- 当有主从架构时,从服务器向主服务器发送sync 命令来执行复制操作时,主服务器会执行 bgsave操作
RDB 工作原理
-
Redis默认会将快照文件存储在Redis当前进程的工作目录中的dump.rdb文件中
-
可以通过配置dir和dbfilename两个参数分别指定快照文件的存储路径和文件名
-
流程过程如下(rdb.c中)
-
这个图,也是通过源码 rdb.c 来总结的
-
serverCron 是一个循环时间事件函数
- 这个函数它的作用就是来监控配置文件的
- 它就监控配置文件里边的save配置项
- 当这个时间满足,就开始触发,触发之后,最终也会执行 bgsave 的底层流程
-
bgsave 内部调用 bgsaveCommand, 它fork子进程做持久化,是非阻塞的
-
save 也是调用 saveCommand, 它是主进程的,它会阻塞,不对外提供读的服务,全心全意持久化
-
也就是说:serverCron, bgsave, save 三者都在做持久化的事情,都会有 rdbSave的流程
- 先写一个 temp 的文件
- 然后这个临时文件开启之后,会把之前的数据先全部通通的加载过来
- 加载过来之后,把新的内存里边的数据给它写进去
- 写进去持久化之后,然后刷新,关闭
- 关闭以后,把这个临时文件内改一个名字,再改成原始的你的文件名把它覆盖一下
- 这个就对应上面 temp-1428.rdb 的文件
-
这就是 Redis RDB的一个工作原理
RBD的优缺点
1 )优点
- 紧凑压缩的二进制文件
- $
vim dump.rdb
可看到 - 备份非常简单
- $
- fork子进程性能最大化
- 非阻塞,可对外继续提供服务
- 启动效率高
- 二进制文件,读取数据快
2 )缺点
- 生成快照的时机问题
- save 5 1 这种 5s 触发一次只是我们的模拟
- 一般生产会配置 15min 和 30min, 在较长的时间窗口期宕机,数据也会丢失
- fork子进程的开销问题
- 数据集比较大,频繁做这件事,会导致性能开销
AOF持久化
- 它也是Redis持久化的重要手段之一,AOF(Append Only File)只追加文件
- 也就是每次处理完请求命令后都会将此命令追加到aof文件的末尾
- 而RDB是压缩成二进制等时机开子进程去干这件事
- 弥补了save配置时间内宕机风险的问题
1 ) 开启AOF
-
通过配置进行启动,默认是关闭的
# 默认 appendonly 为 no appendonly yes appendfilename "appendonly. aof" # RDB文件和AOF文件所在目录 dir /usr/local/redis/data
-
基于此,我们可以开启两个窗口验证下
- 窗口1,追踪文件变动,执行 $
tail -f data/appendonly.aof
- 这个文件中会记录写命令,不会记录读命令
- 窗口2,进行读写操作,执行 $
set username zhagnsan
这是写命令
- 窗口1,追踪文件变动,执行 $
-
在窗口1 中,输出
*2 $6 SELECT $1 0 *3 $3 SET $8 username $8 zhangsan
- 这里, *2 代表下面2行是命令,*3表示下面三行是命令
- $6 不在行计算范围内,$6 表示命令的字节长度 SELECT 长度是6
- 可忽略 $ 相关来看, 就是:
SELECT 0
set username zhangsan
-
所以,可见,aof 文件的优点是易读,缺点是容易让人读不懂
-
这样,即使 dump 文件被删除,也可以通过 aof 来恢复了
2 )同步策略
- Redis中提供了3种AOF同步策略:
- 每秒同步(默认,每秒调用一次fsync,这种模式性能并不是很糟糕)
- 每修改同步(会极大消弱Redis的性能,因为这种模式下每次 write后都会调用 fsync)
- 不主动同步(由操作系统自动调度刷磁盘,性能是最好的, 注意:不主动不代表不写入)
- 参考
#每秒钟同步一次,该策略为AOF的缺省策略 appendfsync everysec #每次有数据修改发生时都会写入AOF文件 appendfsync always #从不同步。高效但是数据不会主动被持久化 appendfsync no
3 )AOF 工作原理
-
client ----> Redis Server ----> 执行命令 ----> 命令写入AOF缓冲区 ----> 刷新磁盘写入AOF文件
-
解释一下
- AOF的频率高的话肯定会对Redis带来性能影响,因为每次都是刷盘操作
- 跟mysql一样了,Redis每次都是先将命令放到缓冲区
- 然后根据具体策略(每秒/每条指令/缓冲区满)进行刷盘操作
- 如果配置的always,那么就是典型阻塞,如果是everysec
- 每秒的话,那么会开一个同步线程去每秒进行刷盘操作,对主线程影响稍小
4 )写入文件与恢复
-
AOF文件是一个只进行append操作的日志文件,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。假如一次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过 redis-check-aof 工具来帮助我们修复问题。
-
AOF文件有序地保存了对数据库执行的所有写入操作,这些写入操作以Redis协议的格式保存,因此AOF文件的内容非常容易被人读懂,对文件进行分析(parse)也很轻松
-
导出(export)AOF文件也非常简单:举个例子,如果你不小心执行了 FLUSHALL 命令,但只要AOF文件未被重写,那么只要停止服务器,移除AOF文件末尾的 FLUSHALL 命令,并重启Redis, 就可以将数据集恢复到 FLUSHALL 执行之前的状态
5 )重写
-
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触发条件
- 当 aof 文件越来越大,恢复的时候会降低性能
- 客户端执行
bgrewriteaof
命令,内部有些配置什么时候重写- auto-aof-rewrite-min-size 64mb 这里文件超过 64m的时候会重写,生产环境要配置大些
- auto-aof-rewrite-percentage 100 这种是按百分比来配置的
7 )常用的配置
# fsync 持久化策略
appendfsync everysec
# AOF重写期间是否禁止fsync;如果开启该选项,可以减轻文件重写时CPU和硬盘的负载(尤其是硬盘)
# 但是可能会丢失AOF重写期间的数据
# 需要在负载和安全性之间进行平衡
# 默认是 no , aof 在重写期间是可能有新的命令写入的,no则表示会记录新写入的命令,yes 则不记录
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
8 )AOF 优点
- 数据不易丢失
- 自动重写机制 (保证aof文件不会无休止膨胀)
- 易懂易恢复
9 )AOF缺点
- AOF文件恢复数据慢
- AOF持久化效率低