Redis设计 - 服务器

前言

Redis服务器负责与多个客户端建立网络连接,并处理客户端的请求,通过资源管理来维持服务器自身的运转。

命令请求的执行过程

一个命令请求从客户端发送到服务器,直至获得回复,都需要经过一系列的处理和交互,大致可以分为以下几个步骤:

  • 客户端向服务器发送命令请求 SET KEY VALUE
  • 服务器接收并处理客户端发来的命令请求 SET KEY VALUE,在数据库中执行操作,并产生命令回复OK
  • 服务器将命令回复OK发送给客户端
  • 客户端接收服务器返回的命令回复OK,并做对应的业务逻辑

下面讲对这些操作的执行细节进行补充

1. 发送命令请求

首先客户端将命令转换成协议格式,然后通过连接服务器的套接字,将协议格式的命令发送给服务器。

客户端发送命令过程
2.读取命令请求

服务器接收到客户端的命令后,调用命令请求处理器,执行以下操作:

  1. 读取套接字中的命令请求,并将其保存在客户端的输入缓冲区。
  2. 对输入缓冲区的命令进行分析,提取其中包含的命令参数和参数个数,并保存到redisClient的argv和argc属性。
  3. 调用命令执行器,执行客户端命令。
3. 命令执行器

3.1 查找命令实现
命令执行器首先会根据redisClient保存的argv[0]参数,在命令表中查找所指定的命令,并将找到的命令保存到客户端状态的cmd属性中。

命令表是一个字典,键是命令的名字,如 set、get等,值是redisCommand结构,该结构记录了命令的实现信息。

3.2 执行预备操作
目前为止,服务器已经得到了执行命令的实现函数、参数、参数个数,但是执行前还要一些预备操作(单机,集群需要的预备操作更多):

  • 检查客户端状态的cmd是否为NULL,为NULL则说明没有对应的命令实现
  • 检查客户端提供的参数数量是否符合要求
  • 如果服务端开启了认证,检查客户端是否通过了身份验证
  • 如果服务器打开了maxmemory,那么执行命令前,先检查服务器的内存占用情况,并在有需要时进行内存回收
  • 如果服务器上一次执行BGSAVE命令出错,并且打开了stop-writes-on-bgsave-error功能,那么拒绝执行修改命令
  • 如果客户端正在使用subscribe命令订阅频道,或者正在用psubscribe命令订阅模式,那么服务器只会执行此客户端发过来的,subscribe,psubscribe,unsubscribe, punsunscribe四个命令,其他命令都会拒绝
  • 如果服务器正在进行数据载入,那么客户端发送的命令必须带有i标识(比如info,shutdown,publish等等),才会被服务器执行
  • 如果服务器正在执行lua脚本而阻塞,那么服务器只会执行客户端发来的SHUTDOWN nosave命令和SCRIPT KILL命令,其他命令都会拒绝
  • 如果客户端正在执行事务,那么服务器只会执行客户端发来的EXEC、DISCARD、MULTI、WATCH四个命令,其他命令都会被放进事务队列中
  • 如果服务器打开了监视器功能,那么服务器会将要执行命令和参数的信息发给监视器

3.3 调用命令的实现函数
服务器已经将命令的实现(cmd)、命令参数(argv)和参数个数(argc)保存在redisClient中,接下来只需要将参数传递给命令的实现函数即可。执行完实现函数后的返回值会被写入客户端的输出缓冲区,并为客户端的套接字关联命令回复处理器。

3.4 执行后续工作
执行完实现函数之后,服务器需要执行一些后续工作:

  • 如果服务器开启了慢查询日志功能,那么慢查询日志模块就会检查刚才的查询是否需要记录
  • 根据执行命令的耗时,更新命令redisCommand对象的millseconds属性,并将calls计数器加1
  • 如果开启了AOF功能,且对数据库做了更改,那么需要将此条写入到AOF缓冲区中
  • 如果有其他从服务器复制当前这个服务器,那么服务器会将刚刚执行的命令传播给所有的从服务器。

3.5 将结果回复给客户端
客户端套接字变WRITEABLE时,命令回复处理器会将结果以协议格式发送给客户端,如 "+OK\r\n"
客户端接收到后,按照协议格式进行转换成人类可读格式。

serverCron函数

