redis RDB 和AOF


redis 持久化

Redis 是内存数据库,关机后数据会消失,重启后如果需要快速恢复数据,需要提供持久化机制 当然,redis 持久化是为了快速恢复数据不是为了存储数据,redis有两种持久化方式RDB 和 AOF

当然 Redis持久化不保证数据的完整性,当redis 用作DB,DB数据要完整,所以一定要有一个完整的数据源(文件,mysql)
在系统启动时,从这个完整的数据源中将数据load到redis中
数据量较小,不易该表,比如:字典库,可以通过info 命令可以查看关于持久化的信息

RDB

rdb(redis database) 是redis默认的存储方式,RDB方式是通过快照(snapshotting)完成的,只是某一时刻的数据,不关注过程。

触发快照的方式

  1. 符合自定义配置的快照规则
  2. 执行save或者bgsave命令
  3. 执行flushall命令
  4. 执行主从复制操作(第一次)

配置参数定期执行

在redis.conf 中配置,save <seconds> <changes> 多少秒内,数据变了多少
save 900 1 #表明15分钟内至少1个键被更改则进行快照
save 300 10 #表明5分钟内至少10个键被更改进行快照
save 60 10000 # 

这样做是漏斗设计,提供性能

命令显式触发

在客户端输入bgsave命令 background saving started

执行原理

  1. redis 父进程首先判断:当前是否在执行save,或者bgsave/bgrewriteaof(aof 文件重写命令)的子进程,如果在执行则bgsave命令直接返回。
  2. 父进程执行fork(调用os 函数复制主进程) 操作创建子进程,这个过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令
  3. 父进程fork后,bgsave命令返回background saving started 信息并不再阻塞父进程,并可以相应其他命令。
  4. 子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原文件进行原子替换,RDB始终完整。
  5. 子进程发送信号给父进程表示完成,父进程更新统计信息
  6. 子进程fork 子进程后,继续工作

RDB 文件结构

  1. 头部5个字节规定为REDIS字符串
  2. 4字节RDB 版本号,如果当前为9 填充后为0009
  3. 辅助字段,以key-value 的形式 redis-ver 5.0.5 aof-preamble 是否开启aof redis-bits 64/32 repl-stream-db 主从复制,ctime 当前时间戳,repl-id 主从赋值,used-mem 使用内存,repl-offset 主从复制
  4. 存储数据库号码 DB_NUM
  5. 字典大小 DB_DICT_SIZE
  6. 过期key EXPIRE_DICT_SIZE
  7. 主要数据,key-value 形式存储 key_value_pairs
  8. 结束标志 EOF
  9. 校验和 就是看文件是否损坏,或者被修改。

RDB 的优缺点

优点
     RDB是二进制压缩文件,占用空间小,便于传输
     主进程fork子进程,可以最大化redis性能,主进程不能太大,复制过程中主进程阻塞
缺点
    不保证数据完整性,会丢失最后一次快照以后更改的所有数据

AOF

AOF(append only file) 是redis 的另一种持久化方式,redis 模式情况是不开启的,开启AOF持久化后,redis将所有对数据库进行过写入命令(及其参数)(RESP) 记录到AOF文件,以此达到记录数据库状态的目的,这样当redis 重启后只要按顺序回放这些命令就会恢复到原始状态了,AOF会记录过程,RDB只管结果

AOF持久化实现

配置redis.conf

# 可以通过修改redis.conf 配置文件中的appendonly 参数开启
appendonly yes
# AOF 文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的
dir ./
# 默认的文件名是appendonly.aof,通过appendfilename 参数修改
appendfilename  appendonly.aof

AOF 原理

AOF 文件中存储的是REDIS的命令,同步命令到AOF 文件的整个过程可以分成三部分: 命令传播:redis 将执行完的命令,命令的参数,命令的参数个数等信息发送到AOF程序中 缓存追加:AOF 程序根据接受到的命令数据,将命令转化为网络通讯协议的格式,然后将协议内容追加到服务器的AOF缓存中。 文件写入和保存: AOF 缓存中的内容被写入到AOF文件末尾,如果设定的AOF保存条件被满足中,fsync 函数或者fdatasync 函数会被调用,将写入的内容真正地保存到磁盘中。

命令传播

当一个redis 客户端要执行命令时,它通过网络连接,将协议文本发送给redis 服务器,服务器在街道客户端的请求中,它会 根据协议文本的内容,选择适当的命令函数,并将各个参数从字符串文本转换成redis 字符串对象(stringobject),每当命令函数成功执行之后,命令参数都会被传播到AOF程序中。

缓存追加

当命令被传播到AOF程序之后,程序会根据命令以及命令的参数,将命令从字符串对象转换回原来的协议文本,协议文本生成之后,它会被追加到 redis.h/redisServer 结构的 aof_buf 末尾。

