面试的经典问题就是会问:Redis为什么快呢?
我们都知道Redis是基于内存的数据库,数据都在内存中不需要跟磁盘交互,
- Redis采用的是多路复用IO
- Reids的通信协议是
RESP
网络协议, - Redis的数据结构就基本数据类型的字符串底层也对C语言默认的字符串类型优化成动态字符串
- …
上面我们说了Redis的数据都是在内存中,当Redis实例挂了,数据不就丢失了?所以Redis他就有一种机制来保证Redis实例的数据不会因为故障而导致丢失数据就Redis的持久化机制
Redis的持久化
Redis 的持久化机制有两种,第一种是快照(RDB),第二种是 AOF 日志。快照是一次全量备份, AOF 日志是连续的增量备份。快照是内存数据的二进制序列化形式,在存储上非常紧凑,而 AOF 日志记录的是内存数据修改的指令记录文本。 AOF日志在长期的运行过程中会变得无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长,所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身。
Redis的俩种持久化图:
持久化机制的快照原理:
我们知道 Redis 是单线程程序,这个线程要同时负责多个客户端套接字的并发读写操作和内存数据结构的逻辑读写。在服务线上请求的同时, Redis 还需要进行内存快照,内存快熙、要求 Redis 必须进行文件 IO 操作,可文件 IO 操作不能使用多路复用 API这意昧着单线程在服务线上请求的同时,还要进行文件 IO 操作,而文件 IO作会严重拖累服务器的性能。
还有个重要的问题,为了不阻塞线上的业务, Redis 就需要一边持久化,一边响应客户端的请求。持久化的同时,内存数据结构还在改变,比如一个大型的 hash
字典正在持久化,结果一个请求过来把它给删掉了,可是还没持久化完呢,这该怎么办呢?
Redis 使用操作系统的多进程 COW (Copy On Write)机制来实现快照持久化(了解一下即可,操作系统层面了 是否也有看到java安全的ArrayList集合了 CopyOnWriteArrayList了)
fork(多进程)
这段是为上文,Redis是单线程怎么解决一边持久化一边处理客户端请求
Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。这时你可以把父子进程想象成 个连体婴儿,它们在共享身体。这是 Linux 操作系统的机制,为了节约内存资源,所以尽可能让它们共享起来。在进程分离的瞬间,内存的增长几乎没有明显变化。
原理:
子进程做数据持久化,不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是父进程不一样,它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改。这个时候就会使用操作系统的 cow 机制来进行数据段页面的分离。如果 所示,数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面
是没有变化的,还是进程产生时那一瞬间的数据。
随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增长,但是也不会超过原有数据内存的2倍大小。另外, Redis 实例里冷数据 占的比例往往是比较高的,所以很少会出现所有的页面都被分菌的情况,被分离的往往只有其中一部分页面。每个页面的大小只有 阻,一个 Redis 实例里面一般都会有成千上万个页面
子进程因为数据没有变化,它能看到的内存里的数据在进程产生的 瞬间就凝
固了,再也不会改变,这也是为什 Redis 的持久化叫“快照”的原因。接下来子进
程就可以非常安心地遍历数据,进行序列化写磁盘了。
AOF原理
AOF 曰志存储的是 Redis 服务器的顺序指令序列, AOF 日志只记录对内存进行修改的指令记录假设 AOF 日志记录了自 Redis 实例创建以来所有的修改性指令序列,那可以通过对一个空的 Redis 实例顺序执行所有的指令一一也就是“重放”,来恢复Redis 当前实例的内存数据结构的状态
Redis 会在收到客户端修改指令后,进行参数校验、逻辑处理,如果没问题,就将该指令文本存储到 AOF 曰志中,也就是说,先执 指令才将曰志存盘 这点不同于 level db hbase 等存储引擎,它们都是先存储曰志 做逻辑处理。
Redis 在长期运行的过程中, AOF 的日志会越来越长 如果实例宕机重启,重放整个 AOF 曰志会非常耗时,导致 Redis 长时间无法对外提供服务,所以需要对 AOF日志瘦身。
AOF重写
Redis 提供了 bgrewriteaof 指令用于对 AOF 曰志进行瘦身,其原理就是开辟一个子进程对内存进行遍历,转换成一系列 Redis 的操作指令,序列化到一个新的 AOF曰志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF日志文件中,追加完毕后就立即替代旧的 AOF 曰志文件了,瘦身工作就完成了。
fsync
AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将脏数据刷回到磁盘的。
这就意昧着如果机器突然宕机, AOF 日志内容可能还没有来得及完全刷到磁盘中,这个时候就会出现日志丢失。那该怎么办?
Linux glibc 提供了 fsync(int fd)函数可以将指定文件的内容强制从内核缓存刷到磁盘。只要 Redis 进程实时调用 fsync 函数就可以保证 AOF 日志不丢失。但是fsync 是一个磁盘 IO 操作,它很慢!如果 Redis 执行一条指令就要 fsync 一次,那么Redis 高性能的地位就不保了所以在生产环境的服务器中, Redis 通常是每隔 ls 左右执行一次 fsync 操作,这ls 的周期是可以配置的。这是在数据安全性和性能之间做的一个折中,在保持高性能的同时,尽可能使数据少丢失。
Redis 同样也提供了另外两种策略,一个是永不调用 fsync一一让操作系统来决定何时同步碰盘,这样做很不安全,另一个是来一个指令就调用 fsync 一次一一结果导致非常慢。这两种策略在生产环境中基本不会使用,了解一下即可。
注意:(大快写 快照是基于COW机制的,page页的,大块写就是当页操作很多的时候)
- 快照是通过开启子进程的方式进行的,它是一个比较耗资源的操作。遍历整个内存,大块写磁盘会加重系统负载。
- AOF 的fsync 是一个耗时的 IO 操作,它会降低 Redis 性能,同时也会增加系统IO 负担。所以通常 Redis 的主节点不会进行持久化操作,持久化操作主要在从节点进行。从节点是备份节点,没有来自客户端请求的压力,它的操作系统资源往往比较充沛。但是如果出现网络分区,从节点长期连不上主节点,就会出现数据不一致的问题,特别是在网络分区出现的情况下,主节点一旦不小心宕机了,那么数据就会丢失,所以在生产环境下要做好实时监控工作,保证网络畅通或者能快速修复。另外还应该再增加一个从节点以降低网络分区的概率,只要有一个从节点数据同步正常,数据也就不会轻易丢失。
Redis4.0的混合持久化
重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志相对于使用 rdb 来说要慢很多,这样在Redis 实例很大的时候,启动需要花费很长的时间。
Redis 4.0 为了解决这个问题,带来了一个新的持久化选项一一混合持久化。如所示,将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志
不再是全量的曰志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF日志,通常这部分 AOF 日志很小。
于是在 Redis 重启的时候,可以先加载 rdb 的内容, 然后再重放增量 AOF 日志,就可以完全替代之前的 AOF 全量文件重放,重启效率因此得到大幅提升。
面试
上面我们都说了Redis的持久化机制有哪些,跟原理的实现,那面试的时候该怎么组合呢?
Redis的俩种持久化策略 RDB跟AOF
RDB策略是发送一个bgsave指令fork一个子进程,然后由子进程去负责生成RDB文件,父进程还可以继续处理命令请求,不会阻塞进程。
AOF是通过保存redis服务器所执行的写命令来记录数据库状态的。通过系统的fsync命令来实现刷盘的。
解释一下:
AOF通过追加、写入、同步三个步骤来实现持久化机制。
- 当AOF持久化处于激活状态,服务器执行完写命令之后,写命令将会被追加append到aof_buf缓冲区的末尾
- 在服务器每结束一个事件循环之前,将会调用flushAppendOnlyFile函数决定是否要将aof_buf的内容保存到AOF文件中,可以通过配置appendfsync来决定。
always ##aof_buf内容写入并同步到AOF文件
everysec ##将aof_buf中内容写入到AOF文件,如果上次同步AOF文件时间距离现在超过1秒,则再次对AOF文件进行同步
no ##将aof_buf内容写入AOF文件,但是并不对AOF文件进行同步,同步时间由操作系统决定
如果不设置,默认选项将会是everysec,因为always来说虽然最安全(只会丢失一次事件循环的写命令),但是性能较差,而everysec模式只不过会可能丢失1秒钟的数据,而no模式的效率和everysec相仿,但是会丢失上次同步AOF文件之后的所有写命令数据。
参考:Redis深度历险的书