文章目录
- 一、Redis数据库
- Redis数据库redisDb
- 数据库键空间——dict
- 过期字典——expires
- 设置键的生存时间
- 移除键的过期时间
- 返回键的生存时间
- Redis的过期删除策略
- 1、定期删除
- 2、惰性删除
- 3、内存淘汰机制
- 过期键处理
- 1、RDB功能对过期键的处理
- 2、AOF功能对过期键的处理
- 3、复制功能对过期键的处理
- 数据库通知
- 二、Redis服务端
- 服务器redisServer
- 服务器初始化步骤
- 命令请求的执行过程
- 1、发送命令请求
- 2、读取命令请求
- 3、命令执行器【核心】
- 4、服务端将命令回复给客户端
- 5、客户端接收并打印命令回复
- serverCron函数【核心】
- 三、Redis客户端
- 客户端redisClient
- 客户端的创建与关闭
- 伪客户端
- 四、事件
- 文件事件
- 文件事件处理器
- 时间事件
- 事件的调度与执行
- 五、RDB持久化
- RDB文件的创建
- RDB文件的载入
- RDB相关参数
- RDB文件结构
- 六、AOF持久化
- AOF持久化实现
- 1、命令追加
- 2、文件写入与同步
- 3、文件载入与数据还原
- AOF重写
- AOF后台重写
- RDB 与 AOF 混用
先来一张图说明redis最重要的三种结构:服务器redisServer、客户端redisClient、数据库redisDb。
一、Redis数据库
Redis数据库redisDb
在Redis服务器状态结构中,“dbnum”属性记录了服务器的数据库数量,它的值可以通过服务器配置的“databases”选项决定,默认为16。“db”属性是一个数组,保存保存着服务器中的所有数据库,其中每一个数据库都对应一个“redisDb”数据结构。redisDb主要由dict键空间和expires过期字典构成。
struct redisDb {
dict *dict;//数据库键空间字典,保存数据库中所有的键值对
dict *expires;//过期字典,保存数据库中所有键的过期时间
dict *watched_keys;//字典,正在被WATCH命令监视的键
int id; /* Database ID */
};
数据库键空间——dict
Redis是一个键值对数据库服务器,服务器的每个数据库都由一个redisDb结构表示,redisDb结构中的dict字典保存了数据库中的所有键值对,称之为键空间。
键空间和用户所见的数据库是直接对应的:
键空间的键也就是数据库的键,每个键都是一个字符串对象。
键空间的值也就是数据库的值,每个值都可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象的任意一种Redis对象(RedisObject)。
Redis数据库结构的“dict”属性是Redis数据库的键空间,底层由字典实现。所有在数据库上的增删改查,实际上都是通过对键空间字典进行相应操作来实现的。除此之外,还需要进行一些额外的维护操作,主要有如下操作内容:
- 在读取一个键之后,服务器会根据键是否存在来更新服务器的键空间命中次数或不命中次数(这两个值可以在’info stats’命令返回中的’keyspace_hits’属性和’keyspace_misses’属性中查看)。
- 在读取一个键之后,服务器会更新键的LRU属性值(最后一次使用时间,使用’object idletime’命令可以查看键的闲置时间)。
- 在读取一个键时,若发现该键已过期,则删除这个键。
- 如果有客户端使用’watch’命令监视了某个键,服务器在对被监视的键进行修改之后,会将这个键标记为脏,从而让事务程序注意到这个键已经被修改过了。
- 服务器每次修改一个键之后,都会对脏键计数器(即Redis服务器状态的’dirty’属性)的值加一,这个计数器会触发服务器的持久化以及复制操作。
- 如果服务器开启了数据库通知功能,在对键进行修改之后,服务器将按配置发送相应的数据库通知。
过期字典——expires
过期字典——expires字典保存了数据库中所有键的过期时间或生存时间(TTL)。
设置键的生存时间
-
expire 命令用于将键key的生存时间设置为ttl秒。
-
pexpire 命令用于将键key的生存时间设置为ttl毫秒。
-
expireat 命令用于将键key的过期时间设置为某个指定的秒数时间戳。
-
pexpireat 命令用于将键key的过期时间设置为某个指定的毫秒数时间戳。
实际上expire、pexpire、expireat都是使用pexpireat命令实现的,无论客户端执行哪种命令,最后的执行效果都会转换为pexpireat命令。
移除键的过期时间
-
persist 命令用于移除过期时间。
实现原理:在过期字典中查找给定的键,并解除键和值在过期字典中的关联。
返回键的生存时间
-
ttl 以秒为单位返回键的剩余生存时间。
-
pttl以毫秒为单位返回键的剩余生存时间。
实现原理:都是通过计算键的过期时间和当前时间的差来实现。即当前时间 - 过期时间
Redis的过期删除策略
定期删除→惰性删除→内存淘汰机制
1、定期删除
定期删除是指Redis默认每隔 100ms 就 随机抽取 一些设置了过期时间的key,检测这些key是否过期,如果过期了就将其删除。
实现原理:在规定时间内,分多次遍历服务器的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。
- 定期时间的间隔,在Redis的配置文件redis.conf中有一个属
hz
,默认为10,表示1s执行10次定期删除,即每隔100ms执行一次,可以修改这个配置值。 - 随机抽取样本的数量,同样是由redis.conf文件中的
maxmemory-samples
属性决定的,默认为5。 - 之所以每隔一段时间只遍历一部分expires过期字典键,因为如果Redis里面有大量key都设置了过期时间,全部都去检测一遍的话CPU负载就会很高,会浪费大量的时间在检测上面,甚至直接导致redis挂掉。所有只会抽取一部分而不会全部检查。
正因为定期删除只是随机抽取部分key来检测,这样的话就会出现大量已经过期的key并没有被删除,这就是为什么有时候大量的key明明已经过了失效时间,但是redis的内存还是被大量占用的原因 ,为了解决这个问题,Redis又引入了“惰性删除策略”。
2、惰性删除
惰性删除不是去主动删除,而是在你要获取某个key 的时候,redis会先去检测一下这个key是否已经过期,如果没有过期则返回给你,如果已经过期了,那么redis会删除这个key,不会返回给你。
"定期删除+惰性删除"就能保证过期的key最终一定会被删掉 ,但是只能保证最终一定会被删除,要是定期删除遗漏的大量过期key,我们在很长的一段时间内也没有再访问这些key,那么这些过期key不就一直会存在于内存中吗?不就会一直占着我们的内存吗?这样不还是会导致redis内存耗尽吗?由于存在这样的问题,所以redis又引入了“内存淘汰机制”来解决。
3、内存淘汰机制
Redis 的内存占用会越来越高。Redis 为了限制最大使用内存,提供了 redis.conf 中的
配置参数 maxmemory
。当内存超出 maxmemory,Redis 提供了几种内存淘汰机制让用户选择,配置 maxmemory-policy
:
- noeviction:当内存超出 maxmemory,写入请求会报错,但是删除和读请求可以继续。
- allkeys-lru:当内存超出 maxmemory,在所有的 key 中,移除最少使用的key。只把 Redis 既当缓存是使用这种策略(推荐)。
- allkeys-random:当内存超出 maxmemory,在所有的 key 中,随机移除某个 key。
- volatile-lru:当内存超出 maxmemory,在设置了过期时间 key 的字典中,移除最少使用的 key。把 Redis 既当缓存,又做持久化的时候使用这种策略。
- volatile-random:当内存超出 maxmemory,在设置了过期时间 key 的字典中,随机移除某个key。
- volatile-ttl:当内存超出 maxmemory,在设置了过期时间 key 的字典中,优先移除 ttl 小的。
过期键处理
1、RDB功能对过期键的处理
(1)生成RDB文件:
在执行save或bgsave命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。
(2)载入RDB文件:
- 主服务器模式:载入RDB文件时,程序会对文件中保存的键进行检查,只有未过期的键会被载入到数据库中。
- 从服务器模式:文件中保存的所有键都会被载入到数据库中。不过因为主从服务器在进行数据同步的时候,从服务器的数据库会被清空,所以过期键对载入RDB文件的从服务器也不会造成影响。
2、AOF功能对过期键的处理
(1)AOF文件写入:
当过期键被惰性删除或定期删除之后,程序会向AOF文件追加一条del命令,来显式地记录该键已被删除。
(2)AOF重写:
程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。
3、复制功能对过期键的处理
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:
-
主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个del命令,告知从服务器删除这个过期键。
-
从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将其删除,而是将过期键的值继续返回给客户端。【所以对于从库来说,即使key已经过期,也可能被读取到】
-
从服务器只有在接到主服务器发送来的del命令之后,才会删除过期键。
通过由主服务器来控制从服务器统一删除过期键,可以保证主从服务器数据的一致性,也正是因为这个原因,当一个过期键仍然存在于主服务器的数据库时,这个过期键在从服务器里的复制品也会继续存在。
数据库通知
数据库通知是redis2.8版本新增加的功能,这个功能可以让客户通过订阅给定的频道或模式,来获知数据库中键的变化,以及数据库中命令的执行情况。
键空间通知:关注某个键执行的了什么命令。
键事件通知:关注某个命令被什么键执行了。
服务器配置的notify-keyspace-events
选项决定了服务器所发送通知的类型:
AKE:服务器可以发送所有类型的键空间通知和键事件通知。
AK:服务器可以发送所有类型的键空间通知。
AE:服务器可以发送所有类型的键事件通知。
二、Redis服务端
服务器redisServer
Redis服务器负责与多个客户端建立网络连接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来维持服务器自身的运转。
首先介绍一下Redis服务器的状态结构。Redis使用一个类型为“redisServer”的数据结构来保存整个Redis服务器的状态:
struct redisServer {
int dbnum;//服务器的数据库数量,值由服务器配置的“databases”选项决定,默认为16
redisDb *db;//数组,保存着服务器中的所有数据库
list *clients;//一个链表,保存了所有客户端状态,每个链表元素都是“redisClient”结构
time_t unixtime;//保存秒级精度的系统当前UNIX时间戳,减少获取系统当前时间的系统调用次数,100毫秒更新一次
long long mstime;//保存毫秒级精度的系统当前UNIX时间戳
unsigned lruclock;//默认每10秒更新一次,用于计算数据库键的空转时长,数据库键的空转时长 = 服务器的“lruclock”属性值 - 数据库键值对象的“lru”属性值
long long ops_sec_last_sample_time;//上一次进行服务器每秒执行命令数量抽样的时间
long long ops_sec_last_sample_ops;//上一次进行服务器每秒执行命令数量抽样时,服务器已执行命令的数量
long long ops_sec_samples[REDIS_OPS_SEC_SAMPLE];//环形数组,每个元素记录一次服务器每秒执行命令数量抽样结果,估算服务器在最近一秒钟处理的命令请求数量(数组长度默认为16,100毫秒更新一次)
int ops_sec_idx;//ops_sec_samples数组的索引值,每次抽样后值增1,等于16时重置为0
size_t stat_peak_memory;//已使用内存峰值
int shutdown_asap;//关闭服务器的标识,1表示关闭,0不关闭
pid_t rdb_child_pid;//记录执行BGSAVE命令的子进程的ID,-1表示服务器没有正在执行BGSAVE
pid_t aof_child_pid;//记录执行BGREWRITEAOF命令的子进程的ID,-1表示服务器没有正在执行BGREWRITEAOF
int aof_rewrite_scheduled;//1表示有BGREWRITEAOF命令被延迟了(服务器执行BGSAVE期间收到的BGREWRITEAOF会被延迟到BGSAVE执行完成之后执行)
struct saveparam *saveparams;//记录了自动保存条件的数组(执行BGSAVE的条件)
long long dirty;//修改计数器(上一次执行BGSAVE之后已经产生了多少修改)
time_t lastsave;//上一次执行自动保存操作(BGSAVE)的时间
sds aof_buf;//AOF缓冲区
int cronloops;//serverCron函数的运行次数计数器
lua;//用于执行Lua脚本的Lua环境
redisClient *lua_client;//Lua脚本的伪客户端,在服务器运行的整个生命周期一直存在,直至服务器关闭才会关闭
dict *lua_scripts;//字典,记录所有载入的Lua脚本,键为某个Lua脚本的SHA1校验和,值为对应的Lua脚本
dict *repl_scriptcache_dict;//字典,记录已经传播给所有从服务器的所有Lua脚本,键为脚本的SHA1校验和,值为NULL,用于EVALSHA1命令的复制
long long slowlog_entry_id;//下一条慢查询日志的ID
list *slowlog;//保存了所有慢查询日志的链表
long long slowlog_log_slower_than;//服务器配置“slowlog-log-slower-than”选项的值,表示查询慢于多少微秒便记录慢查询日志
unsigned long slowlog_max_len;//服务器配置“slowlog-max-len”选项的值,表示服务器最多保存多少条慢查询日志记录,若超出,最久的记录会被覆盖
monitors;//链表,监视器客户端列表
dict *pubsub_channels;//字典,保存所有频道的订阅关系,键为某个被订阅的频道,值为链表,记录了所有订阅这个频道的客户端
list *pubsub_patterns;//链表,保存所有模式的订阅关系,每个链表节点都包含了订阅的客户端和被订阅的模式
};
服务器初始化步骤
Redis
服务器从启动到能接受客户端的命令请求,需要经过一系列初始化和设置过程:比如初始化服务器状态,接受用户指定的服务器配置,创建相应的数据结构和网络连接等。详细的步骤:
-
(1)初始化服务器状态结构initServerConfig
创建一个struct redisServer类型的实例变量作为服务器的状态,并为结构中的各个属性设置默认值,例如:服务器的运行ID、默认配置文件路径、默认端口等等,同时创建Redis命令表。
-
(2)载入配置选项initServerConfig
载入用户指定的配置参数和配置文件,并根据用户设定的配置,对服务器状态变量的相关属性进行修改。
-
(3)初始化服务器数据结构initServer【核心】
这一步主要是为服务器状态中的一些数据结构分配内存,例如:
<1>“clients“:链表,保存所有与服务器连接的客户端的状态结构。
<2>”db“:字典保存服务器的所有数据库。
<3>”pubsub_channels“:字典,保存频道订阅信息。
<4>“pubsub_patterns”:链表,保存模式订阅信息。
<5>”lua“:用于执行Lua脚本的Lua环境。
<6>”slowlog“:用于保存慢查询日志。除此之外,还会进行一些非常重要的设置操作,例如:
<1>为服务器设置进程信号处理器。
<2>创建共享对象,例如经常经常用到的“OK”回复字符串对象,1到10000的字符串对象等等。
<3>为serverCron函数创建时间事件。
<4>如果AOF持久化功能已经打开,则打开现有的AOF文件,若AOF文件不存在,则创建并打开一个新的AOF文件,为AOF写入做好准备。
<5>初始化服务器的后台I/O模块,为将来的I/O操作做好准备。 -
(4)还原数据库状态
若服务器启用了AOF持久化功能,则载入AOF文件,否则载入RDB文件,根据AOF文件或RDB文件记录的内容还原数据库状态,同时在日志文件中打印出载入文件并还原数据库状态所耗费的时长。
-
(5)执行事件循环
一切准备就绪,开始执行服务器的事件循环,开始接受客户端的连接请求,处理客户端发送的命令请求。
命令请求的执行过程
一个命令从发送到获得回复的过程中,客户端和服务器需要完成一系列的操作,eg:从客户端发送SET KEY VALUE命令到获得回复期间,客户端和服务器共需要执行以下操作:
1、客户端向服务器发送命令请求SET KEY VALUE。
2、服务器接收并处理客户端发来的命令请求SET KEY VALUE。
3、服务器在数据库中进行命令执行操作,并产生命令回复OK。
4、服务器将命令回复OK发送给客户端。
5、客户端接收服务器返回的命令回复OK,并将其打印给用户观看。
1、发送命令请求
当用户向客户端键入命令时,客户端会将这个命令转为协议格式(resp协议,redis客户端和服务器交互的协议 ),然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器。
2、读取命令请求
当客户端与服务器之间的连接套接字因为客户端的命令输入而变得可读时,服务器将会调用命令请求处理器来执行下面的操作:
1、读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面。
2、对输入缓冲区的命令请求进行解析,提取出命令请求中包含的命令参数、命令参数的个数,并将其分别保存到客户端状态的argv属性和argc属性。
3、调用命令执行器,执行命令。
3、命令执行器【核心】
-
1、查找命令实现
命令执行器要做的第一件事就是根据客户端状态的
argv[0]
参数(在上面的例子中,这个参数是SET
),在命令表(command table
)中查找参数指定的命令,并将找到的命令保存到客户端状态的cmd
属性里面(如果没有找到,会将cmd
属性指向NULL
) 。命令表是一个字典,字典的键是一个个命令名字,比如
set、get、del
等等;而字典的值则是一个个redisCommand
结构,每个redisCommand
结构记录了一个Redis
命令的实现信息。在命令表中查找使用的是大小写无关的算法,所以只要命令的名字是正确的,就能找到相应的
redisCommand
结构。这也就是Redis
客户端的命令输入对大小写不敏感的原因。 -
2、执行预备操作
检查客户端状态的
cmd
属性是否指向NULL
;
检查客户端cmd
属性指向的redisCommand
结构参数是否正确;
检查客户端是否通过身份验证;
如果服务器打开了maxmemory
功能,那么执行命令之前,先检查服务器的内存占用情况,并在有需要的时候进行内存回收,从而使得接下来的命令可以顺利执行;
如果服务器上一次执行BGSAVE时出错,并且服务器打开了stop-writes-on-bgsave-error
功能,而且服务器将要执行的命令是一个写命令,那么服务器将会拒绝执行这个命令并向客户端返回一个错误;
如果客户端正在执行事务,那么服务器只会执行客户端发来的EXEC、DISCARD、MULTI、WATCH命令,其他命令都会被放进事务队列中;
如果服务器打开了监视器功能,那么服务器会将要执行的命令和参数等信息发送给监视器。
等等。当完成了以上的预备操作之后,服务器就可以开始真正执行命令。
-
3、调用命令的实现函数
通过指针调用命令实现函数执行指定的操作,并产生相应的命令回复,这些命令回复被保存在客户端的输出缓冲区中,之后实现函数为客户端的套接字关联命令回复处理器,用于将命令回复返回给客户端。
-
4、执行后续操作
如果服务器开启了慢查询日志功能,那么慢查询日志会判断是否需要为刚才执行的命令添加一条新的慢查询日志;
根据命令的执行时长,更新被执行命令的redisCommand
结构的milliseconds
属性,并将redisCommand
结构的calls
计数器的值+1;
如果服务器开启了AOF持久化功能,那么AOF持久化模块会将刚才执行的命令请求写入到AOF缓冲区;
如果有其他缓冲区正在复制当前这个服务器,那么服务器就会将刚刚执行的命令传播给所有从服务器。
4、服务端将命令回复给客户端
前面提及的命令实现函数会将命令回复保存到客户端的输出缓冲区里面,并为客户端的套接字关联命令回复处理器,当客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区的命令回复发送给客户端。
当命令回复发送完毕之后,回复处理器会清空客户端的输出缓冲区,为处理下一个命令请求做好准备。
5、客户端接收并打印命令回复
当客户端接收到协议格式的命令回复之后,会将这些回复转换成人类可读的格式,并打印给用户看。
serverCron函数【核心】
持续运行的Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行,这些定期操作就由周期性事件serverCron来负责执行。
周期性事件serverCron会每隔一段时间执行一次,直到服务器关闭为止。
serverCron默认每隔100毫秒执行一次,可以通过Redis配置中的hz
选项来设置serverCron的每秒回执行次数。
serverCron函数主要负责执行的有以下操作:
- (1)更新服务器时间缓存
- (2)更新LRU时钟
- (3)更新服务器每秒执行命令次数
- (4)更新服务器内存峰值记录
- (5)处理SIGTERM信号
- (6)管理客户端资源【调用clientsCron函数】
- (7)管理数据库资源【调用databaseCron函数】
- (8)检查持久化操作运行状态、标识与运行条件
-
(9)将AOF缓冲区的内容写入AOF文件
-
(10)增加cronloops计数器的值
如下:
struct redisServer{
//保存了秒级精度的系统当前UNIX时间戳
time_t unixtime;
//保存了毫秒级精度的系统当前UNIX时间戳
long long mstime;
//默认每10秒更新一次的时钟缓存
//用于计算键的空转时长
unsigned lrulock:22;
//已使用的内存峰值
size_t stat_peak_memory;
//打印日志
redisLogFromHandler(REDIS_WARNING,"Received SIGTERM,scheduleing shutdown...");
//打开关闭标识 值为1,关闭服务器 值为0,不做动作
server.shutdown_asap=1;
//如果值为1,那么表示有BGREWRITEAOF命令被延迟执行了
int aof_rewrite_scheduled;
//记录执行BGSAVE命令的子进程ID
//如果服务器没有执行BGSAVE命令,那么这个属性的值为-1
pid_t rdb_child_pid;
//记录执行BGREWRITEAOF命令的子进程的id
//如果服务器没有在执行BGREWRITEAOF,那么这个属性的值为-1
pid_t aof_child_pid;
//serverCron函数的运行次数计数器
//serverCron函数每执行一次,这个属性的值就增一
int cronloops;
};
typedef struct redisObject{
//...
unsigned lru:22;
//...
}robj;
三、Redis客户端
Redis服务器是典型的一对多程序:一个服务器可以与多个客户端建立网络连接,每个客户端可以向服务器发送命令请求,而服务器则接收并处理客户端发送的命令请求,并向客户端返回命令回复。
通过使用由IO多路复用技术实现的文件事件处理器,Redis服务器使用单线程进程的方式来处理命令请求,并与多个客户端进行网络通信。
客户端redisClient
Redis服务器状态结构中的“clients”属性是一个链表,保存了所有连接到当前服务器的客户端状态,每个客户端状态使用类型为“redisClient”的数据结构进行表示(每个属性按照即将讲解的顺序进行排序):
//Redis客户端的状态结构
struct redisClient {
redisDb *db;//记录客户端当前正在使用的数据库
int fd;//客户端正在使用的套接字描述符,-1表示伪客户端(AOF文件或者Lua脚本),大于-1表示普通客户端
robj *name;//客户端名字
int flags;//客户端标志,记录了客户端的角色,以及客户端目前所处的状态
sds querybuf;//输入缓冲区,根据输入内容动态地缩小或扩大,但不能超过1GB,否则服务器将关闭这个客户端
robj **argv;//命令与命令参数,数组,每个元素都是一个字符串对象,argv[0]为命令,其余元素为参数
int argc;//argv数组的长度
struct redisCommand *cmd;//当前执行的命令的实现函数,指向命令表中的命令结构
char buf[REDIS_REPLY_CHUNK_BYTES];//固定大小输出缓冲区,数组,默认大小为16KB
int bufpos;//buf数组目前已使用的字节数量
list *reply;//可变大小输出缓冲区,链表
obuf_soft_limit_reached_time:记录了“reply”输出缓冲区第一次到达软性限制的时间,用于计算持续超出软性限制的时长,以此决定是否关闭客户端
int authenticated;//0表示未通过身份验证,1表示已通过身份验证
time_t ctime:创建客户端的时间,可用于计算客户端与服务器连接的时间长度
time_t lastinteraction:客户端与服务器最后一次进行互动的时间,可用于客户端的空转时长
multiState mstate;//事务状态,包含一个事务队列,以及一个已入列命令计数器
};
- “db”:
是一个指针,指向Redis服务器状态结构中的“db”数组其中一个元素,表示当前客户端正在使用的数据库。
默认情况下,Redis客户端的目标数据库为0号数据库,可以通过select命令切换,所以select命令的实现原理为:修改redisClient.db指针,让它指向服务器中指定的数据库。
示例图:
- “fd”:
连接当前客户端与Redis服务器的套接字描述符。值为-1表示伪客户端(AOF文件或者Lua脚本),值大于-1则表示普通客户端。
Redis客户端分为普通客户端与伪客户端两种类型,其中通过网络连接与Redis服务器进行连接的就是普通客户端,反之则是伪客户端了。伪客户端也有两种类型,分别是Lua脚本的伪客户端和AOF文件的伪客户端。Redis服务器状态结构的“lua_client”属性就保存了Lua脚本的伪客户端,它会在Redis服务器初始化时就被创建,负责执行Lua脚本中包含的Redis命令,在服务器运行的整个生命周期一直存在,直至服务器关闭才会关闭。而AOF伪客户端则是在载入AOF文件时被创建,用于执行AOF文件中的Redis命令,在AOF文件载入完成之后被关闭。
client list:列出目前所有连接到服务器的普通客户端。 - “name”:
当前客户端名字。
client setname:为客户端设置一个名字。 - “flags”:
客户端标志,记录了客户端的角色,以及客户端目前所处的状态。例如:REDIS_MASTER表示当前客户端是一个主服务器;REDIS_BLOCKED表示当前客户端正在被列表命令阻塞。它的值可以是单个标志,也可以是多个标志的二进制或。 - “querybuf”:
输入缓冲区,存储客户端输入的内容,可以根据输入内容动态地缩小或扩大,但不能超过1GB,否则服务器将关闭这个客户端。 - “argv” & “argc”:
这两个属性的值都是由输入缓冲区的内容分析得来的。其中“argv”属性是一个数组,数组的每个元素都是一个字符串对象,argv[0]为客户端当前执行的命令,其余元素为传给该命令的参数。而“argc”属性则记录了“argv”数组的长度。 - “cmd”:
当前执行的命令的实现函数,指向命令表中的命令结构。
Redis服务器中保存着一个由字典实现的命令表,服务器会根据agrv[0]的值(不区分字母大小写),在命令表中查找命令对应的命令实现函数,然后将“cmd”指针指向这个函数。
命令表示例图:
redisCommand结构:保存了命令的实现函数、命令的标志、命令应该给定的参数个数,命令的总执行次数和总消耗时长等统计信息。
- “buf” & “bufpos”:
“buf”属性是一个数组,作为固定大小的输出缓冲区,默认大小为16KB,用于保存长度比较小的回复(服务器给客户端的回复)。
“bufpos”属性则记录了“buf”目前已经使用的字节数。 - “reply”:
链表,可变大小的输出缓冲区,用于保存长度比较大的回复。
当“buf”数组的空间已用完,或者因为回复太大而没办法放进“buf”数组时,服务器就会开始使用可变大小缓冲区。
但可变大小的缓冲区也是有限制的,分为硬性限制与软性限制两种模式,一旦超过硬性限制服务器会立刻关闭客户端,若是超过软性限制,客户端不会立刻被关闭,但若是持续一段时间一直超过软性限制,服务器也是会关闭客户端的。这两种限制可以使用Redis配置的“client-output-buffer-limit”选项来进行配置:
client-output-buffer-limit
配置示例(以下分别为普通客户端、从服务器客户端、执行发布与订阅功能的客户端设置不同的软性限制与硬性限制):
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60 - “obuf_soft_limit_reached_time”:
记录了“reply”输出缓冲区第一次到达软性限制的时间,用于计算持续超出软性限制的时长,以此决定是否关闭客户端。 - “authenticated”:
身份验证的标识,值为0表示未通过身份验证,1则表示已通过身份验证。 - “ctime”:
创建客户端的时间,可用于计算客户端与服务器连接的时间长度。
client list:“age”域记录了客户端与服务器连接的时间长度。 - “lastinteraction”:
客户端与服务器最后一次进行互动的时间,可用于客户端的空转时长。
client list:“idle”域记录了客户端的空转时长。
客户端的创建与关闭
- 创建
如果客户端是通过网络连接与服务器进行连接的普通客户端,那么客户端使用connect函数连接到服务器时,服务器就会调用连接事件处理器,为客户端创建相应的客户端状态,并将这个新的客户端状态添加到服务器状态结构clients链表的末尾。
- 关闭
一个普通客户端可以因为多种原因而被关闭:
1、如果客户端进程退出或者被杀死,那么客户端与服务器之间的网络连接将被关闭,从而造成客户端被关闭。
2、如果客户端向服务器发送了带有不符合协议格式的命令请求,那么这个客户端也会被服务器关闭。
3、如果客户端成为了CLIENT KILL命令的目标,那么它也会被关闭。
4、如果用户为服务器设置了timeout配置选项,那么当客户端的空转时间超过timeout选项设置的值时,客户端将被关闭。
5、如果客户端发送的命令请求的大小超过了输入缓冲区的限制大小(默认为1GB),那么这个客户端会被服务器关闭。
6、如果要发送给客户端的命令回复的大小超过了输出缓存区的限制大小,那么这个客户端会被服务器关闭。
- Redis 连接被强制关闭通常有以下几个原因:
1、服务器的网络问题:Redis 服务器和客户端之间的网络连接出现问题,可能由于网络不稳定或者服务器出现了故障。
2、Redis 配置问题:Redis 的配置文件中可能存在一些问题,比如超时时间设置不合理,或者 Redis 实例的最大连接数已经被耗尽。
3、客户端的问题:客户端可能存在一些问题,比如连接池中的连接数被错误地关闭或者由于长时间没有使用而超时关闭。
4、Redis 服务端的问题:Redis 服务器本身可能存在一些问题,比如由于过多的客户端连接而导致内存耗尽,或者由于负载过高而导致性能下降。
伪客户端
redis客户端除了普通客户端,还有两个伪客户端:lua客户端和aof客户端。
- lua客户端
服务器在初始化时创建负责执行Lua脚本的伪客户端,lua_clients伪客户端在服务器运行的整个生命周期会一直存在,只有服务器被关闭时,这个客户端才会被关闭。
- aof客户端
服务器在载入AOF文件时,会创建用于执行AOF文件的伪客户端,并在载入完成之后,关闭这个伪客户端。
四、事件
Redis服务器是一个事件驱动程序(event loop),需要处理两类事件:文件事件与时间事件。
文件事件
Redis服务器通过套接字与客户端或其他Redis服务器进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端或其他服务器的通信会产生相应的文件事件,而服务器通过监听并处理这些事件来完成一系列网络通信操作。
Redis基于Reactor模式开发了自己的网络事件处理器——文件事件处理器,文件事件处理器使用I/O多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。当被监听的套接字准备好执行连接应答、读取、写入、关闭等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前已关联好的事件处理器来处理这些事件。
Redis的I/O多路复用程序会在编译时自动选择系统中性能最高的I/O多路复用函数【evport、epoll、kqueue、select】来作为Redis的I/O多路复用程序的底层实现。
文件事件处理器的构成:
(其中I/O多路复用程序通过队列向文件事件分派器传送套接字)
文件事件处理器
-
两个事件:
读事件:当套接字变得可读时(客户端对套接字执行connect操作/write操作/close操作),套接字产生AE_READABLE操作;
写事件:当套接字变得可写时(客户端对套接字执行read操作),套接字产生AE_WRITEABLE操作。 -
三个应答处理器:
连接应答处理器——用于对连接服务器监听套接字的客户端进行应答。
命令请求处理器——负责从套接字读入客户端发送的命令请求内容。
命令回复处理器——负责将服务器执行命令后得到的命令回复通过套接字返回给客户端。
下面通过一张图来看客户端与 Redis 的一次通信过程:
下面来大致说一下这个图:
1、客户端 Socket01 向 Redis 的 Server Socket 请求建立连接,此时 Server Socket 会产生一个 AE_READABLE 事件,I/O 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列 中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个 能与客户端通信的 Socket01,并将该 Socket01 的AE_READABLE 事件与命令请求处理器关联。
2、假设此时客户端发送了一个 set key value 请求,此时 Redis 中的 Socket01 会产生 AE_READABLE 事件,IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面 Socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 Socket01 的 set key value 并在自己内存中完成 set key value 的设置。操作完成后,它会将 Socket01 的 AE_WRITABLE 事件与令回复处理器关联。
3、如果此时客户端准备好接收返回结果了,那么 Redis 中的 Socket01 会产生一个AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 Socket01 输入本次操作的一个结果,比如 ok ,之后解除 Socket01 的AE_WRITABLE 事件与命令回复处理器的关联。
这样便完成了一次通信。
时间事件
Redis服务器中的一些操作需要在给定时间点执行,时间事件就是服务器对这类定时操作的抽象。
-
1. 时间事件的分类
(1)定时事件:让一段程序在指定的时间之后执行一次。
(2)周期性事件:让一段程序每隔指定的时间就执行一次。 -
2. 时间事件的属性
(1)id:Redis服务器为每个时间事件创建的全局唯一ID,从小到大递增。
(2)when:毫秒精度的UNIX时间戳,记录了时间事件的到达时间。
(3)timeProc:时间事件处理器,一个时间事件到达时就会被服务器调用的函数。 -
3. 时间事件的实现
服务器将所有时间事件都放在一个无序链表中,新加入的时间事件总是插入到链表的表头中。每当时间事件执行器运行时,它就遍历整个链表,查找所有已经到达的时间事件,并调用相应的事件处理器。
正常模式下Redis服务器只使用serverCron一个时间事件,而在benchmark模式下,服务器也只使用两个时间事件。所以时间事件无序链表几乎退化成一个指针,使用它来保存时间事件并不影响事件执行的性能。
事件的调度与执行
因为服务器中同时有两种事件:文件事件和时间事件,所以服务器必须对这两种事件进行调度,决定何时应该处理文件事件,何时应该处理时间事件,以及花多少时间来处理它们等。
事件的调度与执行(ae.c/aeProcessEvents函数)规则:
(1)获取到达时间与当前时间最接近的时间事件。(避免频繁轮询时间事件)
(2)如果时间事件一直没有到达,则服务器等待并处理【阻塞】随机发生的文件事件。
(3)若有文件事件产生,则处理文件事件。若处理完文件事件,有时间事件到达则处理时间事件。
(4)若获取的时间事件的到达时间已到,则执行时间事件,完成之后重新从步骤一开始新一轮的事件循环。
Redis服务器对文件事件和时间事件的处理都是同步、有序、原子地执行的。因为时间事件在文件事件之后执行,并且事件之间不会出现抢占,所以时间事件的实际处理时间,通常会比时间事件设定的到达时间稍晚一些。
五、RDB持久化
RDB文件的创建
- save命令
save
命令会直接在主线程生成RDB
文件,如果写入RDB
的时间太长,就会阻塞主线程。 此时其他客户端的请求就会被阻塞, 在生产环境下,一定要慎用该操作!
当SAVE命令执行时,Redis服务器会被阻塞,所以当SAVE命令正在执行时,客户端发送的所有命令请求都会被拒绝。只有在服务器执行玩SAVE命令、重新开始接受命令请求之后,客户端发送的命令才会被处理。
- bgsave命令
bgsave
命令会fork
一个子进程来生成RDB
文件,这样主线程可以继续执行其他客户端到来的命令。当子进程任务完成后,它会退出。
虽然bgsave
不影响Redis服务器继续处理其它客户端命令请求。但是,在BGSAVE命令执行期间,服务器处理SAVE、BGSAVE、BGREWRITEAOF三个命令的方式会和平时有所不同。
首先,在BGSAVE命令执行期间,客户端发送的SAVE命令会被服务器拒绝,服务器禁止SAVE命令和BGSAVE命令同时执行是为了避免父进程和子进程同事执行两个rdbSave调用,放置产生竞争条件。
其次,在BGSAVE命令执行期间,客户端发送的BGSAVE命令会被服务器拒绝,因为同时执行两个BGSAVE命令也会产生竞争条件。
最后BGREWRITEAOF和BGSAVE两个命令不能同时执行:
如果BGSAVE命令正在执行,那么客户端发送的BGREWRITEAOF命令会被延迟到BGSAVE命令执行完毕之后执行。
如果BGREWRITEAOF命令正在执行,那么客户端发送的BGSAVE命令会被服务器拒绝。
因为BGREWRITEAOF和BGSAVE两个命令的实际工作都由子进程执行,所以这两个命令在操作方面并没有什么冲突的地方,不能同时执行它们只是一个性能方面的考虑并发出两个子进程,并且这两个子进程都同时执行大量的磁盘写入操作,这怎么想都不会是一个好主意。
-
RDB文件创建时机
除了手动执行与自动保存间隔会触发,还有以下场景也会触发:
1、主从复制时,从库全量复制同步主库数据,主库会执行bgsave
。
2、执行flushall
命令清空服务器数据,生产环境慎用。
3、执行shutdown
命令关闭Redis
时,会执行save
。 -
RDB文件产生过程
1、生成临时 rdb 文件,并写入数据;
2、完成数据写入后,让临时文件覆盖原rdb文件;
3、删除原有rdb文件。
RDB文件的载入
RDB文件的载入工作是在服务器启动时自动执行的,所以Redis并没有用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件。
另外值得一提的是,因为AOF文件的更新频率通常比RDB文件的更新频率高,所以:
如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态。
只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。
服务器在载入RDB文件期间,会一直处在阻塞状态,直到载入工作完成为止。
RDB相关参数
struct redisServer{
//...
// 设置保存条件【即自动间隔性保存参数】
struct saveparam *saveparams;
// 修改计数器
long long dirty;
// 设上次保存时间
time_t lastsave;
//...
}
-
saveparam 保存条件。用户可以通过save选项设置多个保存条件。
Redis的 serverCron 程序会周期性的遍历检查saveparam数组,只要其中任意一个条件被满足,服务器就会执行BGSAVE命令。
# 900秒内至少用一个key被修改,则执行bgsave,第二个是300秒内至少10次修改则触发,第三条类似。注意虽然指令是写的save,但其实执行的是bgsave。 save 900 1 save 300 10 save 60 10000 # 下面表示禁用RDB save "" # 是否压缩,建议不开启,压缩会更消耗CPU,磁盘不值钱 rdbcompression yes # RDB文件名称 dbfilename dump.rdb
-
dirty计数器记录距离上一次成功执行SAVE命令或者BGSAVE命令之后,服务器对数据库状态进行了多少次修改(写入、删除、更新) ; 当服务器成功执行一个数据库修改命令之后,程序就会对dirty计数器进行更新:命令修改了多少次数据库,dirty计数器的值就增加多少。
-
lastsave属性是一个UNIX时间戳,记录了服务器在上一次成功执行SAVE命令或者BGSAVE命令的时间。
RDB文件结构
RDB文件是一个经过压缩的二进制文件,它由多个部分组成。对于不同类型的键值对,RDB文件会采用不同的方式来保存它们。可以通过工具redis-check-dump检查工具来分析RDB文件。
-
REDIS(常量):RDB文件的最开头是REDIS部分,用于快速检查所载入的文件是否RDB文件。
-
db_version(变量):记录了RDB文件的版本号,比如"0006"就代表RDB文件的版本为第六版。
-
database(变量): 每个非空数据库在RDB文件中都可以保存为SELECTDB、db_number、pairs三个部分。
-
pairs:部分都保存了一个或以上数量的键值对,如果键值对带有过期时间的话,那么键值对的过期时间也会被保存在内。
-
不带过期时间的键值对结构由TYPE、key、value三部分组成。
-
带有过期时间的键值对在RDB文件中由EXPIRETIME_MS、ms、TYPE、key、value五部分组成。
-
type:记录了value的类型,即在redis中定义的数据类型。
-
key:key总是一个字符串对象。它的编码方式即和类型redis_rdb_type_string一样。
-
value:每个value部分都保存了一个值对象,即在redis中定义的redisObject,每个值对象的类型都由与之对应的TYPE记录,根据类型的不同,value部分的结构、长度也会有所不同。
-
EOF(常量):标志着RDB文件正文内容的结束。
-
check_sum(变量):保存着一个校验和。用于服务器在载入RDB文件时,会将载入数据所计算出的校验和与check_sum所记录的校验和进行对比,以此来检查RDB文件是否有出错或者损坏的情况出现。
六、AOF持久化
AOF持久化通过保存Redis服务器所执行的写命令来记录数据库状态的。
AOF持久化实现
两个缓冲区:
- aof_buf缓冲区:当AOF持久化功能打开时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
- aof重写缓冲区:为了解决AOF重写期间可能会导致的数据不一致,Redis服务器设置了AOF重写缓冲区。
1、命令追加
当AOF持久化功能打开时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
2、文件写入与同步
Redis的服务器进程就是一个时间循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。
在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面。flushAppendOnlyFile函数的行为由服务器配置的appendfsync选项的值来确定。默认设置为everysec。
开启appendonly配置后【默认关闭】,进行appendfsync配置:
- always :每个指令进入aof_buf缓冲区后都要同步写入到aof文件。性能最差,但最安全。
- everysec :每秒将aof_buf缓冲区中指令写入到aof文件。性能适中,可能会丢失一秒数据。
- no :让操作系统决定何时同步aof_buf缓冲区指令到aof文件。性能最好,但不可靠。
3、文件载入与数据还原
因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令。 流程如下:
AOF重写
因为AOF持久化是通过保存被执行的写命令来记录数据库状态的,所以随着服务器运行时间的流逝,AOF文件中的内容会越来越多,为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写功能。
AOF重写功能实现原理为:先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令。 因为aof重新生成的文件只包含还原当前数据库状态所必须的命令,所以新AOF文件不会浪费任何硬盘空间。
AOF后台重写
AOF重写程序是在子进程里执行【不会影响主进程执行新到来的命令,避免主线程阻塞】:
- 子进程进行AOF重写期间,服务器进程(父进程)可以继续处理命令请求。
- 子进程带有服务器进程的数据副本,使用子进程而不是线程,可以避免在使用锁的情况下,保证数据的安全性。
使用子进程也存在一个问题,在子进程进行AOF重写期间,服务器进程需要继续处理命令请求,而新的命令可能会对数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致。
为了解决这个问题,Redis服务器设置了AOF重写缓冲区,在子进程执行AOF重写期间,服务器进程执行以下三个工作:
1、执行客户端发来的命令。
2、将执行后的写命令追加到AOF缓冲区。
3、将执行后的写命令追加到AOF重写缓冲区。
即AOF缓冲区的内容会定期写入和同步到AOF文件,对现有AOF文件的处理工作会如常进行;从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面。
当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数(仅仅只有这里会对服务器进程造成阻塞,其它执行过程不会阻塞父进程,将AOF重写造成的性能影响降到了最低),并执行以下工作:
- 将AOF重写缓冲区中的所有内容写入到新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致
- 对新的AOF文件进行改名,原子地覆盖现有的AOF文件,完成新旧两个AOF文件的替换。
RDB 与 AOF 混用
在我们的Redis 4.0 后,提供了AOF与RDB的混用机制,也叫混合持久化。
开启混合持久化需要在config文化中手动开启。
# 改为 yes 开启混合持久化
aof-use-rdb-preamble yes
这样有什么好处呢?要知道,在子线程进行 bgsave时,主线程仍然会执行其他客户端发来的命令,这时子线程快照存储的是原本的内存数据,所以就会导致数据不一致,想修改只能等下一次bgsave的到来。
当开启混合持久化后,在AOF重写日志时,fork的子进程会先以RDB的方式写入到AOF文件中,在子进程工程时,主线程处理的命令会存储到重写缓冲区内,当子进程完成RDB方式读入后,会再以AOF方式把重写缓冲区的指令写入到AOF文件中。
这样操作后,AOF文件前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的 增量数据。
这样可以集成两者的优点,前半部分由于是RDB所以加载很快,后半部分由于是AOF,所以丢失的数据更少。