另外redisServer 结构维持着redis 服务器的状态,aof_buf 域则保存着所有等待写入到AOF文本的协议文本(RESP)

文件写入和保存

每当服务器常规任务函数被执行,或者事件处理器被执行时,aof.c/flushAppendOnlyFile 函数都会被调用,这个函数执行一下两个工作: write :根据条件 将aof_buf中的缓存存入到AOF 文件。 save: 根据条件,调用fsync 或 fdatasync 函数,将AOF文件保存到磁盘中

AOF 保存模式

Redis 目前支持三种AOF 保存模式,分别是: AOF_FSYNC_NO:不保存 AOF_FSYNC_EVERYSEC:每一秒中保存一次(默认) AOF_FSYNC_ALWAYS:没执行一个命令保存一次 (不推荐)

不保存

在这种模式下,每次调用flushAppendOnlyFile 函数, write都会被执行,但save会被略过,这种模式下,save只会在以下任意一种情况中执行: REDIS 被关闭 AOF 功能被关闭 系统的写缓存被刷新,此时缓存可能别写满或者定期保存操作被执行, 这三种情况下的save操作都会引起redis 主进程阻塞。

每一秒保存一次

save原则上每隔一秒钟就会执行一次,因为save 操作是由后台子线程(fork) 调用的,所以不会引起服务器主进程阻塞

每执行一个命令保存一次

每执行完一个命令之后,Write和save 都会被执行 ,另外因为save 是由redis 主进程执行的额,在save 执行期间,主进程会被阻塞,不能接受命令请求 AOF 保存模式对性能和安全性的影响,对于三种AOF保存模式,他们对服务器主进程的阻塞情况如下:

  1. AOF_FSYNC_NO write阻塞 save 阻塞 停机时会丢失操作系统最后一次对AOF文件触发save 操作之后的数据。
  2. AOF_FSYNC_EVERYSEC write 阻塞,save不阻塞,一般情况下不超过2sec 的数据
  3. AOF_FSYNC_ALWAYS write 阻塞,save 阻塞,最多只丢失一个命令的数据

AOF重写,触发方式,混合持久化

AOF 记录数据的变化过程,越来越大,需要重写瘦身,Redis 可以在AOF体积变得过大时,自动地在后台(Fork子进程)对AOF进行重写,重写后的新AOF文件包含了 恢复当前数据集所需的最小命令集合,AOF重写并不需要对原有的AOF文件进行任何写入和读取,针对的是数据中键的当前值 。

使用子进程中执行的好处 子进程进行AOF 重写期间,主进程可以继续处理命令请求。 子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免所的情况下,保证数据的安全性。

使用子进程还有一个问题需要解决: 因为子进程在进行AOF重写期间,主进程还需要继续处理命令,而新的命令可能对现有的数据进行修改,这可能会让档期啊数据库的数据和重写后的AOF文件中的数据不一致。

为了结局这个问题,REDIS 增加了一个 AOF重写缓存,这个缓存在fork出子进程之后开始启用,redis 主进程在接受到新的写命令之后, 除了会将这个写命令的协议内容追加到现有的AOF文件之外,还会追加这个缓存中。

重写过程分析(整个重写操作是绝对安全的):

redis 在创建新AOF 文件过程中,会继续讲命令追加到现有的AOF文件里面,即使重写过程中发生停机,现有的AOF文件也不会丢失, 而一旦新AOF文件创建完毕,redis 就会从旧AOF 文件切换到新AOF 文件,并开始对新AOF 文件进行追加操作。

当子进程在执行AOF 重写时,主进程需要执行以下三个工作:

  1. 处理命令请求
  2. 将写命令追加到现有的AOF文件中
  3. 将写命令主家到AOF重写缓存中

这样做可以保证:AOF 功能会继续执行,即使在AOF重写期间发生停机,也不会有任何数据丢失。所有对数据库进行修改的命令都会被记录到AOF重写缓存中, 当子进程完成AOF重写之后,它会向父进程发送一个完成信号,父进程在接到完成信号之后,会调用一个信号处理函数,完成以下工作:

  1. 将AOF重写缓存中的内容全部写入到新AOF 文件中
  2. 对新的AOF文件进行改名,覆盖原有的AOF文件。

REDIS数据库里的+AOF 重写过程中的命令-》新的AOF文件-》覆盖老的 当步骤1 执行完毕后,现有的AOF 文件,新AOF 文件和数据库三者的状态就完全一致了 当步骤2 执行完毕后,程序就完成了新旧两个文件的交替。

这个信号处理函数执行完毕之后,主进程可以继续接受命令请求,这个AOF后台重写过程中,只有最后的写入缓存和改名操作会造成主进程阻塞,在其他时候,AOF 后台 重写都不会对主进程造成阻塞,这将AOF重写对性能造成的影响降低到最低,这也是AOF 后台重写,也是BGREWRITEAOF命令(AOF重写)的工作原理。

