Redis数据库结构/键空间/过期字典/事务/锁/持久化

1 Redis服务器结构

Redis服务器将所有数据库都保存在redis.h/redisServer结构中,这里面最重要的一个结构是 redisDb *db,这是一个数组,保存着服务器中的所有数据库。
在redis初始化的时候,程序会根据dbnum属性来决定创建多少个数据库,这个属性由配置文件里的database选项来决定,默认是16,故而redis服务器默认创建16个数据库。

1.1 目标数据库

每个redis客户端都有自己的目标数据库,redis的读写操作都是针对目标数据库的。

默认情况下,redis的目标数据库是0号数据库,用户可以使用select命令来切换目标数据库。SELECT 1命令即为使目标数据库为1号数据库。

用来存储客户端状态的redisClient对象中的db属性记录了客户端当前的目标数据库,这个指针指向了redisDb数组的某个节点。

Select就是通过改变db属性的指针而切换目标数据库的。

1.2 数据库键空间

上文说过redisDb结构表示一个数据库,其中的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间。我们对键值对的增删改查,都是对这个键空间的操作。

使用字典的好处是,随着数据量的增大,定位到某个键值对的时间复杂度并不会增加太厉害。很适合redis数据库的情况。

1.3 过期字典

要理解过期字典,先知道生存时间这一概念:

1.3.1 设置过期时间

Redis可以设置键的生存时间或过期时间:

  • expire命令或者pexpire命令,客户端可以以秒(前者)或者毫秒(后者)精度,为数据库中某个键设置生存时间。

expire命令或者pexpire命令为一个key设置生存时间,注意,这个生存时间设置在key上,除非是用新的k-v来取代这个k-v,否则,只改变其value(可能是链表,集合,哈希等等)的结构,或者只是重命名这个key,或者对key进行自增自减操作,本质上都不会改变key上的生存时间。

  • Persist命令可以移除一个键的生存时间设置。
  • TTL 命令可以查看一个带有生存时间限制的键的剩余时间,以秒为单位,底层实现是以过期时间戳减去当前时间。(PTTL以毫秒为单位)

1.3.2 过期字典空间

回到我们的redisDb结构,我们可以看到一个dict数组叫做expires

Expires字典保存了数据库中所有键的过期时间,这个键就是过期字典。

  • 过期字典的键是一个指针,指向了键空间中的某个键对象(所以才说生存时间是针对key的)
  • 过期字典的值是一个long long 类型的整数,这个整数保存了键所指向的键对象的过期时间——以毫秒为精度的一个时间戳(所以底层其实都是用PEXPIREAT命令)

1.3.3 过期键删除策略

Redis使用了惰性删除(在使用到键时才去判断是否过期以及删除)和定期删除(每隔一段时间删除库中的过期键)两种结合的删除策略

  • db.c/expireIfNeeded函数负责惰性删除,
  • redis.c/activeExpireCycle函数实现定期删除,它在规定的时间内,分多次遍历服务器中的各个数据库,从过期字典中随机检查一部分键的过期时间,并删除过期键。

2 Redis的事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//我们首先使用MULTI命令告诉Redis:
//“下面我发给你的命令属于同一个事务,你先不要执行,而是把它们暂时存起来。”Redis回答:“OK。

redis > MULTI
OK

//而后我们发送了两个SADD命令来实现关注和被关注操作.
//可以看到Redis遵守了承诺,没有执行这些命令,
//而是 返回QUEUED表示这两条命令已经进入等待执行的事务队列中了。
redis > 命令1
QUEUED
redis > 命令2
QUEUED

//当把所有要在同一个事务中执行的命令都发给Redis后,
//我们 使用EXEC命令告诉Redis将等待执行的事务队列中的所有命令(即刚才所有返回QUEUED的命令)
//按照发送顺序依次执行。
//EXEC命令的返回值就是这些命令的返回值组成的列表,返回值顺序和命令的顺序相同。

redis > EXEC

Redis的事务和其他的事务一样保证最基本的原子性,只要执行exec命令,事务就会执行,执行过程中,一个命令失败,事务中的命令就全部取消。

redis没有类似关系型数据库的回滚功能,烂摊子只能自己收拾。

3 Redis的锁

Redis的Watch命令具有乐观锁的功能,WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。

监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值)

WATCH只负责 当被监控的键值被修改后 阻止紧随其后的一个事务的执行,而不能保证其他客户端不修改这一键值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
redis > SET key 1
OK
redis > WATCH key
OK
redis > SET key 2
OK
redis > MULTI
OK
redis > SET key 3
QUEUED
redis > EXEC
(nil)
redis > GET key //得到2,说明SET key 3命令没有实现,事务被取消了
"2"

4 Redis的持久化

为了防止因为突发情况,导致Redis数据库的数据丢失,我们需要对Redis做持久化。Redis如何做持久化呢?有两种方式:

  • RDB(快照)持久化
  • AOF

4.1 RDF

RDB(快照)持久化:保存某个时间点的全量数据快照,生成RDB文件在磁盘中。RDB文件是一个压缩过的二进制文件,可以还原为Redis的数据。

