我在复盘自己的微博系统的时候突然想到了一个问题。我的点赞关注模块,是利用了Redis当中的一些数据类型的特殊性质来实现的。可是Redis是缓存数据库,而且我并没有对我的这些数据做持久化,那如果Redis宕机了,我所保存的这些数据岂不是都要丢失了?带着这个问题我专门去查了查。哦,redis原来有自己的持久化策略,那么有了这些策略是不是万无一失呢?它和MySQL的持久化有什么区别呢?它能不能在某些场景替代掉MySQL呢?带着这些问题,我开始了对redis持久化的学习......
Reids的持久化策略:AOF持久化
当我的项目将点赞关注的数据存入到redis中后,此时redis宕机了,redis有什么办法将数据回复呢?redis当中的第一种持久化技术叫做AOF,他会将redis中的数据,以命令的格式存入到磁盘当中,当redis宕机以后就会重新读取AOF文件并且重新执行里面的命令,从而恢复数据(因为我们保存每一条数据的时候本身也相当于在redis中执行命令,将数据保存成命令的方式不得不说是一个好办法)。
数据在存入数据库之后,也就是在执行完写命令以后就会对该数据进行aof(读操作没有意义)。
注意,这里是在命令执行后再进行保存,这样做可以保证存入的命令都是有效的(以最终是否存入redis为标准判断是否有效)。还能保证不影响写的操作,因为写肯定是重点,aof只是对redis的持久化策略。
aof操作也有风险:
1.因为是先执行命令再aof,如果在执行完命令后redis宕机,那么还没有aof的数据就丢失了。
2.虽然不会阻塞当前的写入命令,但是有可能会后面的命令带来阻塞的风险。
因为aof这个操作是在主进程中进行的,而执行命令操作也是在主进程中进行的,也就是说两个操作时同步的。如果当aof文件写入硬盘使恰好服务器的I/O压力很大,就会导致写入很慢,这样的话就会阻塞后面的操作。这样看的话,这两个问题都和aof写入硬盘的时机有关。
redis的三种写回策略:
1.Always:每次在命令执行结束后,同步将aof日志数据写回硬盘。
2.Everysec:每次在命令执行结束后,将先将命令写入到aof的内核缓冲区,然后每隔一秒,将缓冲去中的内容写入到硬盘
3.No:意味着写入磁盘的操作将不由redis来控制,而将这个流程转交给操作系统,redis写入到内核缓冲区后,由操作系统来决定写入磁盘的时机。
可是要是将数据无限的写回磁盘,有的时候有的数据已经被修改过了,但是它旧版的数据的保存命令依旧存在于磁盘的aof文件上,这样的话aof文件里就会保存很多没用的数据,而且会导致aof文件会“无限变大”。所以redis提供了aof重写机制,来定期更新磁盘中的aof文件,消除冗余,只保存目前存储在redis中的数据
AOF文件的重写机制:
aof的重写机制就是读取当前数据库的所有键值对数据,将它们以命令的方式保存在一个新的aof文件当中,当保存完毕之后,用新的aod文件替换旧的aof文件。
为什么不在原文件中直接进行更新呢?因为如果在原文件中进行跟新时,redis宕机了,这样就会污染原本的aof文件,可能让原文件失去对数据的准确恢复性。所以在重写的过程中,如果发生意外,直接删点这个新的aof文件即可。
AOF的后台重写:
随着aof文件的不断增大,当它超过某个阈值的时候,redis就会触发重写机制。这个重写操作不同于写回,它会将目前数据库当中的所有数据进行备份,这无疑是一项庞大的工作,将其放在主进程中进行显然是不合适的。
redis一般采用后台重写,会创建一个子进程来进行重写aof文件的操作,然后讲主进程的页表复制给子进程。而为什么不用采线程呢,那么假设我们启用了线程进行这个操作,那么数据库中的数据相当于共享变量,如果此时数据库中有新的数据插入,即对共享变量进行写操作,那么为了保证数据的安全,就要给共享变量进行加锁操作,那么子线程(也就是重写线程)就会阻塞,这样效率就会大大降低。而redis采用了子进程来实现该操作,主进程先会将包含虚拟地址与物理地址的页表复制一份给子进程,然后此时也不会复制物理内存,是为了节约物理内存。
读到这里,就会有问题产生,那么在子进程备份新的aof文件的时候,无疑需要读取redis中的数据,也就是共享内存,此时如果后台重写已经开启,就会出现两个问题:
1.后台重写已经备份过的数据被主进程修改。
2.主进程修改了没有备份过的数据,那会对子进程的重写产生影响吗?
针对第一个问题,此时重写已经开始,已经重写成功的数据在主进程中被修改,那么此时子进程重
写的数据就是一个过期数据,并没有对这个新的数据进行保存。我查阅了一些资料,有的说是此时会触发写时复制机制,备份出主进程在此期间修改过的数据;也有的说是此时会将这个数据写入重写缓冲区,然后再追加到新的aof文件末尾。我整理了一下,针对这个问题,我是这样理解的:
因为子进程在重写的过程中只需要对共享内存进行读操作,而主进程因为还在继续工作,此时有需求,写入了一个新的数据进来,那么共享内存已经发生了变化,而redis本身是无法确定写入的是已经备份过的还是没有备份过的,对于这种随意改变共享内存的行为,显然是不安全的。所以redis在重
写操作开始以后,当主进程进行写入操作时,触发一个写时复制的操作,将新的数据不直接写入内存当中,而是复制一份内存(重写缓冲区),然后放入到这个缓冲区中,这个缓冲区只保存在重写过程中插进来的新的数据。在重写结束后再将缓冲区中的数据同步到新的aof文件末尾。这样即使新的aof文件中的前面保存了旧的数据,但是后面依然会由缓冲区同步过来的命令将这些旧数据更新。
因为redis无法确定或者分清是否插入的数据是否已经被重写,索性就将此时所有的数据都同步到缓冲区中,最后再统一加到新的aof文件末端。最后再由新的aof文件替换掉原有的aof文件,此时重写就结束了。