触发方式

  1. 配置触发 在redis.conf 中配置 表示当前aof 文件大小超过上一次AOF文件大小的百分之多少的时候会进行重写,如果之前没有重写过,以启动时aof文件大小为准。 auto-aof-rewrite-percentage 100 限制允许重写最小aof文件大小,也就是文件大小小于64MB的时候不要进行优化 auto-aof-rewrite-min-size 64mb

  2. 执行bgrewriteaof 命令

混合持久化

4.0 之后支持rdb和 aof的混合持久化,如果持久化打开, aof rewrite 的时候直接把rdb内容写到aof文件开头。 RDB的头+AOF的身体---》appendonly.aof 开启混合持久化 auto-use-rdb-preamble yes 在加载时,首先识别 AOF文件是否以redis 字符串开头,如果是就按 RDB格式加载,加载完RDB 后继续按AOF格式加载剩余部分。

AOF 文件的载入和数据还原

因为AOF 文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态 redis 读取AOF文件并还原数据库状态的详细步骤如下:

  1. 创建一个不带网络连接的伪客户端(fake client):因为redis 的命令只能在客户端上下文中执行,而载入AOF文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样
  2. 从AOF文件中分析并读取一条写命令
  3. 使用伪客户端执行被读出的写命令
  4. 一直执行步骤2 和 步骤3 知道AOF 文件中的所有写命令都被处理完毕为止

RDB 和AOF对比

  1. RDB存的是某一时刻的数据快照,采用二进制,AOF存操作命令,采用文本存储(混合)
  2. RDB性能高,AOF性能低
  3. RDB在配置触发状态会丢失最后一次快照以后更改的所有数据,AOF设置为每秒保存一次,则最多丢2秒的数据
  4. redis 以主服务器模式运行,rdb 不会保存过期键值对数据,redis 以服务器模式运行,RDB会保存过期键值对,当主服务器向从服务器同步时,再清空过期键值对 AOF 写入文件时,对过期的key会追加一条del 命令,当执行AOF重写时,会忽略过期key 和del 命令

订阅和发布

Redis 提供了发布订阅功能,可以用于消息的传输,Redis 的发布订阅机制包括三个部分:publisher,subscriber 和channel 发布者和订阅者都是REDIS 客户端,channel为 redis服务器段 发布者将消息发送到某个频道,订阅了这个频道的订阅者就能收到这条消息。

频道/模式的订阅和退订

subscribe:订阅 subscribe channel1 channel2...
    redis 客户端1 订阅频道2 和频道2
    ```
    subscribe ch1 ch2
    ```
publish: 发布信息publish channel  messsage
    redis 客户端2 将消息发布在频道1 和 频道2 上
    ```
    publish ch1 hello 
    publish ch2 world
    ```
unsubscribe 退订channel
    redis 客户端1 退订频道1 
    ```
    unsubscribe ch1

    ```
psubscribe :模式匹配 psubscribe+模式
    redis 客户端1订阅所有以ch开头的频道
    ```
    psubscribe ch8
    ```

发布订阅的机制

订阅某个频道或模式 客户端(client): 属性为pubsub_channels,该属性表明了该客户端订阅了所有频道 属性为pubsub_patterns 该属性表明了该客户端订阅的所有模式 服务器端(redisServer): 属性为pubsub_channels,该服务器中的所有频道以及订阅了这个频道的客户端 属性为pubsub_patterns ,该服务器中的所有模式和订阅了这些模式的客户端

typedef struct redisClient {
    ...
    dict *pubsub_channels; //该client订阅的channels,以channel为key用dict的方式组织
    list *pubsub_patterns;  // 该client 订阅的pattern,以list的方式组织
    ...
} redisClient;

struct redisServer {
    ...
    dict *pubsub_channels; //redis server 进程中维护的channel dict ,它以channel为可以,订阅channel的client  list 为 value 
    list * pubsub_patterns; //redis server 进程中维护的pattern list
    int notify_keyspace_event;

};
 当客户端向某个频道发送消息时,REDIS首先在redisServer 中的pubsub_channels 中找出键位该频道的结点,遍历改结点的值,即遍历订阅了该频道的所有客户端,将消息发送给这些客户端
 然后遍历结构体redisServer中的pubsub_patterns找出包含该频道的模式的结点,将消息发送给订阅了该模式的客户端。

使用场景:哨兵模式,redisson框架使用

在REDIS哨兵模式中,哨兵通过发布于订阅的方式与redis主服务器和redis从服务器进行通信,redisson是一个分布式锁框架,在redisson 分布式锁释放的时候,
是使用发布于订阅的方式通知的。