Redis:单机数据库的实现

数据库

  • Redis服务器的数据库实现:
    • 服务器保存数据库的方法。
    • 客户端切换数据库的方法。
    • 数据库保存键值对的方法。
    • 数据库的增删改查的实现方法。
    • 服务器保存键的过期时间的方法。
    • 服务器自动删除过期键的方法。
    • Redis2.8数据库通知功能的实现。

服务器中的数据库

Redis服务器将所有数据库保存在服务器状态redis.h/redisServer中:

1
2
3
4
5
6
struct redisServer{
//一个数组,保存着服务器中所有的数据库
redisDb *db;
//服务器中的数据库数量
int dbnum;
}

切换数据库

每个redis客户端都有自己的目标数据库,即写命令的操作对象,默认为0号数据库。

执行select 2(需要转换到的数据库,即db[2])命令可切换目标数据库。

1
2
3
4
typedef struct redisClient;{
//记录客户端当前使用的数据库
redisDb *db;
}redisClient;

数据库键空间

Redis是一个键值对数据库服务器,服务器每一个数据库都是由一个redisDb表示。

1
2
3
4
typedef struct redisDb{
//数据库键空间,保存着数据库中所有的键值对
dict *dict;
}redisDb;

键空间与用户所见的数据库是直接对应的,键空间的键就是数据库的键,键空间的值就是数据库的值。

数据库的键空间是一个字典,所以所有针对数据库的操作都是通过对键空间的字典进行操作来实现的。

添加新键

1
set date "222"

删除键

1
del date

更新键

1
set date "balst"

取值

1
get date

等命令。

读写键空间的维护操作

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

  • 读取一个键后,服务器会根据键是否存在来更新服务器的键空间命中次数或不命中次数。
  • 读取一个键后,服务器会更新键LRU时间,用于计算键的闲置时间。
  • 服务器在读取一个键时发现该键已经过期,服务器会先删除这个过期键,然后才执行余下的其他操作。
  • 如果有客户端使用Watch监视了某个键,服务器在对该键进行修改后,会将这个键标记为脏,从而让事务注意到这个键已经被修改。
  • 服务器每次修改一个键后,都会对脏键计数器++,这个计数器会触发服务器的持久化以及复制操作。
  • 如果服务器开启了数据库通知功能,那么在对键进行修改后,服务器将按配置发送给相应的数据库通知。

设置键的生存时间或过期时间

使用EXPIRE命令或者PEXPIRE,以毫秒或秒的精度为数据库的某个键设置生存时间。在经过指定的时间,服务器会自动删除生存时间为0的键。

以Expireat或PExpireat为键设置过期时间,当过期时间来临,自动删除该键。

保存过期时间

redisDb结构中的expires字典保存了数据库中所有键的过期时间,称为过期字典。

1
dict *expires;//值是long long类型的整数,保存这个键指向的数据库键的过期时间

命令:

1
2
3
设置过期时间:expire <key> <ttl>
删除过期时间:persist <key>
获取剩余过期时间:ttl <key>

过期键判定

  • 检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间。
  • 检查当前UNIX时间戳是否大于键的过期时间,如果是则键已过期。

过期键删除策略

如果一个键过期了,那么它什么时候会被删除呢:

  • 定时删除:
    • 在设置键的过期时间的同时,创建一个定时器,让定时器在过期时间来临时,立即执行对键的删除操作。
    • CPU时间不友好,可能占用相当一部分的CPU时间。
    • 创建一个定时器需要时间事件,而时间事件的实现方式是无序链表,不能高效处理大量时间事件。
  • 惰性删除:
    • 放任键过期不管,每次从键空间获取键时,都检查获得的键是否过期。
    • 对内存最不友好,可能导致内存泄漏。
  • 定期删除:
    • 每隔一段时间,就对数据库进行一次检查,删除里面的过期键。删除的数量以及检查多少数据库由算法决定。

Redis的过期键删除策略