serverCron函数每隔100毫秒执行一次,负责维护服务器资源,保持redis良好的运转。下面将介绍serverCron函数执行的操作,redisServer结构中的一些属性

1. 更新服务器时间缓存

Redis服务器中有不少功能需要获取系统当前时间,为了减少系统调用执行次数,服务器状态中的unixtime属性和mstime属性被用作当前时间的缓存。

struct redisServer {
    // 保存秒级精度的系统当期unix时间戳
    time_t unixtime;
    // 保存毫秒级进度的系统当前unix时间戳
    long long mstime;
};

因为serverCron函数是默认每100毫秒执行一次,因此这个时间的精度并不高
• 服务器只会在打印日志,更新服务器lru时钟,判断持久化条件,计算服务器上线时间等这类对精度要求不高的功能上
• 对于为键设置过期时间,添加慢查询日志等需要高精度时间要求的来说,Redis依旧会调用系统函数,获得准确的时间

2. 更新LRU时钟

服务器状态中保存了用于计算LRU用的时钟lruclock,它和上面介绍的时间缓存一样,都是时间缓存的一种

struct redisServer {
    // 默认每10秒更新一次时钟缓存,用于计算键的空转时长
      unsigned lruclock:22;
}

//每个redis对象都会有一个lru属性,这个lru属性保存了对象最后一次被命令访问的时间
typedef struct redisObject {
    unsigned lru:22;
}

// 查看键的空转时间
reids> OBJECT IDLETIME key
(integer) 10

数据库键的空转时间 = lruclock时间 - 对象的lru时间

3. 更新服务器每秒执行命令次数

serverCron中的trackOperationsPerSecond函数以每100毫秒一次的频率执行,以抽样计算的方式估算最近一秒钟处理的请求数量。

可通过INFO status命令查看:

redis> INFO status 
...
instantaneous_ops_per_sec:6 // 最近一秒钟处理了6个命令
...
4. 更新服务器内存峰值记录

redisServer中的stat_peak_memory属性记录了服务器内存峰值

每次执行serverCron时,程序都查看服务器当前使用的内存数量,并与当前的stat_peak_memory值进行比较,大则将之替换。

5. 处理SIGTERM信号

在启动服务器时,Redis会为服务器进程的SIGTERM信号关联处理器sigtermHandler函数,这个函数在服务器接收到SIGTERM信号时,打开服务器状态的shutdown_asap标记。

static void sigtermHandler(int sig) {
    // 打印日志
    redisLogFromHandler(REDIS_WARING, "received SIGTERM,scheduling shutdown...");
    // 打开关闭标识
    server.shutdown_asap = 1;
}

serverCron函数运行时,会检查服务器状态的shutdown_asap,判断是否关闭服务器。

6. 管理数据库资源

serverCron函数每次执行都会调用databasesCron函数,这个函数对服务器的数据库进行检查,删除过期键,并在需要的时候对字典进行收缩操作。

7. 执行被延时的BGREWRITEAOF

在服务器执行BGSAVE命令的期间,如果客户端向服务器发送BGREWRITEAOF命令,那么该命令会被延时,直到BGSAVE执行完毕。

服务器的aof_rewrite_scheduled标记记录了服务器是否延迟BGREWRITEAOF命令:

struct redisServer {
    //如果值为1,表示BGREWRITEAOF命令被延迟
    int aof_rewrite_scheduled;
}

每次serverCron函数执行时,都会检查BGSAVE命令或者BGREWRITEAOF命令是否在执行,如果这个两个命令都没在执行,并且标记为1,则服务器就会执行BGREWRITEAOF命令。

8. 检查持久化操作的运行状态

服务器使用rdb_child_pid属性记录执行BGSAVE命令的子进程ID,aof_child_pid属性记录BGREWRITEAOF命令的子进程ID,这两个属性可用于检查BGSAVE或者BGREWRITEAOF命令是否在执行。

struct redisServer {
    // 如果服务器没有在执行BGSAVE,值为 -1
    pid_t rdb_child_pid;
    // 如果服务器没有在执行BGREWRITEAOF,值为 -1
    pid_t aof_child_pid;
}

1)serverCron函数在运行时,都会检查两个属性的值是否为-1,只要其中一个不是-1,就会执行一次wait3函数,检查子进程是否有信号发到服务器进程:

  • 如果有信号到达,表示新的RDB文件或者AOF文件已经重写完成,服务器需要执行命令后续的操作,例如替换旧RDB文件,替换旧AOF文件等等
  • 如果没有信号到达,表示持久化操作还未完成,程序不做动作。

