Redis 面试笔记
从内存访问、命令执行模型与 I/O 多路复用梳理 Redis 高性能的原因。
为什么 Redis 很快
Redis 快不是只靠某一个点,主要是下面这些原因:
- 主要数据放在内存中:常规命令不需要等待磁盘随机 I/O,内存访问本身的延迟也很低。
- 数据结构和执行路径比较高效:Redis 针对字符串、哈希、列表、集合等类型使用了不同的数据结构,很多常用操作的时间复杂度是 或 。
- 命令主要由单线程顺序执行:避免了命令执行过程中大量的锁竞争,也减少了线程调度和上下文切换的开销。
- 使用非阻塞 I/O 和 I/O 多路复用:一个事件循环可以同时管理大量客户端连接,不需要为每个连接都创建一个线程。
“单线程”不代表只有一个线程
平时说 Redis 是单线程,主要是指大部分命令由主线程顺序执行,并不是说整个 Redis 进程只能有一个线程。
Redis 还会使用后台线程处理部分耗时任务;从 Redis 6 开始,也可以使用 I/O 线程分担网络读写,但命令执行仍然主要在主线程中完成。
这种方式的优点是模型简单,单条命令在执行过程中不会被其他命令插入。缺点也很明显:如果某一条命令执行时间太长,其他客户端的命令也要跟着等待。
所以 Redis 的性能问题不一定出在网络上,也可能来自大 Key、慢命令、持久化或者系统本身的调度。
下面的 sub blog 会继续整理网络 I/O 模型、select / poll / epoll,以及 Reactor 和 Proactor 的区别。
RDB 和 AOF
RDB是定期进行snaphshot,进行快照之后保存。Redis 通常不会让主线程自己慢慢把所有数据写磁盘,因为这样会长时间阻塞请求,而是通过 fork 创建子进程,由子进程负责把内存中的数据写成 RDB 文件,父进程继续处理客户端请求。
fork 之后父子进程是不是立刻复制一份完整内存?不是。Linux 会用 Copy-On-Write,也就是写时复制。刚 fork 时,父子进程共享同一批物理内存页;如果父进程之后修改了某个页面,操作系统才会复制那个页面出来。所以 RDB 做快照时正常情况下不会直接把内存翻倍,但如果 Redis 数据量很大,同时写请求特别多,就会触发大量 COW,内存开销和 CPU 开销都会增加。这也是为什么大实例执行 RDB 时可能出现延迟抖动。
RDB 最大的优点是文件紧凑、恢复速度快,因为恢复时基本就是把一个完整快照直接加载回内存,很适合做备份、灾备,也方便把文件复制到其他机器。但是他它的缺点就是并不能真正实现数据不丢失,不能实现实时的持久化,如果在下一次快照之前宕机了,数据就真没了
AOF是记录Redis执行过的指令,这个和我学到的minilsm项目里面的思路是一样的,每次把操作记下来,然后直接append-onnly就行。Redis 重启时,不是直接加载一个内存快照,而是把这些命令重新执行一遍,从而把数据库恢复出来.
AOF 的关键配置是 appendfsync,它决定数据什么时候真正刷到磁盘。常见有三种策略。always 是每次写命令都立即 fsync,数据最安全,理论上宕机最多丢一条甚至基本不丢,但性能最差,因为磁盘同步刷写非常昂贵。everysec 是最常见的配置,大约每秒刷一次磁盘,性能和可靠性比较均衡,机器突然掉电时一般最多丢 1 秒左右的数据。no 则是不主动调用 fsync,交给操作系统自己决定什么时候刷盘,性能最好,但能丢多少数据就不太可控。
这里还要区分一个细节:写入 AOF 文件和真正刷到磁盘不是一回事。Redis 执行写命令后,通常先把内容写到用户态/内核页缓存里,write 成功并不代表数据已经进入物理磁盘。真正控制数据落盘可靠性的,是 fsync。所以面试官如果问“为什么 everysec 最多可能丢 1 秒数据”,核心就是因为 Redis 大约每秒才调用一次 fsync。
Redis 有 AOF Rewrite,也就是 AOF 重写机制。它不是简单地把旧日志压缩一下,而是根据当前内存中的最终数据状态,重新生成一份更精简的 AOF 文件
当然也有混合机制,很自然的想到就是我一次快照之后到下一次快照之前使用aof不就行了。