采用惰性删除与定期删除两种策略:

  • 惰性删除:读写操作前判断ttl,如过期则删除。
  • 定期删除:在redis定时事件中随机抽取部分key判断ttl。
    • 函数运行时从一定量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。
    • 全局变量记录当前函数检查的进度,下一次调用时会接着上一次的进度处理,即上次到了10号数据库,下次从11号数据库开始。
    • 不断迭代,直到所有数据库都检查一次,开始再次迭代。

特点:

  • 并不一定是按照设置事件准时地过期。
  • 定期删除的时候会判断过期比例,达到阈值才会退出。

建议打散key的过期事件,避免大量key在同一时间点过期。

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

过期键对Redis服务器中其他模块的影响。

生成RDB文件

SAVE命令或BGSAVE命令创建一个新的RDB文件时,已过期的键不会被保存到新创建的RDB文件中。

是经过压缩的二进制格式,fork子进程dump可能会造成瞬间卡顿。

载入RDB文件

在启动Redis服务器,如果开启了RDB功能,服务器会对RDB文件载入:

  • 如果服务器以主服务器模式运行,那么载入RDB文件时,过期键将会忽略。
  • 如果以从服务器运行,文件中所有键,不管是否过期,都会载入到数据库中。
    • 主从服务器进行数据同步时,从服务器的数据库就会被清空,因此过期键对从服务器也不会造成影响。

AOF文件写入

当服务器以AOF持久化模式运行时,如果键已经过期,但没有删除,那么AOF文件不会因为这个过期键产生任何影响。

当过期键被删除后,程序会向AOF文件追加DEL命令,以显式记录该键已经被删除。

  • 先写aof缓存,再同步到aof文件。
  • AOF重写,达到阈值时触发,减少文件大小。

AOF重写

已过期的键不会被保存到重写后的AOF文件中。

复制

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

  • 主服务器在删除一个过期键后,会显式向所有从服务器发送一个DEL命令。
  • 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将其删除,而是继续像处理未过期键一样处理。
  • 从服务器只有在接到主服务器的DEL命令才会删除过期键。
    • 实现了主从数据库的数据一致性。

应用

利用AOF文件容灾。可以将数据恢复到最近3天任意小时粒度。

数据库通知

Redis2.8,可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。

  • 键空间通知,关注某个键执行了什么命令。

    • SUBSCRIBE _keyspace@0_:message

    • 当键执行了set命令,则返回:

      1
      2
      3
      "message" 
      "_keyspace@0_:message"
      "set"
  • 键事件通知,关注某个命令被什么键执行了。

    • SUBSCRIBE _keyevent@0_:del订阅所有的del行为。

    • 当key1被删除了:

    1
    2
    3
    "message" 
    "_keyspace@0_:del"
    "key1"

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

  • AKE:让服务器发送所有类型的键空间通知和键事件通知。
  • AK:发送所有类型的键空间通知。
  • AE:发送所有类型的键事件通知。
  • K$:只发送和字符串键有关的键空间通知。
  • El:只发送和列表键有关的键事件通知。

发送通知

通知功能由notify.c/notifyKeyspaceEvent函数实现。

1
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid);
  • type:想要发送的通知的类型。判断是否是服务器允许发送的通知。
  • event:事件的名称。
  • key:产生事件的键。
  • dbid:产生事件的数据库号码。

发送通知的实现:

  • 如果给定的通知不是服务器允许发送的通知,则直接返回。
  • 发送键空间通知
    • 将通知发送到频道keyspace@dbid\:<key>。
    • 内容为键所发生的事件<event>
    • 构建频道命中。
    • 发送通知。等同于执行PUBLISH。
  • 发送键事件通知。
    • 将通知发送到频道keyevent@dbid\:<key>。
    • 内容为发生事件的键<key>
    • 构建频道名称。
    • 发送通知。等同于执行PUBLISH。

持久化

