单线程 Redis 为什么快
redis 单线程是指Redis的网络IO和键值对读写是由一个线程来完成的,这也是redis对外提供键值存储服务的主要流程。
但是redis的其它功能:持久化、异步删除、集群数据同步等都是由额外的线程执行的
多线程的开销
系统中通常会存在被多线程同时访问的共享资源,比如一个共享的数据结构。当多个线程要修改这个共享资源时,为了保证共享资源的正确性,就需要有额外的机制保证,而这个额外的机制会带来额外的开销。
单线程redis快的原因
- 大部分的操作在内存上完成
- 高效的数据结构
- 多路复用机制
多路复用的 I/O 模型
在redis只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核上会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给redis线程处理,这就实现了一个redis线程处理多个IO流的效果
为了在请求到达的时能通知到redis线程,select/epoll 提供了 基于事件的回调机制,即针对不同的事件发生,调用相应的处理函数。
select/epoll 一旦检测到FD(文件描述符file descriptor)上有请求到达时,就会触发相应的事件。
这些事件会被放进一个事件队列,redis单线程对该事件队列不断进行处理。这样一来,redis无需一直轮询是否有请求实际发生,这样就可以避免造成cpu资源的浪费。redis在对事件队列中的事件进行处理时,会调用相应的处理函数,这就实现了基于事件的回调。因为redis一直在对事件队列进行处理,所以能及时响应客户端请求,提升了redis的响应性能。
RDB
RDB可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中
RDB 持久化 生成的 RDB 文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态
RDB 文件的创建与载入
两个命令可以生成RDB文件:
- save:save命令会阻塞redis主线程,直到rdb创建完成为止,在主线程阻塞期间,服务器不能处理任何命令请求。
- bgsave:bgsave命令会派生出一个子线程,然后由子线程负责创建 RDB 文件,主线程可以继续处理命令
RDB文件的载入工作是在服务器启动的时候自动执行的,所以 redis 并没有专门用于载入RDB文件的命令,只要redis服务在启动时检测到 RDB 文件存在,它就会自动载入RDB文件。
AOF文件的更新频率比RDB文件更新频率更高,所以:
- 如果服务器开启了 AOF 持久化功能,那么服务器会优先使用 AOF 文件来还原数据库状态
- 只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态
在bgsave命令执行期间:
- 客户端发送的 save 命令会被服务器拒绝,(避免父进程和子进程同时执行两个rdbSave调用)
- 客户端发送的 BGREWRITEAOF 命令会被服务器拒绝。(相当于并发执行两个子线程,而且这两个子线程都是同时执行大量的磁盘写入操作)
自动间隔保存
redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令
比如配置如下:
save 900 1
save 300 10
save 60 10000
- 在900秒之内,对数据库进行了至少一次修改
- 在300秒内,对数据库至少进行了 10 次修改
- 在60秒内,对数据库至少进行了 10000 次修改
dirty 计数器和 lastsave 属性
- dity计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态(所有的数据库)进行了多少次修改
- lkastsave属性是一个时间戳,记录了上一次执行成 SAVE 或者 BGSAVE 命令的时间
当服务器执行成功一个数据库修改命令之后,程序就会对dirty计数器进行更新。
检查条件是否满足
redis服务器周期性操作函数serverCron默认每隔100ms执行一次,该函数用于对正在运行的服务进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足就执行BGSAVE命令
补充
-
在持久化过程中,“写实复制” 会重新分配内存副本,如果此时的服务器内存使用接近饱和,同时父进程又有大量的新 key 写入,很快,机器内存就会被消耗完,如果机器开启了swap机制,那么redis会有一部分数据映射到磁盘上,当redis访问这部分在磁盘上的数据时,性能会急剧下降,已经达不到高性能的标准。如果机器没有开swap,会直接出发oom,父子进程会面临被系统kill掉的风险。
-
虽然是自己才能在做持久化,但是生成的RDB快照过程会消耗大量的CPU资源,虽然redis处理请求是单线程的,但redis服务还有其它线程在后台工作,例如AOF的每秒刷盘,异步关闭文件描述符等操作。当父进程占用CPU资源过多的时候进行RDB持久化,可能会产生CPU竞争,导致的结果是父进程处理请求延时增大,子进程生成RDB快照时间变长,导致整个redis服务性能下降。
-
如果redis绑定了CPU,子进程会继承父进程的CPU亲和属性,子进程必然会与父进程争夺同一个CPU资源,进而影响redis服务的性能。所以如果redis需要开启定时RDB和AOF重写,进程一定不要绑定CPU
AOF
AOF日志是写后日志,先将数据写入内存,然后才记录日志
AOF中保存的是redis接收到的每一条命令,这些命令是以文本形式保存的
- 为了避免额外的检查开销,redis在向AOF里面记录日志的时候,并不会先去对这些命令进行语法检查。所以如果先记录日志再执行命令的话,日志中就有可能会记录错误的命令,在恢复数据的时候可能会出错
- 在命令执行之后记录日志,不会阻塞当前的写操作
潜在风险
- 如果刚执行完一个命令,还没有来得及记录日志就宕机了,那么这个命令和相应的数据就有丢失的风险。
- AOF虽然避免了对当前命令的阻塞,但可能会给下一个命令带来阻塞风险。因为AOF日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了。
三种回写策略
- always,同步回写:每个写命令执行完,立马同步将日志写回磁盘
- everysec,每秒回写,每个命令执行完,先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区的内容写入磁盘。
- no,操作系统控制的回写,每个命令执行完,先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容回写至磁盘
但是这三种方式都无法避免主线程阻塞和数据丢失问题
- 同步回写:
- 可靠性高,数据基本不会丢失
- 对性能影响比较大
- 操作系统控制的回写:
- 性能好
- 宕机时丢失的数据比较多
- 每秒回写:
- 性能适中
- 宕机丢失1s内的数据
需要注意AOF文件过大带来的性能问题
- 操作系统对文件大小有限制
- 文件过大,追加命令记录效率会变低
- 如果发生宕机,进行数据恢复的时候,AOF中的命令要一个个的执行,如果日志文件过大,整个恢复过程会非常缓慢
重写机制
重写
比如一个 key => value 是 “name” => “tom”
现在redis收到了如下命令
set name jim
set name lisi
set name wangwu
这时候AOF里面只会记录最后的命令 set name wangwu
和AOF日志由主线程回写不同,重写的过程是由后台线程 bgrewriteaof 来完成的,这也是为了避免阻塞主线程,导致数据库性能下降
一个拷贝、两处日志
一个拷贝
每次重写的时,主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
两处日志
因为主线程未阻塞,仍可以处理新来的操作。此时,如果有写操作,第一处日志就是指正在使用的AOF日志,redis 会把这个操作写到它的缓冲区。这样一来,即使宕机了,这个AOF日志仍然是齐全的。
第二处日志,就是指新的AOF重写日志。这个操作也会被写到重写日志的缓冲区。这样