4.1.1 触发和载入方式

  • 手动触发方式

    • SAVE命令:阻塞Redis的服务器进程,直到RDB文件被创建完毕,阻塞期间服务器不能处理任何命令请求。
    • BGSAVE命令:Fork出一个子进程来创建RDB文件,不阻塞服务器进程。lastsave 指令可以查看最近的备份时间。
  • 载入方式

    • Redis没有主动载入RDB文件的命令,RDB文件是在服务器启动时自动载入,只要Redis服务器检测到RDB文件的存在,即会载入。且载入过程,服务器也会是阻塞状态
  • 自动触发方式

    • 根据redis.conf配置里的save m n定时触发(用的是BGSAVE),m表示多少时间内,n表示修改次数。save可以设置多个条件,任意条件达到即会执行BGSAVE命令

      1
      2
      3
      save 900 1  //设置条件1,即服务器在900秒内,对数据库进行了至少1次修改,即会触发BGSAVE
      save 300 10 //设置条件2,即服务器在300秒内,对数据库进行了至少10次修改,即会触发BGSAVE
      save 60 1000 //设置条件3,即服务器在60秒内,对数据库进行了至少1000次修改,即会触发BGSAVE
    • redis如何保存自动触发方式的save配置呢

      • redisServer结构中维护了一个saveParam的数组,数组每个saveParam都存储着一个save条件,如下图:
      • 前文所述三个save,其saveParam的数组将会是下图的样子
    • 自动触发方式如何实现的呢

      • redisServer结构维护了一个dirty计数器和lastsave属性。
      • dirty计数器记录了上次SAVE或者BGSAVE之后,数据库执行了多少次的增删改,当服务器成功执行一个修改命令后,程序就会对该值+1,(对集合操作n个元素,dirty+n)。SAVE或者BGSAVE命令执行后,dirty计数器清零。
      • lastsave属性是一个unix时间戳,记录了服务器上次成功执行SAVE或者BGSAVE命令的时间。
      • Redis服务器有个周期性操作函数serverCron,默认每100毫秒执行一次,它其中一项工作就是检查saveParam保存的条件,并根据dirty和lastsave字段判断是否有哪一条条件已经被满足。

4.1.2 RDB文件的结构

不加赘述,详见让你彻底了解RDB存储结构

4.2 AOF

除了RDB持久化以外,Redis还提供了AOF(append only file)持久化功能,和RDB通过保存数据库的键值对来记录状态不同,AOF持久化是通过保存Reids服务器所执行的写命令来记录数据库状态的。

4.2.1 AOF持久化的实现

AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤。

  • 命令追加
    • 当AOF持久化功能处于打开状态时,服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓存区的末尾。
  • AOF文件的写入和同步
    • Redis的服务器进程就是一个事件循环。
    • 每次结束一个事件循环之前,都会调用flushAppendOnlyFile函数,考虑是否将缓冲区的内容写入和保存到AOF文件里面。
    • flushAppendOnlyFile函数根据配置项appendsync的不同选值有不同的同步策略。

4.2.2 AOF文件的载入

Redis读取AOF文件并还原数据库状态的详细步骤如下:

  • 服务器创建一个不带网络连接的伪客户端(fake client)(因为Redis的命令只能在客户端上下文中执行);
  • 从AOF文件中分析并读取出一条写命令。
  • 执行命令。
  • 一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止。

4.2.3 AOF重写

体积过大的AOF文件很可能对Redis服务器、甚至整个宿主计算机造成影响,并且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多。

为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写(rewrite)功能。

通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多。

我们称新的AOF文件为AOF重写文件,AOF重写文件不是像AOF一样记录每一条的写命令,也不是对AOF文件的简单复制和压缩。AOF重写是通过读取当前Redis数据库状态来实现的

比如一个animals键,我们有如下操作:

在AOF中,我们要保存四条写命令,而在AOF重写文件中,我们使用一条SADD animals "Dog" "Panda" "Tiger" "Lion" "Cat"来替代四条命令。

从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,即直接从结果反推命令,这就是AOF重写功能的实现原理。(比如连续6条RPUSH命令会被整合成1条)

在实际中,为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,如果元素的数量超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量(默认为64)的值,那么重写程序将使用多条命令来记录键的值,而不单单使用一条命令。例如如果SADD后面加入的元素为90条,那么会分成两条SADD,第一条SADD 64个元素,第二条SADD 36个元素。

4.2.3 AOF后台重写

Redis服务器是单线程,如果由服务器发起AOF重写,那么服务器将阻塞。为了防止这个情况,Redis使用子进程进行AOF重写,在这同时主进程仍然在接受并处理客户端的请求。

因为在子线程重写的过程中,主线程也在处理请求导致数据库状态变化,为了解决数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用。

在子线程重写的过程中,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区AOF重写缓冲区

这样一来可以保证:

  1. AOF缓冲区的内容会定期被写入和同步到AOF文件,对现有AOF文件的处理工作会如常进行。
  2. 从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面。

当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作:

  1. 将AOF重写缓冲区中的所有内容写入到AOF重写文件中,这时AOF重写文件所保存的数据库状态将和服务器当前的数据库状态一致。

  2. 对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成新旧两个AOF文件的替换。

在整个AOF后台重写过程中,只有信号处理函数执行时会对服务器进程(父进程)造成阻塞,在其他时候,AOF后台重写都不会阻塞父进程,这将AOF重写对服务器性能造成的影响降到了最低。

0%