redis所有数据保持在内存中,对数据的更新将异步地保存到磁盘当中。

  • 启动优先级:RDB低,AOF高(AOF可以保存最新的数据)。
  • 体积:RDB小,其是二进制。AOF大,是日志形式。
  • 恢复速度:RDB快,AOF慢。
  • 数据安全性:RDB丢数据,AOF根据策略决定。
  • 轻重:RDB重,AOF轻。

最佳策略:

  • 小分片。一个Redis进程不要分配太大内存。

RDB持久化

Redis是一个键值对数据库服务器,服务器中通常包含着任意个非空数据库,每个非空数据库又可以包含任意个键值对。

数据库状态:服务器中的非空数据库以及它们的键值对。

Redis是内存数据库,将数据库状态存储在内存当中,如果服务器进程一旦退出,则数据库状态也会消失不见。

因此提供了RDB持久化功能,将Redis在内存当中的数据库状态保存到磁盘当中

  • RDB持久化既可以手动,也可以根据服务器配置定期执行。将某个时间点上的数据库状态保存到一个RDB文件中。
  • RDB持久化功能生成的RDB文件是一个压缩的二进制文件。

RDB文件的创建与载入

  • 生成命令:SAVE于BGSAVE:
    • SAVE是同步的,会阻塞Redis服务器进程,直到RDB文件创建完毕为止。
    • BGSAVE是一个异步命令,会fork()一个子进程,子进程负责创建RDB文件,服务器进程继续处理命令。
    • 如果存在老的RDB文件,则会覆盖。

RDB文件的载入时在服务器启动时自动执行,只要检测到RDB文件,就会自动载入。在载入文件期间,服务器一直处于阻塞状态。

与AOF对比:

  • AOF文件的更新频率通常比RDB文件更新频率高,因此:
    • 如果服务器开启了AOF持久化功能,服务器会优先使用AOF文件来还原数据库状态。
    • 只有在AOF持久化功能处于关闭状态,服务器才会使用RDB文件来还原数据库状态。

自动间隔保存

Redis允许用户通过服务器配置save选项,让服务器每隔一段时间自动执行一次BGSAVE。

RDB文件结构

结构:

1552468398778

  • RDB文件保存的是二进制数据,而不是C字符串。
  • REDIS:长度为5字节,通过检测该字符,快速检查所载入的文件是否为RDB文件。
  • db_version:4字节,是一个字符串表示的整数,记录了RDB文件版本号。
  • database:包含0或任意个数据库,以及各个数据库中的键值对数据。
  • EOF:长度为1字节,表示正文内容结束。
  • check_sum:一个8字节长的无符号整数,保存一个校验和,通过对前4部分计算得出。服务器载入RDB文件时,会将载入数据计算得出的校验和与check_sum进行比较,以检查文件是否出错或损坏。

database部分

如果0号与3号数据库非空,则:

1552468706260

每个非空数据库在RDB文件中的保存:

1552468722522

  • SELECTDB:长度为1字节,表示接下来读取的是一个数据库号码。
  • db_number:保存着一个数据库号码,长度1、2、5字节,当读取到的时候,服务器使用select进行数据库切换。
  • key_value_pairs保存所有键值对数据。

key_value_pairs

如果键值对带有过期时间,则也会保存在内。不带过期时间的表示:

1552468871546

1552468919035

  • TYPE:对象类型或者底层编码,决定如何读入和解释value数据。
  • key:字符串对象。
  • EXPIRETIME_MS:1字节,表示接下来将读取一个过期时间,以毫秒为单位。
  • ms:8字节带符号整数,记录一个毫秒为单位的UNIX时间戳。

value的编码

不同类型的值对象在RDB文件中的保存结构:

  • 字符串对象

  • 列表对象

  • 集合对象

  • 哈希表对象

  • 有序集合对象

分析RDB文件

应用策略

关闭。但如果主从复制的话,则需要RDB写入。

集中管理。RDB对数据备份具有作用,其文件较小,作为快照也容易管理。

主从,从开。

AOF持久化

通过保存Redis服务器所执行的写命令来记录数据库状态。

1552469157583

写入的命令都是以Redis的命令请求协议格式保存的。

AOF持久化的实现