2)如果两个属性值都为-1,那么服务器目前没有进行持久化操作,这种情况下程序执行以下三个检查:

  • 查看是否有BGREWRITEAOF被延迟了,也就是上面所说的情况
  • 检查服务器自动保存条件是否满足,如果满足,那么开始一场新的BGSAVE操作。
  • 检查服务器设置的AOF重写条件是否满足,如果满足且没有其他持久化操作,则进行后台AOF重写

以下流程图展示了检查过程

持久化流程检查
9. 将AOF缓冲区中的内容写入AOF文件

如果服务器开启了AOF持久化功能,并且AOF缓冲区里面还有待写入的数据,那么serverCron函数会调用相应的程序,将AOF缓冲区的内容写入到AOF文件里面。

10. 关闭异步客户端

服务器会关闭那些输出缓冲区超过限制的客户端。

初始化服务器

Redis服务器从启动到能够接受客户端的命令请求,需要经过一系列的初始化和设置过程,比如初始化服务器状态,设置用户指定的服务器配置,创建后续的数据结构和网络连接等等。

1. 初始化服务器状态结构(redisServer)

初始化就是创建一个redisServer类型的实例变量server作为服务器的状态,并为各个属性设置默认值。
初始化工作由initServerConfig函数完成:

void initServerConfig(void) {
    // 设置服务器的运行id
    getRandomHexChars(server.runid, REDIS_RUN_ID_SIZE);
    // 为运行id加上结尾字符
    server.runid[REDIS_RUN_ID_SIZE] = '\0';
    // 设置默认配置文件路径
    server.configfile = NULL;
    // 设置默认服务器频率
    server.hz = REDIS_DEFAULT_HZ;
    // 设置服务器的运行架构
    server.arch_bits = (sizeof(long) == 8) ? 64 : 32;
    // 设置默认服务器端口号
    server.port = REDIS_SERVERPORT;
    // ...
}
2. 载入配置选项

启动服务时,用户可以通过修改配置文件,或者在启动命令追加配置来达到修改参数的目的,如果用户指定了具体参数的值,则进行更新,否则使用默认值。
如:

$ redis-server redis.conf
3. 初始化服务器数据结构

initServerConfig函数初始化server状态时,只创建了命令表一个数据结构,其余数据结构在此步骤初始化:

  • server.clients链表,记录了所有与服务器连接的客户端状态。
  • server.db 数组,包含了服务器所有的数据库。
  • server.pusubchanels字典:用于保存频道订阅信息,server.pubsubpatterns链表:保存模式订阅信息。
  • 执行lua脚本环境的server.lua
  • 保存慢查询日志的server.slowlog属性

创建完以上属性后,开始调用initServer函数为数据结构分配内存。
initServerConfig负责初始化一般属性,initServer负责初始化数据结构,之所以分成两个步骤,是考虑到用户可以通过修改配置选项修改和数据结构相关的服务器状态属性,所以等到载入用户配置后,再进行数据机构的初始化。

除了初始化数据结构之外,还进行了一些非常重要的设置操作:

  • 为服务器设置进程信号处理器
  • 创建共享对象:这些对象包含了一些常用的字符串,整数1到1000的字符串
  • 打开服务器的监听端口,为监听套接字关联连接应答事件处理器,等待客户端连接
  • 为serverCron函数创建时间时间,等待服务器正式运行时执行serverCron函数
  • 如果AOF持久化功能已经打开,那么打开AOF文件,如果不存在AOF文件,则创建一个新的AOF文件,用作后续写入
  • 初始化服务器的后台I/O模块(bio),为将来I/O操作做好准备
4. 还原数据库状态

完成了对服务器状态server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,并根据配置来恢复数据库的内容:

  • 如果服务器启用了AOF持久化功能,那么服务器使用AOF文件来还原数据库
  • 如果没有开启AOF功能,使用RDB文件来恢复数据库状态
5. 执行事件循环

初始化的最后一步,开始执行事件循环(loop),开始接受客户端的连接请求。

回顾

本文介绍了Redis服务器的初始化过程、命令请求的执行过程和serverCron函数的主要工作,都是偏细节的东西,算是科普了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353