1. 数据库

redis数据库的很多操作都是通过对键空间进行操作来实现的,比如添加,删除,更新,取值操作,比如用于清空整个数据库的FLUSHDB命令,用于返回数据库中随机键的RANDOMKEY,类似的命令还有EXISTS,RENAME,KEYS等.

当使用redis命令对数据库进行读写时,服务器不仅对键空间执行指定的读写操作,还会执行一些额外的维护操作,包括:

  • 读取一个键后(包括读操作和写操作)更新键的命中(hit)或不命中(miss)的次数,这两个值可以在INFO stats命令的keyspace_hits属性和keyspace_misses属性中查看.
  • 更新键的LRU(最后一次使用)时间,这个值可以用来计算键的闲置时间(为将来键删除算法做准备)
  • 读取时如果发现键已经过期,就删除该键之后再执行剩下的操作
  • 如果有客户端使用WATCH命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty)
  • 服务器每次修改一个键之后,都会对脏(dirty)键计数器的值增1,这个计数器会触发服务器的持久化和复制操作

设置过期时间

redis有四个命令用来设置键的过期时间:

EXPIRE <key> <ttl> 将键的生存时间设置为ttl

PEXPIRE <key> <ttl>将键的生存时间设置为ttl毫秒

EXPIRE <key> <timestamp> 将键的生存时间设置为timestamp所指定的秒数时间戳

PEXPIRE <key> <timestamp>将键的生存时间设置为timestamp所指定的毫秒数时间戳

以上四个命令,最终执行都是转换为第四个命令的执行方式执行的.

保存过期时间

redisDb结构的expires字典保存了数据库中所有键的过期时间,这个字典叫做过期字典:过期字典是一个long long类型的整数,这个整数保存了键所指向的数据库键的过期时间(毫秒精度的UNIX时间戳)

移除过期时间

PERSIST命令可以移除一个键的过期时间,用法 PERSIST key

计算并返回剩余生存时间

TTL命令以秒为单位返回键的剩余生存时间,而PTTL命令则以毫秒为单位返回键的剩余生存时间,这两个命令都是通过计算键的过期时间和当前时间之间的差来实现的

过期键删除策略

  • 定时删除: 在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行键删除操作

    优点: 对内存友好,通过使用定时器保证过期键会尽快删除,并释放掉占用的内存

    缺点: 对CPU时间不友好,比如某一时刻过期键比较多,那就会占用比较多的CPU时间来删除

  • 惰性删除: 平时不对键进行操作,当从键空间获取键的时候,检查键是否过期,过期的话就删除该键,没过期就返回

    优点: 对CPU时间友好

    缺点: 对内存不友好,比如一个键已经过期,但是没有及时删除,就会一直占用内存

  • 定期删除: 每个一段时间对数据库进行检查,删除里面的过期键,至于删除多少,检查多少个数据库,由具体算法确定

    定期删除是以上两种方案优缺点的折中,但是需要合理设定删除操作的执行时长和频率

redis实际采取的是惰性删除和定期删除两种策略

惰性删除策略由db.c/expireIfNeeded函数实现,所有读写数据库的命令(如SET LRANGE SADD HGET KEYS等)在执行之前都会调用该函数对键进行检查:如果已经过期该函数会将该键删除,否则不做操作.

定期删除策略由redis.c/activeExpireCycle函数实现,每当redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用.

AOF/RDB和复制功能对过期键的处理

生成RDB文件: 当执行SAVE或者BGSAVE命令创建新的RDB文件时,已过期的键不会被保存到新建的RDB文件中

载入RDB文件: 启动redis服务器时,如果服务器开启了RDB功能,那么服务器将对RDB文件进行载入.如果服务器以主服务器模式运行,载入RDB文件时,会对文件中保存的键进行检查,过期的键不会被载入;如果服务器以从服务器的模式运行,不论键是否过期,都会载入到数据库,但是由于主从服务器进行数据同步的时候从服务器的数据库就会清空,所以不会有什么影响.

AOF文件写入: 当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加一条DEL命令,来显式的记录该键已被删除

AOF重写: 在执行AOF重写的过程中会对数据库中的键进行检查,如果已经过期了的不会被保存到重写后的AOF文件

复制: 当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:

  • 主服务器删除一个键之后会显式的向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键
  • 从服务器只有在接到主服务器发来的DEL命令之后,才会删除过期键
  • 从服务器接到客户端发送的读命令时,即使碰到过期键也不会处理,还是会按照正常情况返回值

数据库通知

数据库通知功能可以让客户端通过订阅给定的频道或模式,来获知数据库中键的变化,以及数据库中命令的执行情况,这类通知可以分为两类:

键空间通知(key-space notification): 关注某个键执行了什么命令,比如针对某个key执行了expire,set,del等