功能实现分为:命令追加、文件写入、文件同步三个步骤。

命令追加

当服务器执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区末尾。

文件写入与同步

  • 文件写入:只是写入到了内存缓冲区,可能还没有写到文件所拥有的磁盘数据块上。
  • 文件同步:将缓冲区中的内容冲洗到磁盘上。

Redis服务器进程就是一个事件循环,在循环中的:

  • 文件事件负责接收客户端的命令请求,以及向客户端发送命令恢复。
  • 时间事件负责执行像serverCron函数这样需要定时运行的函数。

服务器在处理文件事件时可能执行写命令,使得一些内容被追加到aof_buf缓冲区,因此服务器每次结束一个事件循环前,都会调用函数,考虑是否将aof_buf缓冲区里的内容写入和保存到AOF文件中。

1552470569474

flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值决定:

  • 值为always,将缓冲区内所有内容写入并同步到AOF文件。
  • 默认为everysec,将缓冲区内所有内容写入到AOF文件,如果上次同步AOF文件的时间距离现在超过1S,则再次对AOF文件进行同步。
  • no,将缓冲区内所有内容写入并同步到AOF文件,但并不进行同步,何时同步由操作系统决定。

AOF文件载入与数据还原

1552470819774

AOF重写

随着服务器允许,AOF文件内容与体积会越来越大,可能会对服务器造成影响,并且文件太大会使得AOF文件进行数据还原时间太长。

AOF文件重写:创建一个新的AOF文件替代现有的AOF文件,新旧的AOF保存的数据库状态相同,但是新的AOF文件不包含任何浪费空间的冗余指令。

冗余指令:

1552470946418

AOF文件重写的实现

通过读取服务器当前数据的状态来实现。

AOF后台重写

因为Redis使用单个线程处理命令请求,如果由服务器调用重写,则会无法处理客户端命令请求:

使用子进程进行重写。

  • 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以避免在使用锁的情况下,保证数据的安全性。
  • 在AOF重写期间,可能会出现对现有数据库状态的修改。
    • 设置AOF重写缓冲区,当Redis执行完一个写命令,会将命令同时发送给AOF缓冲区与AOF重写缓冲区。
    • 当子进程完成AOF后,向父进程发送信号,将AOF重写缓冲区所有内容写入新AOF文件。
    • 替换原有的AOF文件。

应用策略

开。对服务器压力不是很大。

everysec执行。

开销

CPU:

  • RDB和AOF文件生成,属于CPU密集型。
  • 优化:不做CPU绑定,不和CPU密集型部署。

内存:

  • fork内存开销, copy-on-write。
  • 不要和高硬盘负载服务部署在一起:存储服务、消息队列等。

事件

Redis服务器是一个事件驱动程序,需要处理一下两类事件:

  • 文件事件:Redis服务器通过套接字与客户端进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端的通信会产生相应的文件事件,服务器通过监听并处理这些事件完成一系列的网络通信操作。
  • 时间事件:需要在给定的时间点执行的操作。

1566436870819

文件事件

Redis基于Reactor模式开发了自己的网络事件处理器,这个处理器被称为文件事件处理器。

  • 文件事件处理器使用IO多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
  • 当被监听的套接字准备好执行连接accept、read、write、close等操作时,与操作相对应的文件事件就会产生,此时文件事件就会调用套接字之前关联好的事件处理器来处理这些事件。

文件事件处理器将IO事件监听与事件执行分离,保持了Redis内部单线程设计的简单性。

文件事件处理器的构成

1566400379933

服务器通常会连接多个套接字,因此多个文件事件有可能会并发地出现。

尽管会并发地出现,但是IO多路复用程序总是会将所有产生事件的套接字都放到有关队列中,然后通过这个队列,以有序、同步、每次有关套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕后,IO多路复用程序才会继续传送下一个套接字。

文件事件分派器会接收IO多路复用程序传来的套接字,根据套接字产生的事件类型调用相应的事件处理器。

