漫画redis持久化&主从复制
内存和磁盘
为了方便理解,假设某公司暂存文件资料的
redis柜子
,有多个格子,柜子还有一个专门的管理员小张
,他的工作就是按公司员工的命令在某个格子(key)存资料(value),取资料(value)。
这个柜子在办公室内为了方便快速存取,也不关门也不锁,总之就是方便。
公司除了这个柜子
可以存储资料,还有一个专门的资料仓库
也可以存储资料,而且仓库有专人守护,保证资料一般情况肯定丢不了。
这个小张
和柜子
就是是redis,其中柜子
就是内存,存取很快,但一重启数据就清空。
这个资料仓库
就是磁盘,存取比较慢,但文件不会丢失。
公司员工就相当于客户端,可以给小张
发命令存取资料。
柜子只有一个管理员小张
对应redis的单线程。
本文例子里的所有人物就相当于执行程序的cpu
,脑子记性为0,只能靠纸笔记录。
RDB
某天有个领导说这个
快照柜子
用的很方便,但希望资料不会丢失。
于是有人提出方案,方案名叫RDB,思路比较简单,每隔一段时间,把当前资料存储的位置和内容全都记录到一个快照文件里,并把快照文件放到存储仓库
,快照文件的格式类似如下
某天保洁来了把柜子
清空了,资料都丢失了(相当于redis重启),按仓库
中的快照文件还原资料即可。
具体隔多久存一次呐?可以有多种原则,比如 "60 秒内有至少有 100 个格子被改动"就快照一次。
redis:
RDB快照(snapshot)是redis默认的持久化方案,只要设置好保存的时间策略(可以多个)就默认开启rdb,除此之外还可以设置快照文件名和保存的路径等:
save 3600 1 #3600 秒1次key变更
save 300 100 #300秒100次key变更
save 60 10000 #60 秒10000次key变更
dbfilename dump.rdb ##存储文件名
dir ./ ##存储路径
存储好之后,下次重启会直接按rdb文件恢复数据(实现了持久化)。
rdb文件就保存在存储路径下
内容如下(以二级制的形式压缩保存)
以二级制的形式压缩保存
刚开始
工作图小张
自己去记录快照并去资料仓库存储,但是这个过程很慢,导致这期间其他人想存资料只能等。
为了解决这个问题,公司新给他配加一个人小李
,这样小张
还是干之前的活,小李
负责在规定条件下记录并存储快照文件,在小李
工作的期间(记录并去资料仓库存储),如果有新的存储变动,小张
还会通知小李
。
redis:
上面小张
相当于主线程,由主线程去快照并持久化的方式叫save
,该过程是同步的阻塞客户端请求,小李
加入相当于新fork一个子线程专门处理rdb,这种叫bgsave(background save)
,该过程是异步的不阻断客户请求,而且过程中有新的变化还会同步(Copy-On-Write),redis默认使用bgsave
。
总结一下:bgsave
肯定在性能上优于save
,因为不阻塞请求,但是新开一个进程肯定要耗费更多资源。
RDB方案有一个致命的问题,
小李
需要触发一定条件才回去做快照工作,比如十分钟做一次,但在这十分钟内保洁来了把柜子清掉了(相当于redis重启),这十分种内新增的资料小李
还没来得及记录就丢了,而且再也找不回来了。
redis:
与其对应,rdb存储最大的问题就是容易造成数据丢失,比如新数据还没来得及做快照,redis就重启了,那么这些新增的数据就丢了。
AOF
为了解决文件丢失的问题,公司提出一个新方案,名曰AOF(append-only-file),就是让柜子管理员
AOF小张
在柜子
里放一张纸,每次收到员工的存储请求,处理完就把请求内容用记录到纸上,并在恰当的时机把记录转到仓库
(落盘)。
那么什么是恰当的时机呐?公司提出三种方案:
PlanA.每次有存储请求就记录一次。
PlanB.每隔一秒记录一次。
PlanC.公司下发的命令就记录一次。
小张
可以根据公司要求切换方案。
有了这份AOF文件,当保洁清理掉所有柜子文件后,只要按照AOF文件的记录一条一条执行就会恢复柜子原来的样子。
再看一眼AOF的记录
AOF记录
第一条和第四五条分别是01格存了A,B,C,这样显得文件有太多没用的指令,因为最终目的是为了恢复数据,所以只要存第五条:“在01格存储了C”即可,前两条数据多余,而且会造成文件大,读写时间慢,恢复数据也慢。
这时候本分案另一个闲置人员小李
登场了,他负责整理AOF文件,也就是重写,结果如下:
AOF重写
redis:
以上过程就是redis的AOF,开启AOF持久化方式,需要修改配置
appendonly yes # 开启AOF
appendfsync always # 每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,也非常安全。
appendfsync everysec# 每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。
appendfsync no # 从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择。
其中appendfsync三选一,分别对应实时同步,每秒同步,操作系统同步三种存储方式,安全性从高到低,效率从低到高,默认是everysec。
开启之后执行一条指令
set pq 217
查看AOF文件
里面的内容如下
aof文件
其实就是记录每条命令。
上例中
小李
的工作就是AOF重写,目的就是删除多余指令,redis是通过新fork一个线程来做重写的,可以通过配置auto-aof-rewrite-
参数来调整重写的策略。对比一下: RDB方式不安全容易丢失数据,AOF比较安全,但是文件大、一条条命令执行来恢复数据时间长(即使有重写也达不到rdb的效果)
混合持久化
RDB、AOF两种方案各有优缺点,于是很快就有人发现了,为什么不两种方式混合使用呐。
混合持久化
方案如下:
小李
再做重写的时候看一眼AOF文件,比如当前行是10行,记住,然后新做一个文件,存储RDB格式的快照数据,在这期间如果有新的指令来了小张
会存入原AOF文件,小李
做完快照看一眼原AOF文件,比如到了12行,代表这期间新增了两行,那么把这两行剪切下来粘到新文件中,最后用新的文件替换并覆盖原AOF文件(最终的AOF文件两种格式混搭),恢复时候先恢复RDB部分,再执行少量的AOF命令,很快就恢复了数据而且还保证了安全。
redis:
使用redis很少使用 RDB方式来进行持久化,因为会丢失大量数据。通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 RDB来说要非常慢,所以使用混合持久化方式最好,开启混合持久化需要配置(默认是开启的)
aof-use-rdb-preamble yes # 开启混合持久化
从上面的例子可以看出混合持久化是基于AOF的重写(只是使用RDB技术),所以AOF必须开启(并且配置重写策略),重写后的AOF一般如下
混合持久化的AOF
开启之后的好处上面也说了,安全的同时又重启快。
主从复制
公司不断壮大,用柜子的人也越来越多,而且大部分是为了查看,柜子倒是够用,但
主从结构小张
及其疲惫,为了减轻小张
的压力,公司决定多上几个柜子,每个柜子都配置不同的管理员,为了资源统一,每个柜子存放的文件一模一样,大家需要存资料统一找小张
,需要读资料去找其它柜子的管理员即可,这样大大减轻了小张
的压力。
按以上方案,小张
是主,其它管理员是从,这就是一个典型的主从结构。
那么问题来了,要保证每个柜子存放的文件一模一样,怎么实现主从复制?
注意:上面介绍了持久化的方案,但是在主从复制时不一定开启了持久化,资料库
可能不存在AOF和RDB文件,所以不能依靠持久化来解决
方案如下:
比如下面的某个柜子管理员叫小美
1.小美
告诉小张
我要同步数据
2.小张
通知下属小李
做一个RDB快照文件(bgsave),与此同时小张
继续接受请求,并记录请求的内容(类似AOF,但不存入资料仓库)
3.小李
做完RDB快照也不用存库,直接发给小美
4.小美
清空柜子,按RDB快照重新布置柜子
5.小张
把这段时间新增的请求指令发给小美
6.小美
按新增命令修改柜子数据,此时两个柜子数据一致
7.之后小张
接受新命令时自己存完,再告诉小美
存一下,此时两个柜子实时同步
redis:
redis实现主从复制就是按照上面例子的思路来的:
master收到同步命令后,会通过bgsave异步生成最新的rdb快照文件,这期间期间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。rdb完成以后,master会把这份rdb发送给slave,slave会把接收到的数据加载到内存中。然后,master再将之前缓存在内存中的指令集发送给slave。
主从复制(断点续传)
还是上面的例子,
断点续传小美
的柜子通过主从复制一直保持和小张
一致,但是问题出现了:
小美
有时候要去上厕所(从节点掉了),那么这段时间小张
柜子里的新增的数据就没办法同步到小美
的柜子里了。
为了解决这个问题,小张
每次接收到新的写命令都会记录在一个小本上存到柜子里(内存),而且只保留最近的几条记录,比如10条(不能存太多,一是查找费劲,二是占用柜子空间),其它都删掉,并且每条记录都有序号01,02,03...,再同步命令的时候,小美
也记录下自己当前的序号(偏移量),这样小美
上厕所回来后只要告诉小张
序号,小张
把序号后面的命令发给小美
,就完成了断点续传。
如果小美
身体不舒服,去厕所去了很久,回来之后把序号报给小张
,小张
发现这个序号早就不在自己的小本本上了,这时候就按照之前的初始复制方案整体复制一遍即可。
redis:
断点续传基本就是如上例这么个方式,master会在其内存中开辟缓存队列,缓存最近一段时间的指令,master和它所有的slave都维护了复制的数据下标offset,因此,当网络连接断开后,slave会请求master继续进行未完成的复制,从所记录的数据下标开始。如果从节点数据下标offset太旧,已经不在master的缓存队列里了,那么将会进行一次全量数据的复制。
总结
其实持久化也好,主从复制也好,redis的诉求都一样,就是不阻塞用户请求,在jvm垃圾回收也有类似的诉求,即尽量避免stw(stop-the-world)。
而解决的方案简单概括:增量更新,也就是对于同一数据后台线程一边记,用户请求一边改,在过程中记录改了什么,最后把结果按改动内容调整一下就完成了最终的数据备份。
over~新人求赞!