redis-服务端部分实现原理

本文根据《redis设计与实现》,浅谈redis的单机实现原理。

数据库

键空间

  • 数据结构字典
  • 键都是string对象

过期删除策略

redis采用的是惰性删除和定期删除

惰性删除

只有对key进行读写访问时,才会进行过期判断,过期进行删除。很明显这样缺点是会内存中存在大量的过期key,无法释放空间。

定期删除

通过限制一定的频率和操作的时长,来控制删除操作,保证删除操作不会长时间占用cpu

定期删除实现
  1. 周期性函数中
  2. 每次读取一定数量的key
  3. 记录进度,便于后面进行处理

aof,rdb和赋值对过期key的处理

  • rdb对过期key处理
  1. 生成rdb文件,过期key会跳过
  2. 主服务模式运行,rdb载入会跳过过期key
  3. 从服务模式运行,rdb载入会全部载入,主从同步时会进行清理
  • aof对过期key处理
  1. key被惰性删除和定期删除,会追歼del语句到aof,具体操作,惰性删除:删除-追加del-返回客户端空
  • 复制模式下
  1. 主服务删除key,会发送del给从服务,从服务收到del才删除
  2. 从服务被客户端访问到过期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函数是在写入到内核缓冲区就返回了,实际要落到磁盘,是操作系统决定的,当然我们也可以调用fsyncfdatasync来强制同步数据到磁盘

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,下次再写
codelover wechat
原创公众号
-----若有不妥,敬请斧正-----