因为有小伙伴问Redis的bgsave命令里面,cow(copy on write)到底是如何实现的,所以顺便复习一下RDB相关的知识点。
1.RDB的基本概念
Redis有两种数据持久化的方式:AOF和RDB。
简单来说,AOF是记录数据增量的方式,将每次对服务器写的操作存入日志(类似MySQL的binlog);而RDB是记录全量数据,根据指定的时间间隔对数据进行快照存储,以二进制格式文件(后缀RDB)保存在硬盘当中。
2.RDB的触发方式
2-1、配置文件
最常见的使用RDB进行持久化的方式,是在配置文件中配置Redis进行快照保存的时机:
save [seconds] [changes]
意为在[seconds]秒内如果发生了[changes]次数据修改,则进行一次RDB快照保存,例如
save 60 100
可以配置多条save指令,让Redis执行多级的快照保存策略。
Redis默认开启RDB快照,默认的RDB策略如下:
save 900 1
save 300 10
save 60 10000
2-2、手工触发
也可以直接使用手工命令的方式触发RDB生成快照文件。
一种是 save 命令
redis> save
OK
save 命令是同步方式生成快照,会造成Redis阻塞,所有后续到达的命令要等待save完成以后才能执行。
另一种是 bgsave 命令
redis> bgsave
Background saving started
bgsave 命令采用异步方式生成快照,Redis会fork出一个子进程进行RDB文件的生成。
Redis只有在fork子进程时被阻塞,子进程完成快照生成的同时,Redis可以正常工作。
2-3、其他触发方式
- 主从复制时,自动生成RDB文件
- Redis中的debug reload提供debug级别的重启(不清空内存),此时自动生成RDB文件
- shutdown会自动生成RDB文件
3.bgsave的工作流程
重点说一下 bgsave 是如何使用异步方式生成快照的。
一般资料提到这里的时候都是一句话带过,说Redis创建子进程以后,利用cow方式完成快照文件的生成。这没有错,但是大多数都没说清楚这个cow是如何工作的。
我甚至在一些博客上看到“fork消耗额外内存”、“fork时对内存的消耗比较大”这样的说法。
这其实是没有理解清楚Redis fork出来的子进程是如何工作的。
3-1、什么是cow
cow = copy on write
这是一种简单的读写分离思想,适用于读多写少的并发场景。比如黑白名单,热点文章等等。
正常情况下我们说cow,指的是修改共享资源时,将共享资源copy一份,加锁后修改,再将原容器的引用指向新的容器。
对于java来说,是有线程的cow容器的,比如CopyOnWriteArrayList。
另外就是cow保证的是最终一致性而不是强一致。
3-2、Redis面临的问题
在Redis生成快照这个问题上,显然不能直接使用标准的cow流程来操作。
很简单,这会导致Redis的可用内存容量就直接减半。
cow的第一步是要将Redis在内存中的内容copy一份副本;然后主进程操作原数据,进行正常的读写操作,子进程利用副本专心写盘,写完以后销毁子进程。
真的直接copy一份副本的话,多少内存够用啊?这不是简单的copy一个java容器那么简单。
3-3、Redis的cow
- Redis创建子进程以后,根本不进行数据的copy,主进程与子线程是共享数据的。主进程继续对外提供读写服务。
- 虽然不copy数据,但是kernel会把主进程中的所有内存页的权限都设为read-only,主进程和子进程访问数据的指针都指向同一内存地址。
- 主进程发生写操作时,因为权限已经设置为read-only了,所以会触发页异常中断(page-fault)。在中断处理中,需要被写入的内存页面会复制一份,复制出来的旧数据交给子进程使用,然后主进程该干啥就干啥。
也就是说,在进行IO操作写盘的过程中(on write),对于没有改变的数据,主进程和子进程资源共享;只有在出现了需要变更的数据时(写脏的数据),才进行copy操作。
在最理想的情况下,也就是生成RDB文件的过程中,一直没有写操作的话,就根本不会发生内存的额外占用。
当然,仍然需要合理配置Linux的内存分配策略。避免在写操作过于集中时,发生因为物理内存不足导致fork失败的情况。
网友评论