服务器会为执行不同任务的套接字关联不同的事件处理器,这些处理器时一个个函数,它们定义了某个事件发生时,服务器应该执行的动作。

IO多路复用程序的实现

其所有功能都是通过包装select、epoll、evport和kqueue这些IO多路复用函数库来实现的。

事件的类型

IO多路复用程序可以监听多个套接字的ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件。

  • 当套接字变得可读时(即客户端对套接字执行write事件,或执行close操作,即Redis读取客户端的写入),或有新的可应答套接字出现时(客户端对服务器的监听套接字执行connect),套接字产生AE_READABLE事件。
  • 当套接字变得可写时(客户端对套接字执行read操作),套接字产生AE_WRITABLE事件。

IO多路复用程序允许服务器同时监听套接字的AE_READABLE事件与AE_WRITABLE事件,如果应该套接字同时产生了这两种事件,那么文件事件分派器会优先处理AE_READABLE事件,等到AE_READABLE处理完毕才会处理AE_WRITABLE。

即如果套接字可读又可写,则先处理读套接字,再处理写套接字。

文件事件的处理器

Redis为文件事件编写了多个处理器,分别用于实现不同的网络通信需求:

  • 为了对连接服务器的各个客户端进行应答,服务器要为监听套接字关联连接应答处理器。
  • 为了接收客户端传来的命令请求,服务器要为客户端套接字关联命令请求处理器。
  • 为了向客户端返回命令的执行结果,服务器要为客户端套接字关联命令恢复处理器。
  • 当主服务器和从服务器进行复制操作时,主从服务器都需要关联特别为复制功能编写的复制处理器。

连接应答处理器

  • 当Redis服务器进行初始化时,程序会将这个连接应答处理器和服务器监听套接字的AE_READABLE事件关联起来。
  • 当有客户端连接服务器监听套接字时,就会产生AE_READABLE事件。
  • 引发连接应答处理器执行并执行相应的套接字应答操作。

命令请求处理器

负责从套接字中读入客户端发送的命令请求内容。

  • 当一个客户端向服务器发送命令请求时,就会产生AE_READABLE事件。
  • 引发命令请求处理器执行,并执行相应的套接字读入操作。
  • 在客户端连接服务器的整个过程当中,服务器会一直为客户端套接字的AE_READABLE事件关联命令请求处理器。

命令回复处理器

负责将服务器执行命令后得到的命令回复通过套接字返回给客户端。

  • 当有命令回复需要传送给客户端的时候,服务器会将客户端套接字的AE_WRITABLE事件和命令回复处理器关联起来。
  • 当客户端准备好接收服务器传回的命令回复时,就会产生AE_WRITABLE事件。
  • 引发命令回复处理器执行,并执行相应的套接字写入操作。
  • 命令回复发送完毕后,服务器就会解除命令回复处理器与客户端套接字的AE_WRITABLE事件间的关联。

时间事件

Redis的时间事件分为两类,其类型取决于时间事件处理器的返回值:

  • 定时事件:让一个程序在指定的时间后执行一次。
    • 时间处理器返回ae.h/AE_NOMORE即为定时事件,执行一次后就删除。Redis暂时不使用。
  • 周期性事件:让一段程序每隔一段时间就执行一次。
    • 非AE_NOMORE的整数值,则该值为每次执行的时间间隔。

一个时间事件主要有以下三个属性:

  • id:服务器为时间事件创建的全局唯一ID,新事件的ID>旧事件的ID。
  • when:毫秒精度的UNIX时间戳,记录了时间事件的到达事件。
  • timeProc:时间事件处理器,当时间事件到达时,服务器就会调用相应的处理器去处理事件。

实现

服务器将所有的时间事件都放在一个无序链表当中,每当事件事件执行器运行时,就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。

新的时间事件总会插入在表头,即按ID逆序排序。

正常情况下的时间事件只有serverCron,即链表大小非常小,不会影响性能。

ServerCron