键事件通知(key-event notification): 关注某个命令被什么键执行了,比如del命令执行在哪些键上了

服务器配置的notify-keyspace-events选项决定了服务器所发送通知的类型:

AKE : 发送所有类型的键空间和键事件通知

AK : 发送所有类型的键空间通知

AE : 发送所以类型的键事件通知

K$ : 只发送和字符串键有关的键空间通知

El : 只发送和list键有关的键事件通知

2. RDB持久化

RDB持久化功能所生成的RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态.

RDB文件的创建与载入

有两个Redis命令可以用于生成RDB文件,一个是SAVE,另一个是BGSAVE,SAVE 命令会阻塞服务器进程,BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件.

Redis并没有专门用于载入RDB文件的命令,只要服务器启动之后检测到RDB文件就会自动载入,由于AOF文件的更新频率通常比RDB文件的更新频率高,所以:

  • 如果服务器开启了AOF持久化功能,则优先使用AOF文件来还原数据库状态
  • AOF持久化功能关闭时,会采用RDB文件来还原数据

服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止,BGSAVE命令可以在配置文件中配置每个一段时间自动执行,详见save 900 1...这一组配置

具体RDB文件保存的格式和内容建议直接看原书第10章内容

3.AOF持久化

与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库的状态的,AOF持久化功能的实现分为命令追加(append),文件写入,文件同步(sync)三个步骤

命令追加

当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾:

1
2
3
4
5
6
7
struct redisServer{
// ...

//AOF缓冲区
sds aof_buf;
//...
}

例如:当客户端向服务器发送如下命令:

redis > SET KEY VALUE

OK

服务器在执行完命令之后,会将以下协议内容追加到aof_buf缓冲区的末尾:

*3\r\n$3\r\nSET\r\nKEY\r\n$5\r\nVALUE\r\n

文件的写入与同步

redis服务器进程就是一个事件循环(loop).这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数.

由于服务器在处理文件事件时可能会执行写命令.在此期间一些内容被追加到aof_buf缓冲区中,所以在服务器每次结束一个事件循环之前,都会调用一个flushAppendOnlyFile函数,考虑是否将aof_buf缓冲区中内容写入和保存进AOF文件中,该函数的行为有服务器配置文件中的appendfsync选项中的值来决定,各个值的含义如下:

appendfsync选项的值 flushAppendOnlyFile函数的行为
always 将缓冲区中的所有内容写入并同步到AOF文件
everysec (默认) 将缓冲区中的所有内容写入到AOF文件,如果上次同步距离现在时间超过1秒,
那么再次对AOF文件进行同步,该同步行为有一个专门负责的线程
no 将缓冲区中的所有内容写入到AOF文件,但并不进行同步,何时同步由操作系统决定

AOF文件载入与数据还原

因为AOF文件中记录了重建数据库所需要的所有写命令,所以服务器只要读入并重新执行一遍AOF文件中保存的写命令,就可以还原服务器关闭之前的数据库状态.

AOF重写

随着AOF文件中保存的指令越来越多,文件体积越来越大,这个时候需要对文件进行重写来缩小文件所占存储空间.

AOF文件重写并不需要对现有AOF文件进行读取或者分析,是直接通过读取服务器当前的数据库状态实现的,比如当前数据库存在一个键值对,老的AOF文件中可能存储了对这个键值对的各种历史操作命令,重写的时候只要读取该键值对最新的状态将之前的多个指令合并成最后的一条指令保存即可,这就是AOF重写的原理

有个特殊的点是对于除了字符串之外的另外四种数据类型,如果一个键对应的元素个数很多,超过某个配置的值,会使用多条记录保存命令,而不只是一条.

AOF文件的重写是通过一个子进程来执行的,使用子进程而不是线程,主要是为了在避免使用锁的情况下还能保证数据安全性.由于在子进程执行重写任务的过程中,可能主进程依然会执行新的命令.Redis设置了一个AOF重写缓冲区来存储在这段时间中主进程执行的新的指令.该缓冲区当子进程建立的时候开始使用

因此在子进程进行重写的过程中,如果主线程接收到新的命令,会将其同时保存在aof_buf缓冲区及AOF重写缓冲区中;而子进程在完成AOF重写工作后会给主进程发送一个信号,之后主进程收到信号并调用一个函数,执行以下工作:

  • 将AOF重写缓冲区中的所有内容写入到新的AOF文件中,这时新的AOF文件所保存的数据库状态将和当前服务器的状态一致.
  • 对新的AOF文件进行改名,原子地覆盖现有的AOF文件,完成新旧文件的替换

这两个动作完成之后,主线程就可以继续接受新的指令了.

在整个AOF后台重写的过程中,只有该过程会对主进程造成阻塞,其他时间都是在后台进行,不影响主进程执行任务.