本文根据《redis设计与实现》,浅谈redis的单机实现原理。
数据库
键空间
- 数据结构字典
- 键都是
string
对象
过期删除策略
redis采用的是惰性删除和定期删除
惰性删除
只有对key进行读写访问时,才会进行过期判断,过期进行删除。很明显这样缺点是会内存中存在大量的过期key,无法释放空间。
定期删除
通过限制一定的频率和操作的时长,来控制删除操作,保证删除操作不会长时间占用cpu
定期删除实现
- 周期性函数中
- 每次读取一定数量的key
- 记录进度,便于后面进行处理
aof,rdb和赋值对过期key的处理
- rdb对过期key处理
- 生成rdb文件,过期key会跳过
- 主服务模式运行,rdb载入会跳过过期key
- 从服务模式运行,rdb载入会全部载入,主从同步时会进行清理
- aof对过期key处理
- key被惰性删除和定期删除时,会追歼del语句到aof,具体操作,惰性删除:删除-追加del-返回客户端空
- 复制模式下
- 主服务删除key,会发送del给从服务,从服务收到del才删除
- 从服务被客户端访问到过期key,不会删除key,直接返回空
RDB持久化
redis是内存数据库,所有数据都在内存中,重启会丢失,所以redis提供了持久化的功能。保证数据能落到磁盘进行保存,下次启动能恢复。
- rdb持久化,实现原理就是,对数据库进行快照,保存为一定格式的二进制文件。
save&bgsave
- save命令会阻塞进程,直到rdb文件写入完成
- bgsave创建子进程保存rdb
- aof开启,不会使用rdb载入数据
- 只有aof关闭时,才会使用rdb来还原
- save期间,所有客户端命令被阻塞
- bgsave期间
- bgsave命令被拒绝
- save命令被拒绝
- bgsave和bgrewriteaof互斥
- save 900 1 900s内对数据库进行1次修改
- 通过dirty值和lastsave时间戳判断是否满足save条件
AOF持久化
- 记录redis的操作记录
- 文本文件
aof实现
- 命令追加
aof_buf
缓冲区 - redis的时间事件,会考虑是否把缓冲区内容写入aof文件
appendfsync
配置使用何种方式持久化- always 写入aof文件并且同步到磁盘完成(保证完全落到磁盘)
- everysec 如果上次同步aof距离现在超过1s,那么再次同步,并且由一个线程专门执行
- no 只负责写入aof文件,同步到磁盘依赖操作系统完成
文件的写入和同步
write函数
graph LR subgraph write 用户buf 内核buf 磁盘 end 用户buf --> 内核buf --> 磁盘
write函数是在写入到内核缓冲区就返回了,实际要落到磁盘,是操作系统决定的,当然我们也可以调用fsync
和 fdatasync
来强制同步数据到磁盘
aof安全性
appendfsync
- aways 每次事件循环都追加,并且保证数据完全同步到磁盘,安全性很高,最多丢失一个事件循环的命令,效率不高
- everysec 每一秒进行同步,最大丢失1s的命令
- no 最快,但是,由于依赖操作系统的同步机制,何时真正落到磁盘,取决于缓冲区是否满,最大丢失从上一次同步到这次的命令,丢失数据量相对更多
伪客户端
- 不带网络连接的客户端
- 还原aof使用
aof重写
- 重写原理是
fork
子进程,把内存中的数据库,转换成redis的命令,写入到新的aof文件,再进行原子替换aof文件
aof重写几个问题
线程进程方式对比?
- 用进程,可以直接给内存拍快照,而用线程,就无法避免需要使用锁来保证安全性,进程的方式其实是用内存来换取了性能,当然,操作系统的
copy-on-write
机制使得内存的浪费也不会特别严重。重写期间,如果有新请求怎么办?
- 首先,子进程重写不会影响父进程,父进程会继续提供服务,此时新请求过来,会把新命令追加到
aof缓冲区
和aof重写缓冲区
,现有的aof持久化依旧会进行,子进程写入的文件是新的aof文件,不会冲突。 - 当子进程写入完成后,会发送一个信号给父进程
- 父进程把aof重写缓冲区的内容追加到新的aof文件,替换文件名字
性能影响在哪?
- 父进程收到信号,处理的阶段,是阻塞的,已经把性能损耗降低到了极致
sequenceDiagram participant 父进程 participant 子进程 Note over 父进程: set k1 v1 Note over 父进程: set k2 v2 Note over 父进程: set k3 v3 父进程 ->> 子进程: 开始aof重写 Note over 子进程: 把内存快照转换成
redis命令,
写入新的aof文件 Note over 父进程: set k4 v4
追加到aof重写缓冲区 Note over 父进程: set k5 v5
追加到aof重写缓冲区 子进程 ->> 父进程: 发送信号 Note over 父进程: 将aof重写缓冲区
追加到新的aof文件 Note over 父进程: 替换aof文件
事件
redis主要有两种时事件。文件事件,时间事件。
文件事件
通过封装epoll
select
evport
kqueue
,实现反应堆模型。io复用和单线程模式,让redis用最简单的方式达到了最高的性能。
实现
- redis是通过一段宏在编译时选不同的实现方式,优先级
evport>epoll>kqueue>select
- AE_READABLE 可读事件,读客户端数据,close,accept新连接
- AE_WRITABLE 给客户端回包
- 优先级 AE_READABLE>AE_WRITABLE
事件处理器
- 连接应答处理器
主要负责处理新连接 - 命令请求处理器
负责读取客户端命令 - 命令回复处理器
回包
sequenceDiagram participant 客户端 participant 连接处理器 participant 命令请求处理器 participant 命令回复处理器 Note over 连接处理器: 服务启动时会
为连接套接字绑定
连接处理器 客户端->>连接处理器: 建立连接,触发连接处理器 连接处理器->>命令请求处理器: 连接处理器创建命令请求处理器 命令请求处理器->>命令回复处理器: 处理命令请求处理器,创建命令回复处理器 命令回复处理器->>客户端: 处理命令回复处理器,给客户端回包
时间事件
- redis包含定时时间事件和周期时间事件,通过时间处理器返回的AE_NOMORE来决定
- redis的时间事件和文件事件是一起调度的
实现
- 每次执行事件循环
aeProcessEvents
,都先获取最近的时间事件到达的时间,用这个时间来决定,每次epoll或者其他(select)阻塞的时间,如果期间没有文件事件,那么epoll阻塞完,就刚好需要执行时间事件 - 如果最后一次文件时间处理事件过长,会导致时间事件被延后执行,因为此处redis是单进程单线程的
- 时间事件中类似持久化的操作会放到子线程或者子进程,防止时间事件长时间阻塞
- 文件时间也尽量不长时间阻塞服务器,当write时,如果超过某个阈值,会直接break,下次再写