Redis服务器需要对自身的资源和状态进行自检和调整,从而保证服务器可以长期、稳定的运行,而这些操作由serverCron负责执行。

  • 更新服务器的各类统计信息,比如是时间、内存占用、数据库占用情况等。
  • 清理数据库中的过期键值对。
  • 关闭和清理连接失效的客户端。
  • 尝试进行AOF或RDB持久化操作。
  • 如果服务器是主服务器,那么对从服务器进行定期同步。
  • 如果处于集群模式,对集群进行定期同步和连接测试。

可以通过修改hz选项调整每秒执行次数。Redis2.6执行次数为10次/s。

事件的调度与执行

Redis需要考虑何时处理文件事件,何时处理时间事件,花多少时间处理它们等等。

事件的调度与执行由ae.c/aeProcessEvents负责,其处理流程为:

  • 获取到达时间离当前时间最接近的时间事件。
  • 计算最接近的时间事件距离到达还要多少毫秒。
  • 如果事件已经到达,那么时间可能为负数,则将它设定为0.
  • 根据remaind_ms的值,创建timeval结构。
  • 阻塞并等待文件事件的产生,最大阻塞时间由传入的timeval结构决定。
    • 如果remaind_ms==0,则非阻塞,aeApiPoll调用后立即返回。
    • 这个设计避免了忙等待,也确保不会阻塞太长时间。
  • 处理所有已经产生的文件事件。
  • 处理所有已到达的时间事件。

所有的事件处理都是同步、有序、原子执行的,不会中途中断事件处理,也不会抢占,因此事件处理器都会尽可能减少程序阻塞事件,并在有时候需要主动让出执行权避免事件饥饿的可能性。

若将一个命令回复写入到客户端套接字,如果写入字节超过预设常量就会主动break跳出,将余下数据留到下次再写。时间事件将持久化操作放入到了子进程当中。

1566403644529

客户端

Redis服务器是典型的一对多服务器程序:一个程序可以与多个客户端建立网络连接,每个客户端可以向服务器发送命令请求,而服务器接收并处理客户端发送的命令请求,并向客户端返回命令回复。

通过使用由IO多路复用技术实现的文件事件处理器,Redis使用单线程单进程处理命令,并与多个客户端进行网络通信。

对于每个要连接的客户端,服务器都为这些客户端建立了相应的redis.h/redisClient,其保存了客户端当前的状态信息,以及执行相关功能时需要的数据结构:

  • 客户端的套接字描述符。
  • 客户端的名字。
  • 客户端的标志值flag。
  • 指向客户端正在使用的数据库的指针,以及该数据库的号码。
  • 客户端当前要指向的命令、命令的参数、命令参数的个数,以及指向命令实现函数的指针。
  • 客户端的输入缓冲区和输出缓冲区。
  • 客户端的复制状态信息,以及进行复制所需的数据结构。
  • 客户端指向BRPOP、BLPOP等列表阻塞命令时使用的数据结构。
  • 客户端的事务状态,以及执行watch命令时用到的数据结构。
  • 客户端执行发布与订阅功能时用到的数据结构。
  • 客户端的身份验证标志。
  • 客户端的创建事件,客户端和服务器的最后一次通信的事件,以及客户端的输出缓冲区大小超出软性限制的时间。

客户端的结构属性是一个链表,保存了所有与服务器连接的客户端的状态,要对客户端执行批量操作、对指定客户端操作都可以通过遍历它来完成。

客户端属性

  • 通用的属性。
  • 与特定功能相关的属性。操作数据库需要用到的db属性和dictid属性,执行事务时要用到的mstate属性,执行watch命令需要用到的watched_keys属性等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct redisClient {
int fd; //记录客户端正在使用的套接字描述符。
robj *name;//客户端名称。
int flags;//客户端的角色,以及客户端当前所处的状态。
sds querybuf;//输入缓冲区,保存客户端发送的命令请求。
robj **argv;//命令参数。
int argc;//命令参数的个数。
struct redisCommand *cmd;//命令的实现函数,根据argv[0]确定。
char buf[REDIS_REPLY_CHUNK_BYTES];//固定大小的缓冲区,默认值16KB
int bufpos;//记录了buf数组目前已经使用的字节数量。
list *reply;//可变大小的缓冲区。
int authenticated;//身份验证状态。
time_t ctime;//客户端创建时间
time_t lastinteraction;//客户都安与服务器最后一次交互时间
time_t obuf_soft_limit_reached_time;//记录输出缓冲区第一次到达软性限制的时间。
}

套接字描述符

不同的客户端类型fd的值不同。

  • 伪客户端值为-1,伪客户端处理的命令请求来源于AOF或Lua脚本,而不是网络,因此不需要套接字连接。
    • 载入AOF文件还原数据库状态。执行LUA脚本。
  • 普通客户端的值>-1的整数。

名字

默认情况下客户端是没有名字的。

使用client setname可以设置名称。

标志

flags的值可以伪单个标志,也可以是多个标志的或语句:

1
flags = <flag1> [| <flag2> ...]

每个标志用一个常量来表示,一部分标志记录了客户端的角色。

  • 主从复制时,主服务器会成为从服务器的客户端,从服务器也会成为主服务器的客户端。Redis_Maser标志客户端代表的是一个主服务器,Redis_Server代表客户端是一个从服务器。
  • Redis_PRE_Psync代表客户端版本低于Redis2.8,不可用Psync进行同步。
  • Redis_Lua_Client标识客户端是专门用于处理Lua脚本的伪客户端。
  • Redis_Force_AOF强制服务器将当前执行的命令写入到AOF文件。
  • ….

输入缓冲区

用于保存客户端发送的命令请求。

输入缓冲区会根据输入内容动态扩容或缩小,但最大大小不能超过1GB,否则将关闭该客户端。

命令与命令参数

服务器对缓冲区的内容进行分析,将参数与参数个数分别保存到客户端状态的argv和argc属性。

命令的实现函数

服务器将根据argv[0]的值在命令表当中查找命令所对应的命令实现函数。

输出缓冲区

每个客户端都有两个输出缓冲区可用,一个缓冲区的大小是固定的,另一个大小是可变的:

  • 固定大小的缓冲区用于保存长度较小的回复,如OK、错误回复等。默认大小16KB。
  • 可变大小的缓冲区用于保存长度较大的回复。
    • 当固定大小缓冲区不可用时使用。

身份验证

authenticated用于记录客户端是否通过了身份验证。

如果值为0,则未通过,否则代表已经通过。此时只有AUTH命令可执行,其他都会被拒绝。

时间

客户端的创建与关闭

如果客户端是通过网络连接与服务器进行连接的普通客户都安,那么在客户端使用connect函数连接到服务器时,服务器就会调用连接事件处理器建立相应的客户端状态。

关闭客户端

客户端可用因为多种原因被关闭:

  • 客户端进程退出或被杀死。
  • 客户端向服务器发送了带有不符合协议格式的命令请求。
  • 客户端成为了Client kill命令的目标。
  • 用户为服务器设置了timeout配置项,当空转时间超过timeout,则会被关闭。
    • 如果主从复制正在被BLPOP等命令阻塞,或在执行Subscribe等订阅命令,即使客户端空转也不会被服务器关闭。
  • 客户端发送的命令请求大小超过了输入缓冲区的限制(默认1GB)。
  • 要发送给客户端的命令回复超过了输出缓冲区的限制大小。Redis使用了两种模式限制其大小:
    • 硬性限制:如果大小超过了硬性限制,则立即关闭。
    • 软性限制:输出缓冲区的大小超过了软性但没有超过硬性,则将记录下其到达软性限制的起始时间,并继续监视客户端,如果一直超出软性限制并持续时间超过服务器设定的时长,则关闭客户端。

Lua脚本的伪客户端:在服务器初始化时创建,并一直存在。

AOF文件的伪客户端:服务器在载入AOF文件时,会创建用于执行AOF文件中redis命令的伪客户端,在载入完成后会关闭。

客户端连接池Jedis

客户端是线程池,而redis是单线程,即客户端与redis建立多个连接。

1566437953284

服务器

Redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源关联来维持服务器自身的运转。

命令请求的执行过程

当使用客户端执行:set key value,则服务器与客户端的操作有:

  • 客户端向服务器发送命令请求set key value
  • 服务器接收并处理客户端发来的命令请求set key value,在数据库中进行设置操作,并产生命令回复OK。
  • 服务器将命令回复OK发送给客户端。
  • 客户端接收服务器返回的命令回复OF,并将这个回复打印。

发送、读取命令请求

客户端会首先将这个命令转换伪协议格式,然后将命令发送,即:

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

客户端的写入导致连接套接字可读,服务器将调用命令请求处理器来执行以下操作:

  • 读取套接字当中的协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里。
  • 对输入缓冲区内的命令请求进行分析,提取出命令请求中包含的命令参数,以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的argv属性和argc属性中。
  • 调用命令执行器,执行客户端指定的命令。

命令执行器

查找命令实现

  • 根据argv[0]参数,在命令表(字典)中查找参数所指定的命令,并将找到的命令保存到客户端状态的cmd属性中。

redis的redisCommand结构:

1566435828805

sflags可用的标志值:

1566435854117

对于set命令,器函数伪setCommand,命令参数个数为-3,接收3个或以上的参数。命令标识符为“wm”,标识是一个写入命令,并在执行前服务器需要对占用内存进行检查,因为该命令可能占用大量内存。

执行预备操作

程序需要进行一些预备操作保证命令可正确、顺利地被执行。

  • 检查客户端状态的cmd指针是否执行NULL,如果是即找不到相应命令。
  • 根据redisCommand的arity检查给定参数个数是否正确,如果不正确不执行后续步骤。
  • 检查客户端是否通过了身份验证,如果没有只能执行AUTH。
  • 如果服务器打开了maxmemory,则执行命令前先检查内存占用,并在有需要时进行内存回收。
  • 如果服务器上一次执行BGSAVE出错,并服务器打开了stop-writes-on-bgsave-error,且服务器要执行写命令,那么将拒绝执行该命令。
  • 如果正在执行SUBSCRIBE订阅频道,或PushSCRIBE订阅模式,那么服务器只会执行Subscribe、psubscribe、unsubscribe、punsubscribe。
  • 如果服务器正在进行数据载入,那么客户端的命令必须带有l标识才会被执行,例如info、publish、shutdown。
  • 如果服务器因为执行Lua而超时进入阻塞,则只会执行shutdown nosave和script kill。
  • 如果客户端正在执行事务,那么只会执行EXEC、DISCARD、MULTI、Watch,其他命令都会放入事务队列中。
  • 如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数发送给监视器。

调用命令的实现函数

即执行proc(client)即可。命令回复会保存在客户端状态的输出缓冲区,服务器为套接字关联命令回复处理器,处理器负责将命令回复返回客户端。

执行后续工作

  • 如果服务器开启了慢查询日志功能,那么慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加一条新的慢查询日志。
  • 根据刚刚执行命令所耗费的时长,更新被执行命令的redisCommand结构的milliseconds,并将calls计数器++。
  • 如果开启了AOF,则AOF模块将命令写入AOF缓冲区。
  • 如果有主从,则服务器会将刚执行的命令传播给所有从服务器。

客户端接收

当客户端套接字可写,服务器就会执行命令回复处理器将保存在输出缓冲区中的命令回复发送给客户端。

serverCron函数

  • 更新服务器的时间缓存。
  • 更新LRU时钟。
  • 更新服务器每秒执行命令次数。
  • 更新服务器的内存峰值记录。
  • 处理Sigterm信号。
  • 管理客户端资源。
  • 管理数据库资源。
  • 执行被延迟的BGRewriteAOF。
  • 检查持久化操作的运行状态。
  • 将AOF缓冲区的内容写入AOF文件。
  • 关闭异步客户端。
  • 增加cronloops计数器的值。

初始化服务器

参考