本章介绍:
9.1 服务器中的数据库
Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构体的db数组中,db数组的每个项都是一个redis.h/redisDb结构体,每个redisDb结构体代表一个数据库。在初始化服务器时,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库,dbnum属性由服务器配置的database选项决定,默认情况下,该选项的值为16。
struct redisServer {
……
//一个数组,保存着服务器中所有数据库
redisDb *db;
//服务器的数据库数量
int dbnum;
……
};
9.2 切换数据库
每个Redis客户端都有自己的目标数据库,每当客户端执行数据库写命令或者数据库读命令的时候,目标数据库就会成为这些命令的操作对象。默认情况下,Redis客户端的目标数据库为0号数据库,但客户端可以通过执行SELECT命令来切换目标数据库。
在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针:
typedef struct redisClient {
……
//记录客户端当前正在使用的数据库
redisDb *db;
……
} redisClient;
redisClient.db指针指向redisServer.db数组的其中一个元素,而被指向的元素就是客户端你的目标数据库。
通过修改redisClient.db指针,让它指向服务器中的不同数据库,从而实现切换目标数据库的功能,这就是SELECT命令的实现原理
谨慎处理多数据库:在操作数据库先执行select命令选择相应的数据库,避免切换数据库后忘记在哪个数据库(Redis没有返回当前数据库的函数)。
9.3 数据库键空间
Redis是一个键值对数据库服务器,服务器中的每个数据库都由redis.h/redisDb结构体表示,其中,redisDb结构体的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间。
typedef struct redisDb {
//数据库键空间,保存着数据库中所有键值对
dict *dict;
……
} redisDb;
键空间和用户所见的数据库是直接对应的:
举个例子
127.0.0.1:6379> SET message "hello world"
OK
127.0.0.1:6379> RPUSH alphabet "a" "b" "c"
(integer) 3
127.0.0.1:6379> HSET book name "Redis in Action"
(integer) 1
127.0.0.1:6379> HSET book author "Josiah L. Carlson"
(integer) 1
127.0.0.1:6379> HSET book publisher "Manning"
(integer) 1
读写键空间的维护操作
9.4 设置键的生存时间或过期时间
通过EXPIRE或PEXPIRE命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间(Time To Live,TTL),在经过指定的秒数或者毫秒数之后,服务器就会自动删除生存时间为0的键
127.0.0.1:6379> SET key value
OK
127.0.0.1:6379> EXPIRE key 5
(integer) 1
# 5秒之内
127.0.0.1:6379> GET key
"value"
# 5秒之后
127.0.0.1:6379> GET key
(nil)
客户端可以通过EXPIREAT命令和PEXPIREAT命令,以秒或者毫秒精度给数据库中已存在的某个键设置过期时间。过期时间是一个Unix时间戳,当键的过期时间来临时,服务器就会自动从数据库删除这个键。
TTL命令和PTTL命令接受个带有生存时间或过期时间的键,返回这个键的剩余生存时间,也就是,返回距离这个键被服务器自动删除还有多长时间
9.4.1 设置过期时间
实际上EXPIRE、PEXPIRE 、EXPIREAT三个命令都是使用PEXPIREAT 命令来实现的
EXPIRE命令可以转换成PEXPIRE命令伪码(看个过程)
def EXPIRE(key, ttl_in_sec):
#将TTL从秒转换成毫秒
ttl_in_ms = sec_to_ms(ttl_in_sec)
PEXPlRE(key, ttl_in_ms)
def PEXPIRE(key, ttl_in_ms):
#获取以毫秒计算的当前UNIX 时间戳
now_ms = get_current_unix_timestamp_in_ms()
#当前时间加上TTL,得出毫秒格式的键过期时间
PEXPlREAT(key, now_ms+ttl_in_ms)
def EXPIREAT (key, expire_time_in_ sec):
#将过期时间从秒转换为毫秒
expire_time_ in_ms = sec_to_ms (expire_time_in_sec)
PEXPlREAT(key, expire_time_in_ms)
redisDb结构体的expires字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典:
以下是PEXPIREAT命令的伪代码定义:
def PEXPIREAT(key, expire_time_in_ms) :
#如果给定的键不存在于键空间,那么不能设置过期时间
if key not in redisDB.dict :
return 0
#在过期字典中关联键和过期时间
redisDB.expires[key]=expire_tirne_in_ms
#过期时间设置成功
return 1
9.4.3 移除过期时间
PERSIST命令可以移除一个键的过期时间
127.0.0.1:6379> SET message "hello world"
OK
127.0.0.1:6379> PEXPIREAT message 1538531750000
(integer) 1
127.0.0.1:6379> TTL message
(integer) 302
127.0.0.1:6379> PERSIST message
(integer) 1
127.0.0.1:6379> TTL message
(integer) -1
PERSIST命令就是PEXPIREAT命令的反操作:PERSIST命令在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联。
PERSIST命令的伪代码:
def PERSIST(key):
#如果键不存在,或者键没有设置过期时间,那么直接返回
if key not in redisDB.expires:
return 0
#移除过期字典中给定键的键值对关联
redisDB.expires.remove(key)
#键的过期时间移除成功
return 1
9.4.4 计算并返回剩余生存时间
TTL命令以秒为单位返回键的剩余生存时间,而PTTL命令而以毫秒为单位返回键的剩余生存时间。
def PTTL(key):
#键不存在数据库
if key not in redisDb.dict:
return -2
#尝试取得键的过期时间
#如果键没有设置过期时间,那么expire_time_in_ms将为None
expire_time_in_ms = redisDb.expires.get(key)
#键没有设置过期时间
if expire_time_in_ms is None:
return -1
#获得当前时间
now_ms = get_current_unix_timestamp_in_ms()
#过期时间减去当前时间,得出的差就是键的剩余生存时间
return (expire_time_in_ms - now_ms)
def TTL(key):
#获取以毫秒为单位的剩余生存时间
ttl_in_ms = PTTL(key)
if ttl_in_ms < 0:
#处理返回值为-2和-1的情况
return ttl_in_ms
else:
#将毫秒转换成秒
return ms_to_sec(ttl_in_ms)
9.4.5 过期键的判定
通过过期字典,程序可以用以下步骤检查一个给定键是否过期:
def is_expired(key):
#取得键的过期时间
expire_time_in_ms = redisDb.expires.get(key)
#键没有设置过期时间
if expire_time_in_ms is None:
retrurn False
#取得当前时间的Unix时间戳
now_ms = get_current_unix_timestamp_in_ms()
#检查当前时间是否大于键的过期时间
if now_ms > expire_time_in_ms:
#键已过期
return True
else:
#键未过期
return False
9.5 过期键的删除策略
问题:如果一个键过期了,那么它什么时候会被删除呢?有三种策略
9.5.1 定时删除
现阶段来说并不现实:创建一个定时器需要用到Redis服务器中的时间事件,而当前时间事件的实现方式——无序链表,查找一个事件的时间复杂度为O(N)——并不能高效地处理大量时间事件。
9.5.2 惰性删除
在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远不会被删除(除非用户手动执行FLUSHDB),我们甚至可以将这种情况看作一种内存泄露——无用的垃圾数据占用了大量内存,而服务器却不会自己去释放它们,这对于运行状态非常依赖内存的Redis服务器来说,肯定不是一个好消息
9.5.3 定期删除
定期删除策略是两种策略的一种整合和折中:
定期删除策略的难点是确定删除操作执行的时长和效率!
9.6 Redis过期键删除策略
Redis服务器实际使用的是惰性删除和定期删除两种策略
9.6.1 惰性删除策略的实现
过期键的惰性删除策略由db.c/expireIfNeeded函数实现,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查:
9.6.2 定期删除策略的实现
过期键的定期删除策略由redis.c/activeExpireCycle函数实现,每当Redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键
activeExpireCycle函数的工作模式可以总结如下:
9.7 AOF、RDB和复制功能对过期键的处理
生成RDB文件:
在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。
载入RDB文件:
在启动Redis服务器时,如果服务器开启了RDB功能,那么服务器将对RDB文件进行载入:
AOF文件写入
当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或定期删除,那么AOF文件不会因为这个过期键而产生任何影响。当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加(append)一条DEL命令,来显示地记录该键已被删除。
AOF重写
和生成RDB文件类似,在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中
复制
9.8 数据库通知
数据库通知是Redis2.8版本新增加的功能,这个功能可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况
127.0.0.1:6379> config set notify-keyspace-events KEA
OK
127.0.0.1:6379> SUBSCRIBE __keyspace@0__:message
Reading messages... (press Ctrl-C to quit)
1) "subscribe" #订阅消息
2) "__keyspace@0__:message"
3) (integer) 1
1) "message" #执行SET命令
2) "__keyspace@0__:message"
3) "set"
1) "message" #执行EXPIRE命令
2) "__keyspace@0__:message"
3) "expire"
1) "message" #执行DEL命令
2) "__keyspace@0__:message"
3) "del"
根据发回的通知显示,先后共有SET、EXPIRE、DEL三个命令对message进行了操作
-键空间通知:某个键执行了什么命令”的通知称为(上边)
127.0.0.1:6379> config set notify-keyspace-events KEA
OK
127.0.0.1:6379> SUBSCRIBE __keyevent@0__:del
Reading messages... (press Ctrl-C to quit)
1) "subscribe" #订阅消息
2) "__keyevent@0__:del"
3) (integer) 1
1) "message" #键message执行了DE命令
2) "__keyevent@0__:del"
3) "message"
1) "message" #键numbers执行了DE命令
2) "__keyevent@0__:del"
3) "numbers"
1) "message" #键key执行了DE命令
2) "__keyevent@0__:del"
3) "key"
服务器配置的notify-keyspace-events选项决定了服务器所发送通知的类型:
9.8.1 发送通知
发送数据库通知的功能由notify.c/notifyKeyspaceEvent函数实现:
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid);
type参数是当前想要发送的通知的类型,程序会根据这个值来判断通知是否就是服务器配置notify-keyspace-events选项所选定的通知类型,从而决定是否发送通知。event、keys和dbid分别是事件的名称、产生事件的键,以及产生事件的数据库号码,函数会根据type参数以及三个参数来构建事件通知的内容,以及接收通知的频道名。
举例:
每当一个Redis命令需要发送数据库通知的时候,该命令的实现函数就会调用notifyKeyspaceEvent函数,并向函数传递该命令所引发的事件的相关信息
void saddCommand(client *c) {
……
//如果至少有一个元素被成功添加,那么执行以下程序
if (added) {
//发送事件通知
notifyKeyspaceEvent(NOTIFY_SET,"sadd",c->argv[1],c->db->id);
}
……
}
9.8.2 发送通知的实现
notifyKeyspaceEvent函数执行以下操作:
def notifyKeyspaceEvent(type,event,key,dbid):
#如果给定的通知不是服务器允许发送的通知,那么直接返回
if not(server.notify_keyspace_events & type) :
return
#发送键空间通知
if server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE:
#将通知发送给频道__ keyspace@__ :
#内容为键所发生的事件
#构建频道名字
chan="keyspace@{dbid}:{key}".format(dbid=dbid,key=key)
#发送通知
pubsubPublishMessage(chan,event)
#发送键事件通知
if server.notify_keyspace_events&REDIS_NOTIFY_KEYEVENT:
#将通知发送给频道_keyevent@_:
#内容为发生事件的键
#构建频道名字
chan="keyevent@{dbid}:{event}".format(dbid=dbid, event=event)
#发送通知
pubsubPublishMessage(chan, key)
另外pubsubPublishMessage函数时PUBLISH命令的实现函数,执行这个函数等同于执行PUBLISH命令,订阅数据库通知的客户端收到的信息就是由这个函数发出的
10.1 RDB文件的创建和载入
有两个Redis命令可以用于生成RDB文件,一个是SAVE,另一个是BGSAVE。
创建RDB文件的实际工作由rdb.c/rdbSave函数完成:
def SAVE():
#创建RDB文件
rdbSave()
def BGSAVE():
#创建子进程
pid = fork()
if pid == 0:
#子进程负责创建RDB文件
rdbSave()
#完成之后向父进程发出信号
signal_parent()
elif pid > 0:
#父进程继续处理命令请求,并通过轮询等待子进程信号
handle_request_and_wait_signal()
else:
#处理出错情况
handle_fork_error()
RDB文件的载入工作是在服务器启动时自动执行的,Redis并没有专门用于载入RDB文件的命令。
另外因为AOF文件的更新频率通常比RDB文件的更新频率高,所以:
载入RDB文件的实际工作由rdb.c/rdbLoad函数完成
SAVE命令执行时的服务器状态:
当SAVE命令执行时,Redis服务器会被阻塞,所以当SAVE命令正在执行时,客户端发送的所有命令请求都会被拒绝。只有在服务器完成SAVE命令、重新开始接受命令请求之后,客户端发送的命令才会被处理
BGSAVE命令执行时的服务器状态:
RDB文件载入时的服务器状态:
服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止
10.2 自动间隔性保存
Redis允许用户通过没设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。用户可以通过save选项设置多个保存条件,但只要其中一个条件被满足,服务器就会执行BGSAVE命令。举个栗子,如果我们向服务器提供以下配置:
save 900 1
save 300 10
save 60 10000
那么只要满足以下三个条件中的任意一个,BGSAVE命令就会被执行:
设置保存条件:
当Redis启动时,用户可以通过指定配置文件或传入启动参数的方式设置save选项,如果用户没有主动设置save选项,那么服务器会为save选项设置如上默认条件。
接着,服务器会根据save选项所设置的保存条件,设置服务器状态redisServer结构体的saveparams属性:
struct redisServer {
……
//记录了保存条件的数组
struct saveparam *saveparams;
……
};
struct saveparam {
//秒数
time_t seconds;
//修改数
int changes;
};
dirty计数器和lastsave属性
struct redisServer {
……
//修改计数器
long long dirty;
……
//上一次执行保存的时间
time_t lastsave;
……
};
检查保存条件是否满足
Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否满足,如果满足,就执行BGSAVE命令
def serverCron():
# …
# 遍历所有保存条件
for saveparam in server.saveparams:
# 计算距离上次执行保存操作有多少秒
save_interval = unixtime_now()-server.lastsave
# 如果数据库状态的修改次数超过条件所设置的次数
# 并且距离上次保存的时间超过条件所设置的时间
# 那么执行保存操作
if server.dirty >= saveparam.changes and \
save_interval > saveparam.seconds:
BGSAVE();
# ...
10.3 RDB文件结构
RDB文件组成部分:
REDIS | db_version | databases | EOF | check_sum |
---|
ps:
详述:
10.3.1 database部分
一个RDB文件的databases部分可以保存任意多个非空数据库。例如,如果服务器的0号数据库和3号数据库非空
REDIS | db_version | databases[0] | databases[3] | EOF | check_sum |
---|
每个非空数据库在RDB文件中都可以保存为SELECTDB、db_number、key_value_pairs三个部分
SELECTDB | db_number | key_value_pairs |
---|
一个完成的RDB文件,文件包含了0号数据库和3号数据:
REDIS | db_version | SELECTDB | 0 | key_value_pairs | SELECTDB | 3 | key_value_pairs | EOF | check_sum |
---|
10.3.2 key_value_pairs部分
RDB文件中的每个key_value_pairs部分都保存了一个以上的键值对,如果键值对带过期时间的话,那么键值对的过期时间也会被保存在内。不带过期时间的键值对在RDB文件中由TYPE、key、value三部分组成
TYPE | key | value |
---|
带有过期时间的键值对在RDB文件中的结构:
EXPIRETIME_MS | ms | TYPE | key | value |
---|
TYPE记录了value的类型,长度为1字节,值可以是以下常量的其中一个:
每个TYPE常量都代表了一种对象类型或底层编码,当服务器读入RDB文件中的键值对数据时,程序会根据TYPE的值来决定如何读入和解释value的数据。key和value分别保存了键值对的键对象和值对象:
EXPIRETIME_MS常量的长度为1字节,它告知读入程序,接下来要读入的将是一个以毫秒为单位的过期时间
ms是一个8字节长的带符号整数,记录着一个以毫秒为单位的Unix时间戳,这个时间戳就是键值对的过期时间
10.3.3 value编码
RDB文件中的每个中的每个value部分都保存了一个值对象,每个值对象的类型都由与之对应的TYPE记录,根据类型的不同,value部分的结构,长度也会有所不同。
字符串对象
TYPE的值REDIS_RDB_TYPE_STRING,value保存的就是一个字符串对象,字符串对象的编码可以是REDIS_ENCODING_INT或REDIS_ENCODING_RAW。字符串对象的为REDIS_ENCODING_INT,说明对象中保存的是长度不超过32位的整数
ENCODING | integer |
---|
ENCODING的值可以是REDIS_ENCODING_INT8、REDIS_ENCODING_INT16或者REDIS_ENCODING_INT32三个常量的其中一个,它们分别代表RDB文件使用8位(bit)、16位、32位来保存整数值integer
字符串对象的编码为REDIS_ENCODING_RAW,那么说明对象所保存的是一个字符串值,根据字符串长度的不同,有压缩和不压缩两种方法来保存这个字符串:
没有被压缩的字符串:
len | string |
---|
对于压缩后的字符串:
RDDIS_RDB_ENC_LZF | compressed_len | origin_len | compressed_string |
---|
RDDIS_RDB_ENC_LZF常量标识着字符串已经被LZF算法压缩过,读入程序在碰到这个常量时,会根据之后的compressed_len、origin_len和compressed_string三部分,对字符串进行解压缩:其中compressed_len记录的是字符串被压缩后的长度,而origin_len记录的是字符串原来的长度,compressed_string记录的是被压缩后的字符串
列表对象
TYPE的值为REDIS_RDB_TYPE_LIST,那么value保存的就是一个REDIS_ENCODING_LINKEDLIST(双端链表)编码的列表对象
list_length | item1 | item2 | … | itemN |
---|
list_length记录了列表的长度,它记录列表保存了多少个项(item),读入程序可以通过这个长度知道自己应该读入多少个列表项。图中以item开头的部分代表列表的项,因为每个列表项都是一个字符串对象,所以程序会以处理字符串对象的方式来保存和读入列表项
集合对象
如果TYPE的值为REDIS_RDB_TYPE_SET,那么value保存的就是一个REDIS_ENCODING_HT(字典)编码的集合对象
set_size | elem1 | elem2 | … | elemN |
---|
set_size是集合的大小,它记录了集合保存了多少个元素,读入程序可以通过这个大小知道自己应该读入多少个集合元素。图中以elem开头的部分代表集合的元素,因为每个集合元素都是一个字符串对象,所以程序会以处理字符串对象的方式来保存和读入集合元素
哈希表对象
TYPE的值为REDIS_RDB_TYPE_HASH,那么value保存的就是一个REDIS_ENCODING_HT编码的集合对象
hash_size | key_value_pair1 | key_value_pair2 | … | key_value_pairN |
---|
有序集合对象
TYPE的值为REDIS_RDB_TYPE_ZSET,那么value保存的就是一个REDIS_ENCODING_SKIPLIST(字典和有序表)编码的有序集合对象
sorted_set_size | elem1 | elem2 | … | elemN |
---|
sorted_set_size记录了有序集合的大小,也即是这个有序集合保存了多少个元素,读入程序需要根据这个值来决定应该读入多少个有序集合元素。以element开头的部分代表有序集合中的元素,每个元素又分为成员(member)和分值(score)两部分,成员是一个字符串对象,分值则是一个double类型的浮点数,程序在保存RDB文件时会先将分值转换成字符串对象,然后再用保存字符串对象的方法将分值保存起来
INTSET编码的集合
TYPE的值为REDIS_RDB_TYPE_SET_INTSET,那么value保存的就是一个整数集合对象,RDB文件保存这种对象的方法是,先将整数集合转换成字符串对象,然后将这个字符串对象保存到RDB文件里面。如果程序在读入RDB文件的过程中,碰到由整数集合对象转换成的字符串对象,那么程序会根据TYPE值的指示,先读入字符串对象,再将这个字符串对象转换成原来的整数集合对象
ZIPLIST编码的列表、哈希表或有序集合
如果TYPE的值为REDIS_RDB_TYPE_LIST_ZIPLIST、REDIS_RDB_TYPE_HASH_ZIPLIST或REDIS_RDB_TYPE_ZSET_ZIPLIST,那么value保存的就是一个压缩列表对象,RDB文件保存这种对象的方法是:
如果程序在读入RDB文件的过程中,碰到由压缩列表对象转换成的字符串对象,那么程序会根据TYPE值的指示,执行以下操作:
10.4 分析RDB文件
od命令来分析Redis服务器产生的RDB文件,该命令可以用给定的格式转存(dump)并输入文件。比如说,给定-c参数可以以ASCII编码的方式打印输入文件,给定-x参数可以以十六进制的方式打印输出文件
不包含任何键值对的RDB文件
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> SAVE
OK
# od -c dump.rdb
0000000 R E D I S 0 0 0 6 377 334 263 C 360 Z 334
0000020 362 V
0000022
开头的是"REDIS"字符串,之后的0006是版本号,再之后的一个字节377代表EOF常量,最后的334 263 C 360 Z 334 362 V八字节则代表RDB文件的校验和
包含字符串键的RDB文件
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> SET MSG "HELLO"
OK
127.0.0.1:6379> SAVE
OK
# od -c dump.rdb
0000000 R E D I S 0 0 0 6 376 \0 \0 003 M S G
0000020 005 H E L L O 377 207 z = 304 f T L 343
0000037
根据之前学习的数据库结构知识,当一个数据库被保存到RDB文件时,这个数据库将由以下三部分组成:
一个一字节长的特殊值SELECTDB
RDB文件的最开始仍然是REDIS和版本号0006,之后出现的376代表SELECTDB常量,再之后的\0代表整数0,表示被保存的数据库为0号数据库。在数据库号码之后,直到代表EOF常量的377为止,RDB文件包含有以下内容:
\0 003 M S G 005 H E L L O
在RDB文件中,没有过期时间的键值对由类型(TYPE)、键(key)、值(value)三部分组成:其中类型的长度为一字节,键和值都是字符串对象,并且字符串在未被压缩前,都是以字符串长度为前缀,后跟字符串内容本身的方式来储存的。根据这些特征,我们可以确定\0就是字符串类型的TYPE值REDIS_RDB_TYPE_STRING(这个常量的实际值为整数0),之后的003是键MSG的长度值,再之后的005则是值HELLO的长度
包含带有过期时间的字符串键的RDB文件
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> SETEX MSG 10086 "HELLO"
OK
127.0.0.1:6379> SAVE
OK
# od -c dump.rdb
0000000 R E D I S 0 0 0 6 376 \0 374 \ 2 365 336
0000020 @ 001 \0 \0 \0 003 M S G 005 H E L L O 377
0000040 212 231 x 247 252 } 021 306
0000050
一个带有过期时间的键值对将由以下部分组成:
据此:
包含一个集合键的RDB文件
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> SADD LANG "C" "JAVA" "RUBY"
(integer) 3
127.0.0.1:6379> SAVE
OK
# od -c dump.rdb
0000000 R E D I S 0 0 0 6 376 \0 002 004 L A N
0000020 G 003 004 R U B Y 004 J A V A 001 C 377 202
0000040 312 r 352 346 305 * 023
0000047
关于分析RDB文件的说明
Redis本身带有RDB文件检查工具redis-check-dump,网上也能找到很多处理RDB文件的工具,所以人工分析RDB文件的内容并不是学习Redis所必须掌握的技能。
前面我们一直用od命令配合-c参数来打印RDB文件,因为使用ASCII编码打印RDB文件可以很容易地发现文件中的字符串内容。但是,对于RDB文件中的数字值,比如校验和来说,通过ASCII编码来打印它并不容易看出它的真实值,更好的办法是使用-cx参数调用od命令,同时以ASCII编码和十六进制格式打印RDB文件:
# od -cx dump.rdb
0000000 R E D I S 0 0 0 6 377 334 263 C 360 Z 334
4552 4944 3053 3030 ff36 b3dc f043 dc5a
0000020 362 V
56f2
0000022
RDB文件的校验和为0x 56f2 dc5a f043 b3dc(校验和以小端方式保存)
Redis还提供了AOF(Append Only File)持久化功能,与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的命令来记录数据库状态
举个栗子
127.0.0.1:6379> SET msg "hello"
OK
127.0.0.1:6379> SADD fruits "apple" "banana" "cherry"
(integer) 3
127.0.0.1:6379> RPUSH numbers 128 256 512
(integer) 3
RDB持久化保存数据库状态的方法是将msg、fruits、numbers三个键的键值对保存到RDB文件中,而AOF持久化保存数据库状态的方法是将服务器执行的SET、SADD、RPUSH三个命令保存到AOF文件中。被写入AOF文件的所有命令都是以Redis的命令请求协议格式保存的,因为Redis的命令请求协议是纯文本格式,所以我们可以直接打开一个AOF文件,观察里面的内容。例如,对于之前执行的三个写命令来说,服务器将产生包含以下内容的AOF文件:
*2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n
*3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$5\r\nhello\r\n
*5\r\n$4\r\nSADD\r\n$6\r\nfruits\r\n$5\r\napple\r\n$6\r\nbanana\r\n$6\r\ncherry\r\n
*5\r\n$5\r\nRPUSH\r\n$7\r\nnumbers\r\n$3\r\n128\r\n$3\r\n256\r\n$3\r\n512\r\n
在这个AOF文件里面,除了用于指定数据库的SELECT命令是服务器自动添加的之外,其他都是我们之前通过客户端发送的命令。服务器在启动时,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态
11.1 AOF持久化的实现
AOF持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤(往下读就知道啥是写入啥是同步别急)
命令追加
当AOF持久化功能处于打开状态时,服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾
struct redisServer {
……
//AOF缓冲区
sds aof_buf;
……
};
AOF文件的写入与同步
Redis的服务器进程就是一个事件循环,这个循环中的文件事件(啥是文件事件见下章)负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件(同下章)则负责执行像serverCron函数这样需要定时运行的函数。因为服务器在处理文件事件时可能会执行写命令,使得一些内容被追加到aof_buf缓冲区里面,所以在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面,这个过程可以用以下伪代码表示:
def eventLoop():
while True:
#处理文件时间,接受命令请求以及发送命令回复
#处理命令请求时可能会有新内容被追加到aof_buf缓冲区中
processFileEvents()
#处理时间时间
processTimeEvents()
#考虑是否要将aof_buf中的内容写入和保存到AOF文件里面
flushAppendOnlyFile()
flushAppendOnlyFile函数的行为由服务器配置的appendsync选项的值来决定
appendfsync选项的值 | flushAppendOnlyFile函数的行为 |
---|---|
always | 将aof_buf缓冲区中的所有内容写入并同步到AOF文件 |
everysec | 将aof_buf缓冲区中的所有内容写入到AOF文件,如果上次同步AOF文件的时间距离现在超过一秒钟,那么再次对AOF文件进行同步,并且这个同步操作是由一个线程专门负责执行的 |
no | 将aof_buf缓冲区中的所有内容写入到AOF文件,但并不对AOF文件进行同步,何时同步由操作系统来决定 |
如果用户没有主动为appendsync选项设置值,那么appendsync选项的默认值为everysec
文件的写入和同步: 为了提高文件的写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入到文件的时候,操作系统通常会将写入数据暂时保存在一个内存缓冲里面,等到缓冲区的空间被填满、或者超过了指定的时限之后,才真正地将缓冲区中的数据写入到磁盘中。这种做法虽然提高了效率,但也为写入数据带来安全问题,如果计算机发生停机,那么保存在内存缓冲的数据也会丢失。为此,系统提供了fsync和fdatasync两个同步函数,它们可以强制让操作系统立即将缓冲区中的数据写入到磁盘中,从而确保写入数据的安全性
服务器配置appendfsync选项的值直接决定AOF持久化功能的效率和安全性:
AOF文件的载入与数据还原
服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态。Redis读取AOF文件并还原数据库状态的详细步骤如下:
一直执行步骤2和3,直到AOF文件中的所有写命令都被处理完毕为止
AOF重写
为啥重写:对一个键操作半天又改又删实在麻烦,只需要记录最后的状态即可避免这些冗余操作,提高效率
AOF文件重写的实现
虽然Redis将生成新AOF文件替换旧AOF文件的功能命名为“AOF文件重写”,但实际上,AOF文件重写并不需要对现有的AOF文件进行任何读取、分析或者写入操作,这个功能是通过读取服务器当前的数据库状态来实现的
整个重写过程可以用以下伪代码表示
def aof_rewrite(new_aof_file_name):
# 创建新的AOF文件
f = create_file(new_aof_file_name);
# 遍历数据库
for db in redisServer.db:
# 忽略空数据库
if db.is_empty() : continue;
# 写入SELECT命令,指定数据库号码
f.write_command("SELECT"+db.id);
# 遍历数据库中所有键
for key in db:
# 忽略已经过期的键
if key.is_expired():continue;
# 根据键的类型对键进行重写
if key.type == String:
rewrite_string(key);
else if key.type == List:
rewrite_list(key);
else if key.type == Hash:
rewrite_hash(key);
else if key.type == Set:
rewrite_set(key);
else if key.type == SortedSet:
rewrite_sorted_set(key);
# 如果键带有过期时间,那么过期时间也要被重写
if key.have_expire_time():
rewrite_expire_time(key);
# 写入完毕,关闭文件
f.close();
def rewrite_string(key):
# 使用GET命令获取字符串键的值
value = GET(key);
# 使用SET命令重写字符串键
f.write_command(SET, key, value);
def rewrite_list(key):
# 使用LRANGE命令获取列表键的所有元素
item1, item2,..., itemN = LANGE(key, 0, -1);
# 使用RPUSH命令重写列表键
f.write_command(RPUSH, key, item1, item2,..., itemN);
def rewrite_hash(key):
# 使用HGETALL命令获取哈希键包含的所有键值对
field1, value1, field2, value2,..., fieldN, valueN = HGETALL(key);
# 使用HMSET命令重写哈希键键
f.write_command(HMSET, key, field1, value1, field2, value2,..., fieldN, valueN);
def rewrite_set(key):
# 使用SMEMBERS命令获取集合键的所有元素
elem1, elem2,..., elemN = SMEMBERS(key);
# 使用SADD命令重写集合键
f.write_command(SADD, key, elem1, elem2,..., elemN);
def rewrite_sorted_set(key):
# 使用ZRANGE命令获取有序集合键的所有元素
member1, score1,member2, score2,...,memberN, scoreN = ZRANGE(key, 0, -1, "WITHSCORES");
# 使用ZADD命令重写有序集合键
f.write_command(ZADD, key, member1, score1,member2, score2,...,memberN, scoreN);
def rewrite_expire_time(key):
# 获取毫秒精度的键过期时间戳
timestamp = get_expire_time_in_unixstamp(key);
# 使用PEXPIREAT命令重写键的过期时间
f.write_command(PEXPIREAT, key, timestamp);
因为aof_rewrite函数生成的新AOF文件只包含还原当前数据库状态所必须的命令,所以新AOF不会浪费任何硬盘空间
为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,如果元素的数量超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那么重写程序将使用多条命令来记录键的值,而不单单使用一条命令
在目前版本中,REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值为64,这也就是说,如果一个集合键包含了超过64个元素,那么重写程序会用多条SADD命令来记录这个集合,并且每条命令设里的元素数量也为64个。
AOF后台重写
AOF重写程序aof_rewrite函数可以很好地完成创建一个新AOF文件的任务但是,因为这个函数会进行大量的写入操作,所以调用这个函数的线程将被长时间阻塞,因为Redis服务器使用单线程来处理命令请求,所以如果由服务器直接调用aof_rewrite函数的话,那么在重写AOF文件期间,服务器将无法处理客户端发送的命令请求
Redis决定将AOF重写程序放到子进程里执行,使子进程进行AOF重写期间,服务器进程(父进程)可以继续处理命令请求,子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下,保证数据的安全性。(进程是分配资源的基本单位,线程共享资源)
使用子进程也有一个问题需要解决,因为子进程在进行AOF重写期间,服务器进程还需要处理命令请求,而新的命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的AOF文件所保存的数据库状态不一致
为了解决这种数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区。
在子进程执行AOF重写期间,服务器进程需要执行以下三个工作:
这样一来可以保证:
当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,就会调用一个信号处理函数,并执行以下工作:
这个信号处理函数执行完毕后,父进程就可以继续像往常一样接受命令请求了
在整个AOF后台重写过程中,只有信号处理函数执行时会对服务器进程(父进程)造成阻塞,在其他时候,AOF后台重写都不会阻塞父进程,这将AOF重写对服务器性能造成的影响降到了最低。
Redis是一个事件驱动程序,服务器需要处理以下两类事件
12.1 文件事件
Redis基于Reactor模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器:
文件事件处理器以单线程方式运行,通过使用I/O多路复用程序监听多个套接字,文件事件处理器实现了高性能的网络通信模型,同时也可以和Redis服务器中其他同样以单线程方式运行的模块进行对接,保持了Redis内部单线程的简单性
12.1.1 文件事件处理器的构成
文件事件处理器的四个组成部分,它们分别是套接字、I/O多路复用程序、文件事件分派器,以及事件处理器
文件事件是对套接字操作的抽象,每当一个套接字准备好执行连接应答(accept)、写入、读取、关闭等操作时,就会产生一个文件事件。因为一个服务器通常会连接多个套接字,所以多个文件事件有可能会并发地出现。I/O多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字
尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生事件的套接字都放到一个队列中,然后通过这个队列,以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的实践被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕),I/O多路复用程序才会继续向文件事件分派器传送下一个套接字
文件事件分派器接收I/O多路复用程序传来的套接字,并根据套接字产生的事件的类型,调用相应的事件处理器。服务器会为执行不同任务的套接字关联不同的事件处理器,这些处理器是一个个函数,它们定义了某个事件发生时,服务器应该执行的动作
I/O多路复用程序的实现
Redis的I/O多路复用程序的所有功能都是通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数库来实现的,每个I/O多路复用函数库在Redis源码中都对应一个单独的文件,比如ae_select.c、ae_epoll.c、ae_kqueue.c,诸如此类。因为Redis为每个I/O多路复用函数库都实现了相同的API,所以I/O多路复用程序的底层实现时可以互换的
Redis在I/O多路复用程序的实现源码中用#include宏定义了相应的规则,程序会在编译时自动选择系统中性能最高的I/O多路复用函数库来作为Redis的I/O多路复用程序的底层实现
事件的类型
I/O多路复用程序可以监听多个套接字的ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件,这两类事件和套接字操作之间的对应关系如下:
I/O多路复用程序允许服务器同时监听套接字的AE_READABLE事件和AE_WRITABLE事件,如果一个套接字同时产生了这两种事件,那么文件分派器会优先处理AE_READABLE事件之后,再处理AE_WRITABLE事件。也就是说,如果一个套接字可读又可写的话,那么服务器会先读取套接字,后写套接字
API
ae.c/aeCreateFileEvent函数接受一个套接字描述符、一个事件类型、以及一个事件处理器作为参数,将给定套接字的事件加入到I/O多路复用程序的监听范围内,并对事件和事件处理器进行关联
ae.c/aeDeleteFileEvent函数接受一个套接字描述符和一个监听事件类型作为参数,让I/O多路复用程序取消对给定套接字的给定事件的监听,并取消事件和事件处理器之间的关联
ae.c/aeGetFileEvents函数接受一个套接字描述符,返回该套接字正在被监听的事件类型:
如果套接字没有任何事件被监听,那么函数返回AE_NONE
如果套接字的读事件正在被监听,那么函数返回AE_READABLE
如果套接字的写事件正在被监听,那么函数返回AE_WRITABLE
如果套接字的读事件和写事件正在被监听,那么函数返回AE_READABLE|AE_WRITABLE
ae.c/aeWait函数接受一个套接字描述符、一个事件类型和一个毫秒数为参数,在给定的时间内阻塞并等待套接字的给定类型事件产生,当事件成功产生,或者等待超时之后,函数返回
ae.c/aeApiPoll函数接受一个sys/time.h/struct timeval结构为参数,并在指定的时间內,阻塞并等待所有被aeCreateFileEvent函数设置为监听状态的套接字产生文件事件,当有至少一个事件产生,或者等待超时后,函数返回
ae.c/aeProcessEvents函数是文件事件分派器,它先调用aeApiPoll函数来等待事件产生,然后遍历所有已产生的事件,并调用相应的事件处理器来处理这些事件
ae.c/aeGetApiName函数返回I/O多路复用程序底层所使用的I/O多路复用函数库的名称:返回"select"表示底层为select函数库,诸如此类
文件事件的处理器
Redis为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通信需求
连接应答处理器
networking.c/acceptTcpHandler函数是Redis的连接应答处理器,这个处理器用于对连接服务器监听套接字的客户端进行应答,具体实现为为sys/socket.h/accept函数的包装。当Redis服务器进行初始化的时候,程序会将这个连接应答处理器和服务监听套接字的AE_READABLE事件关联起来,当有客户端用sys/socket.h/connect函数连接服务端监听套接字时,套接字就会产生AE_READABLE事件,引发连接应答处理器执行,并执行相应的套接字应答操作
命令请求处理器
networking.c/readQueryFromClient函数是Redis的命令请求处理器,这个处理器负责从套接字中读入客户端发送的命令请求内容,具体实现为unistd.h/read函数的包装。当一个客户端通过连接应答处理器成功连接到服务器之后,服务器会将客户端套接字的AE_READABLE事件和命令请求处理器关联起来,当客户端向服务器发送命令请求的时候,套接字就会产生AE_READABLE事件,引发命令请求处理器执行,并执行相应的套接字读入操作
命令回复处理器
networking.c/sendReplyToClient函数是Redis的命令回复处理器,这个处理器负责将服务器执行命令后得到的命令回复通过套接字返回给客户端,具体实现为unistd.h/write函数的包装。当服务器有命令回复需要传送给客户端的时候,服务器会将客户端套接字的AE_WRITABLE事件和命令回复处理器关联起来,当客户端准备好接收服务器传回的命令回复时,就会产生AE_WRITABLE事件,引发命令回复处理器执行,并执行相应的套接字写入操作,如图1-6所示。当命令回复发送完毕之后,服务器就会解除命令回复处理器与客户端套接字的AE_WRITABLE事件之间的关联
假设一个Redis服务器正在运作,那么这个服务器的监听套接字的AE_READABLE事件应该正处于监听状态下,而该事件所对应的处理器为连接应答处理器。如果这时有一个Redis客户端向服务器发起连接,那么监听套接字将产生AE_READABLE事件,触发连接应答处理器执行。处理器会对客户端的连接请求进行应答,然后创建客户端套接字,以及客户端状态,并向客户端套接字的AE_READABLE事件与命令请求处理器进行关联,使得客户端可以向主服务器发送命令请求
之后,假设客户端向主服务器发送一个命令请求,那么客户端套接字将产生AE_READABLE事件,引发命令请求处理器执行,处理器读取客户端命令内容,然后传给相关程序去执行。执行命令将产生相应的命令回复,为了将这些命令回复传送给客户端,服务器会将客户端套接字的AE_WRITABLE事件与命令回复处理器进行关联。当客户端尝试读取命令回复时,客户端套接字将产生AE_WRITABLE事件,触发命令回复处理器执行,当命令回复处理器将命令回复全部写入到套接字之后,服务器就会解除客户端套接字的AE_WRITABLE事件与命令回复处理器之间的关联
Redis的时间事件分为以下两类:
一个时间事件主要由以下三个属性组成:
一个时间事件是定时事件还是周期性事件取决于时间事件处理器的返回值:
实现
服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已达的时间事件,并调用相应的事件处理器。下图展示了一个保存时间事件的链表的例子,链表中包含了三个不同的时间事件:因为新的时间事件总是插入到链表的表头,所以三个时间事件分别按ID逆序排序,表头事件的ID为3,中间事件的ID为2,表尾事件的ID为1
我们说保存时间事件的链表为无序链表,指的不是链表不按ID排序,而是说,该链表不按when属性的大小排序。正因为链表没有按when属性进行排序,所以当时间事件执行器运行的时候,它必须遍历链表中的所有时间事件,这样才能确保服务器中所有已达到时间事件都会被处理
API
def processTimeEvents():
#遍历服务器中的所有时间事件
for time_event in all_time_event():
#检查事件是否已经到达
if time_event.when <= unix_ts_now():
#事件已到达
#执行事件处理器,并获取返回值
retval = time_event.timeProc()
#如果这是一个定时事件
if retval == AE_NOMORE:
#那么将该事件从服务器中删除
delete_time_event_from_server(time_event)
#如果这是一个周期性事件
else:
#那么按照事件处理器的返回值更新时间事件的when属性
#让这个事件在指定的时间之后再次到达
update_when(time_event, retval)
时间事件应用实例:serverCron函数
持续运行的Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行,这些定期操作由redis.c/serverCron函数负责执行,它的主要工作包括:
Redis服务器以周期性事件的方式来运行serverCron函数,在服务器运行期间,每隔一段时间, serverCron就会执行一次,直到服务器关闭为止。在Redis2.6版本,服务器默认规定serverCron每秒运行10次,平均每间隔100毫秒运行一次。从Redis2.8开始,用户可以通过修改hz选项来调整serverCron的每秒执行次数
事件的调度与执行
因为服务器同时存在文件事件和时间事件两种事件类型,所以服务器必须对两种事件进行调度,决定何时处理文件事件,何时处理时间事件,以及花多少时间来处理它们等等。事件的调度和执行由ae.c/aeProcessEvents函数负责。
def aeProcessEvents():
#获取到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTimer()
#计算最接近的时间事件距离到达还有多少毫秒
remaind_ms = time_event.when - unix_ts_now()
#如果事件已到达,那么remaind_ms的值可能为负数,将它设定为0
if remaind_ms < 0:
remaind_ms = 0
#根据remaind_ms的值,创建timeval结构
timeval = create_timeval_with_ms(remaind_ms)
#阻塞并等待文件事件产生,最大阻塞时间由传入的timeval结构决定
#如果remaind_ms的值为0,那么aeApiPoll调用之后马上返回,不阻塞
aeApiPoll(timeval)
#处理所有已产生的文件事件(其实并没有这个函数)
processFileEvents()
#处理所有已到达的时间事件
processTimeEvents()
Redis服务器主函数
def main():
# 初始化服务器
init_server()
# 一直处理事件,知道服务器关闭为止
while server_is_not_shutdown():
aeProcessEvents()
# 服务器关闭,执行清理操作
clean_server()
Redis服务器是典型的一对多服务器程序,通过使用I/O多路复用技术实现的文件事件处理器,Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。
服务器都为这些客户端建立了相应的redis.h/redisClient结构(客户端状态),这个结构保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构,其中包括:
Redis服务器状态结构的clients属性是一个链表,这个链表保存了所有与服务器连接的客户端的状态结构,对客户端执行批量操作,或者查找某个指定的客户端,都可以通过遍历clients链表来完成:
struct redisServer {
……
//一个链表,保存了所有客户端状态
list *clients;
……
};
13.1 客户端属性
客户端状态包含的属性可以分为两类:
套接字描述符
客户端状态的fd属性记录了客户端正在使用的套接字描述符
typedef struct redisClient {
int fd;
……
} redisClient;
根据客户端类型不同,fd属性的值可以是-1或者是大于-1的整数:
执行CLIENT list命令可以列出目前所有连接到服务器的普通客户端,命令输出中fd域显示了服务器连接客户端所使用的套接字描述符:
127.0.0.1:6379> CLIENT list
id=3 addr=127.0.0.1:57522 fd=9 name= age=76021 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
名字
默认情况下,一个连接到服务器的客户端是没有名字的,如上name域是空白的,可以使用CLIENT SETNAME命令为客户端设置一个名字,如下:
127.0.0.1:6379> CLIENT SETNAME message_quque
OK
127.0.0.1:6379> CLIENT list
id=3 addr=127.0.0.1:57522 fd=9 name=message_quque age=76399 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
客户端的名字记录在客户端状态的name属性中:
typedef struct redisClient {
……
robj *name;
……
} redisClient;
如果客户端没有为自己设置名字,那么相应客户端状态的name属性指向NULL指针;相反地,如果客户端为自己设置了名字,那么name属性将指向一个字符串对象,而对象则存放着客户端的名字。
标志
客户端的标志属性flags记录了客户端的角色,以及客户端目前所处的状态
typedef struct redisClient {
……
int flags;
……
} redisClient;
flags属性的值可以是单个标志:flags = < flag >,也可以是多个标志的二进制:flags = < flags1 > | < flags2 > | ……
每个标志使用一个常量表示,一部分标志记录了客户端的角色:
在主从服务器进行复制操作时,主从服务器会成为从服务器的客户端,而从服务器也会成为主服务器的客户端。REDIS_MASTER标志表示客户端代表的一个主服务器,REDIS_SLAVE标志表示客户端代表的是一个从服务器
REDIS_PRE_PSYNC标志表示客户端代表的是一个版本低于Redis2.8的从服务器,主服务器不能使用PSYNC命令与这个从服务器同步。这个标志只能在REDIS_SLAVE标志处于打开状态时使用
REDIS_LUA_CLIENT标识表示客户端是专门用于处理Lua脚本里面包含的Redis命令的伪客户端
而另外一部分标志则记录了客户端目前所处的状态:
REDIS_MONITOR标志表示客户端正在执行MONITOR命令
REDIS_UNIX_SOCKET标志表示服务器使用UNIX套接字来连接客户端
REDIS_BLOCKED标志表示客户端正在被BRPOP、BLPOP等命令阻塞
REDIS_UNBLOCKED标志表示客户端已经从REDIS_BLOCKED标志所表示的阻塞状态中脱离出来,不再阻塞。REDIS_UNBLOCKED标志只能在REDIS_BLOCKED标志已经打开的情况下使用
REDIS_MULTI标志表示客户端正在执行事务
REDIS_DIRTY_CAS标志表示事务使用WATCH命令监视的数据库键已经被修改,REDIS_DIRTY_EXEC标志表示事务在命令入队时出现了错误,以上两个标志都表示事务的安全性已经被破坏,只要这两个标记中的任意一个被打开,EXEC命令必然会执行失败。这两个标志只能在客户端打开了REDIS_MULTI标志的情况下使用
REDIS_CLOSE_ASAP标志表示客户端的输出缓冲区大小超出了服务器允许的范围,服务器会在下一次执行serverCron函数时关闭这个客户端,以免服务器的稳定性受到这个客户端影响。积存在输出缓冲区中的所有内容会直接被释放,不会返回给客户端
REDIS_CLOSE_AFTER_REPLY标志表示有用户对这个客户端执行了CLIENT_KILL命令,或者客户端发送给服务器的命令请求中包含了错误的协议内容。服务器会将客户端积存在输出缓冲区中的所有内容发送给客户端,然后关闭客户端
REDIS_ASKING标志表示客户端向集群节点(运行在集群模式下的服务器)发送了ASKING命令
REDIS_FORCE_AOF标志强制服务器将当前执行的命令写入到AOF文件里面,REDIS_FORCE_REPL标志强制主服务器将当前执行的命令复制给所有从服务器。执行PUBSUB命令会使客户端打开REDIS_FORCE_AOF标志,执行SCRIPT_LOAD命令会使客户端打开REDIS_FORCE_AOF标志和REDIS_FORCE_REPL标志
在主从服务器进行命令传播期间,从服务器需要向主服务器发送REPLICATIONACK命令,在发送这个命令之前,从服务器必须打开主服务器对应的客户端的REDIS_MASTER_FORCE_REPLY标志,否则发送操作会被拒绝执行
PUBSUB命令和SCRIPT LOAD命令的特殊性
通常情况下,Redis只会将那些对数据库进行了修改的命令写入到AOF文件,并复制到各个从服务器。如果一个命令没有对数据库进行任何修改,那么它就会被认为是只读命令,这个命令不会被写入到AOF文件,也不会被复制到从服务器以上规则适用于绝大部分Redis命令,但PUBSUB命令和SCRIPT LOAD命令是其中的例外。PUBSUB命令虽然没有修改数据库,但PUBSUB命令向频道的所有订阅者发送消息这一行为带有副作用,接收到消息的所有客户端的状态都会因为这个命令而改变。因此,服务器需要使用REDIS_FORCE_AOF标志,强制将这个命令写入AOF文件,这样在将来载入AOF文件时,服务器就可以再次执行相同的PUBSUB命令,并产生相同的副作用。SCRIPT LOAD命令的情况与PUBSUB命令类似:虽然SCRIPT LOAD命令没有修改数据库,但它修改了服务器状态,所以它是一个带有副作用的命令,服务器需要使用REDIS_FORCE_AOF标志,强制将这个命令写入AOF文件,使得将来在载入AOF文件时,服务器可以产生相同的副作用
另外,为了让主服务器和从服务器都可以正确地载入SCRIPT LOAD命令指定的脚本,服务器需要使用REDIS_FORCE_REPL标志,强制将SCRIPT LOAD命令复制给所有从服务器
例子
# 客户端是一个主服务器
REDIS_MASTER
# 客户端正在被列表命令阻塞
REDIS_BLOCKED
# 客户端正在执行事务,但事务的安全性已被破坏
REDIS_MULTI | REDIS_DIRTY_CAS
# 客户端是一个从服务器,并且版本低于Redis 2.8
REDIS_SLAVE | REDIS_PRE_PSYNC
# 这是专门用于执行Lua脚本包含的Redis命令的伪客户端
# 它强制服务器将当前执行的命令写入AOF文件,并复制给从服务器
REDIS_LUA_CLIENT | REDIS_FORCE_AOF| REDIS_FORCE_REPL
输入缓冲区
客户端状态的输入缓冲区用于保存客户端发送的命令请求
typedef struct redisClient {
……
sds querybuf;
……
} redisClient;
如果客户端向服务器发送了以下命令请求:
SET key value
客户端状态的querybuf属性将是一个包含以下内容的SDS值:
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
命令与命令参数
在服务器将客户端发送的命令请求保存到客户端状态的querybuf属性之后,服务器将对命令请求的内容进行分析,并将得出的命令参数以及命令参数的个数分别保存到客户端状态的argv属性和argc属性:
typedef struct redisClient {
……
int argc;
robj **argv;
……
} redisClient;
argv属性是一个数组,数组中的每个项都是一个字符串对象,其中argv[0]是要执行的命令,而之后的其他项则是传给命令的参数。argc属性负责记录argv数组的长度
argc属性的值为3,而不是2,因为命令的名字"SET"本身也是一个参数
命令的实现函数
当服务器从协议内容中分析并得出argv属性和argc属性的值之后,服务器将根据项argv[0]的值,在命令表中查找命令所对应的命令实现函数。下图展示了一个命令表示例,该表是一个字典,字典的键是一个SDS结构体,保存了命令的名字,字典的值是命令所对应的redisCommand结构体,这个结构体保存了命令的实现函数、命令的标志、命令应该给定的参数个数、命令的总执行次数和总消耗时长等统计信息
当程序在命令表中成功找到argv[0]所对应的redisCommand结构体时,它会将客户端状态的cmd指针指向这个结构体:
typedef struct redisClient {
……
struct redisCommand *cmd;
……
} redisClient;
之后,服务器就可以使用cmd属性所指向的redisCommand结构体,以及argv、argc属性中保存的命令参数信息,调用命令实现函数,执行客户端指定的命令
针对命令表的查找操作不区分输入字母的大小写,所以无论argv[0]是"SET"、"set"或者是"SeT"等等,查找结果都是相同的
输出缓冲区
执行命令所得的命令回复会被保存在客户端状态的输出缓冲区里面,每个客户端都有两个输出缓冲区可用,一个缓冲区的大小是固定的,另一个缓冲区的大小是可变的
typedef struct redisClient {
……
int bufpos;
char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;
buf是一个大小为REDIS_REPLY_CHUNK_BYTES字节的字节数组,而bufpos属性则记录了buf数组目前已使用的字节数量。REDIS_REPLY_CHUNK_BYTES常量目前的默认值为16*1024,也即是说,buf数组的默认大小为16KB。
当buf数组的空间已经用完,或者回复因为太大而无法装进buf数组里面,服务器就会开始使用可变大小缓冲区。可变大小缓冲区由reply链表和一个或多个字符串对象组成:
typedef struct redisClient {
……
list *reply;
……
} redisClient;
通过使用链表来连接多个字符串对象,服务器可以为客户端保存一个非常长的命令回复,而不必受到固定大小缓冲区16KB大小的限制。图1-9展示了一个包含三个字符串对象的reply链表
身份验证
客户端状态的authenticated属性用于记录客户端是否通过了身份验证:
typedef struct redisClient {
……
int authenticated; /* when requirepass is non-NULL */
……
} redisClient;
如果authenticated的值为0,表示客户端尚未通过身份认证;如果authenticated的值为1,表示客户端已通过认认证。
当客户端authenticated属性的值为0时,除了AUTH命令之外,客户端发送的所有其他命令都会被服务器拒绝执行。authenticated属性仅在服务器启用了身份验证功能时使用,如果服务器没有启用身份验证功能的话,那么即使authenticated属性的值为0(默认值),服务器也不会拒绝未验证身份的客户端发送的命令请求。
时间
客户端还有几个和时间相关的属性:
typedef struct redisClient {
……
time_t ctime;
time_t lastinteraction;
time_t obuf_soft_limit_reached_time;
……
} redisClient;
ctime属性记录了创建客户端的时间,这个时间可以用来计算客户端与服务器已经连接了多少秒,CLIENT list命令的age域记录了这个秒数
127.0.0.1:6379> CLIENT list
…… age=87315 ……
…… age=535 ……
lastinteraction属性记录了客户端与服务器最后一次进行互动(interaction)的时间,这里的互动可以是客户端向服务器发送命令请求,也可以是服务器向客户端发送命令回复。lastinteraction属性可以用来计算客户端的空转时间,也即是,距离客户端与服务器最后一次进行互动以来,已经过去多少秒,CLIENT list命令的idle域记录了这个秒数
127.0.0.1:6379> CLIENT list
…… idle=10916 ……
…… idle=0 ……
obuf_soft_limit_reached_time属性记录了输出缓冲区第一次达到软性限制(soft time)的时间
13.2 客户端的创建与关闭
创建普通客户端
如果客户端是通过网络连接和服务器进行连接的普通客户端,那么在客户端使用connect函数连接到服务端时,服务器就会调用连接事件处理器,为客户端创建相应的客户端状态,并将这个客户端状态添加到服务器状态结构体中clients链表的末尾
关闭普通客户端
可变大小缓冲区由一个链表和任意多个字符串对象组成,理论上来说,这个缓冲区可以保存任意长度的命令回复。但为了避免客户端回复过大,占用过多服务器资源,服务器会时刻检查客户端的输出缓冲区的大小,并在缓冲区的大小超出范围时,执行相应的限制操作。服务器使用两种模式来限制客户端输出缓冲的大小:
使用client-output-buffer-limit 可以为普通客户端、从服务器客户端、执行发布与订阅功能的客户端分别设置不同的软性限制和硬性限制,格式为:
client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
//example
client-output-buffer-limit normal 0 0 0 #不限制输出缓冲区大小
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60 #执行发布和订阅功能的客户端硬性限制为32mb,软性限制为8mb,软性限制时长为60秒
Lua脚本的伪客户端
服务器会在初始化时创建负责执行Lua脚本中包含的Redis命令的伪客户端,并将这个伪客户端关联在服务器结构的lua_client属性中:
struct redisServer {
……
redisClient *lua_client;
……
};
lua_client伪客户端在服务器运行的整个生命周期中会一直存在,只有服务器被关闭时,这个客户端才会被关闭
AOF文件的伪客户端
服务器在载入AOF文件时,会创建用于执行AOF文件包含的Redis命令的伪客户端,并在载入完成之后,关闭这个伪客户端
14.1 命令请求过程
客户端发送SET KEY VALUE命令到获得回复OK期间,客户端和服务端共需要执行以下操作:
发送命令请求
Redis服务器的命令请求来自Redis客户端,当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器
假设用户在客户端键入命令:
SET KEY VALUE
客户端会将这个命令转换成协议:
*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
读取命令请求
客户端与服务器之间的连接套接字因为客户端的写入而变得可读时,服务器将调用命令请求处理器来执行以下操作:
命令执行器要做的第一件事就是根据客户端状态的argv[0]参数,在命令表中查找参数所指定的命令,并将找到的命令保存到客户端状态的cmd属性中。命令表是一个字典,字典的键是一个个命令名字,比如"set"、“get”、"del"等等;而字典的值则是一个个redisCommand结构体,每个redisCommand结构体记录了一个Redis命令的实现信息,结构体的各个主要属性的类型和作用:
属性名 | 类型 | 作用 |
---|---|---|
name | redisCommandProc * | 函数指针,指向命令的实现函数,比如setCommand。redisCommandProc类型的定义为typedef void redisCommandProc(redisClient *c) |
arity | int | 命令参数的个数,用于检查命令请求的格式是否正确。如果这个值为负数-N,那么表示参数的数量大于等于N。注意命令的名字本身也是一个参数,比如说SET msg “hello world 命令的参数是"SET”、“msg”、“hello world”,而不仅仅是"msg"和"hello world" |
sflags | char * | 字符串形式的标识值,这个值记录了命令的属性,比如这个命令是写命令还是读命令, 这个命令是否允许在载入数据时使用,这个命令是否允许在 Lua 脚本中使用,等等 |
flags | int | 对sflags标识进行分析得出的二进制标识,由程序自动生成。服务器对命令标识进行检查时使用的都是flags属性而不是 sflags属性,因为对二进制标识的检查可以方便地通过&、^、~等操作来完成 |
calls | long long | 服务器总共执行了多少次这个命令 |
milliseconds | long long | 服务器执行这个命令所耗费的总时长 |
标识 | 意义 | 带有这个标识的命令 |
---|---|---|
w | 这是一个写入命令,可能会修改数据库 | SET、RPUSH、DEL等等 |
r | 这是一个只读命令,不会修改数据库 | GET、STRLEN、EXISTS,等等 |
m | 这个命令可能会占用大量内存, 执行之前需要先检查服务器的内存使用情况,如果内存紧缺的话就禁止执行这个命令 | SET、APPEND、RPUSH、LPUSH、SADD、SINTERSTORE,等等 |
a | 这是一个管理命令 | SAVE、BGSAVE、SHUTDOWN ,等等 |
p | 这是一个发布与订阅功能方面的命令 | PUBLISH、SUBSCRIBE、PUBSUB,等等 |
s | 这个命令不可以在Lua脚本中使用 | BRPOP、BLPOP、BRPOPLPUSH、SPOP,等等 |
R | 这是一个随机命令,对于相同的数据集和相同的参数,命令返回的结果可能不同 | SPOP、SRANDMEMBER、SSCAN、RANDOMKEY,等等 |
S | 当在Lua脚本中使用这个命令时,对这个命令的输出结果进行一次排序,使得命令的结果有序 | SINTER、SUNION、SDIFF、SMEMBERS、KEYS,等等 |
l | 这个命令可以在服务器载入数据的过程中使用 | INFO、SHUTDOWN、PUBLISH,等等 |
t | 这是一个允许从服务器在带有过期数据时使用的命令 | SLAVEOF、PING、INFO,等等 |
M | 这个命令在监视器(monitor)模式下不会自动被传播(propagate) | EXEC |
以SET命令和GET命令作为例子,展示了redisCommand结构体:
客户端状态的cmd指针会指向这个redisCommand结构体:
命令执行器(2):执行预备操作
服务器已经将执行命令所需的命令实现函数(保存在客户点状态的cmd属性)、参数(保存在客户端状态的argv属性)、参数个数(保存在客户端状态的argc属性)都收集齐了,但在真正执行命令前,程序还需要进行一些预备操作,从而确保命令可以正确、顺利地被执行,这些操作包括:
命令执行器(3):调用命令的实现函数
服务器已经将要被执行的命令保存到客户端状态的cmd属性中,并将命令的参数和参数个数分别保存到客户端状态argv属性和argc 属性,当服务器决定要执行命令时,它只要执行以下语句就可以了
// client 是指向客户端状态的指针
client->cmd->proc(client);
因为执行命令所需的实际参数都已经保存到客户端状态的argv属性里面了,所以命令的实现函数只需要一个指向客户端状态的指针作为参数即可。继续以之前的SET命令为例子,图:对于这个例子来说, 执行语句:client->cmd->proc(client);等于执行语句:setCommand(client)
被调用的命令实现函数会执行指定的操作,并产生相应的命令回复,这些回复会被保存在客户端状态的输出缓冲中(buf属性和reply属性),之后实现函数还会为客户端的套接字关联命令回复处理器,这个处理器将命令回复返回给客户端
对于前面SET命令的例子来说,函数调用setCommand(client)将产生一个"+OK\r\n"回复,这个回复会保存到客户端状态的buf属性中,如图:
命令执行器(4):执行后续工作
在执行完实现函数之后,服务器还需要执行一些后续工作:
当以上操作都执行完之后,服务器对当前命令的执行到此告一段落,之后服务器就可以继续从文件事件处理器中取出并处理下一个命令请求
将命令回复发送给客户端
命令实现函数会将命令回复保存到客户端的输出缓冲,并为客户端的套接字关联命令回复处理器,当客户端套接字变为可写状态时,服务器就会执行命令回复处理器,将保存在客户端状态输出缓冲区的命令回复发送给客户端。当命令回复发送完毕之后,回复处理器会清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备。以图1-7所示的客户端状态为例子,当客户端的套接字变为科协状态时,命令回复处理器会将协议格式的命令回复"+OK\r\n"发送给客户端
客户端接收并打印命令回复
当客户端接收到协议格式的命令回复之后,它会将这些回复转换成人们可以识别的可读模式,并打印给用户看
14.2 serverCron函数
Redis服务器中的serverCron函数默认每隔100毫秒执行一次,这个函数负责管理服务器的资源,并且保持服务器自身的良好运转。下面,我们将对serverCron函数执行的操作进行完整介绍,并介绍redisServer结构(服务器状态)中和serverCron有关的属性
更新服务器时间缓存
Redis服务器中有不少功能需要获取系统的当前时间,而每次获取系统的当前时间都需要执行一次系统调用,为了减少系统调用的次数,服务器状态中的unixtime属性和mstime属性被用作当前时间的缓存:
struct redisServer {
……
//保存了秒级精度的系统当前Unix时间戳
time_t unixtime;
//保存了毫秒级精度的系统当前Unix时间戳
long long mstime;
……
};
因为serverCron函数默认会以100毫秒一次的频率更新unixtime属性和mstime属性,所以这两个属性记录的时间的精确度并不高:
更新LRU时钟
服务器中的lrulock属性保存了服务器的LRU时钟,这个属性和上面介绍的unixtime属性、mstime属性一样,都是服务器时间缓存的一种
struct redisServer {
……
//默认每10秒更新一次时钟缓冲
//用于计算键的空转时长
unsigned lruclock:22;
……
};
每个Redis对象都会有一个lru属性,这个lru属性保存了对象最后一次被命令访问的时间:
typedef struct redisObject {
……
unsigned lru:22;
……
} robj;
当服务器要计算一个数据库键的空转时间(也即是数据库键对应的值对象的空转时间),程序会用服务器的lruclock属性记录的时间减去对象的lru属性记录的时间,得出的计算结果就是这个对象的空转时间。
serverCron函数默认会以每10秒一次的频率更新lruclock属性的值,因为这个时钟不是实时,所以根据这个属性计算出来的LRU时间实际上只是一个模糊的估计值
lruclock时钟的当前值可以通过INFO server命令的lru_clock域查看:
127.0.0.1:6379> INFO server
# Server
……
lru_clock:11967088
……
更新服务器每秒执行命令次数
serverCron函数中trackOperationsPerSecond函数会以每100毫秒一次的频率执行, 这个函数的功能是以抽样计算的方式,估算并记录服务器在最近一秒钟处理的命令请求数量,这个值可以通过INFO status命令的instantaneous_ops_per_sec域查看:
127.0.0.1:6379> INFO stats
# Stats
……
instantaneous_ops_per_sec:6
……
上面的命令结果显示,在最近一秒钟内,服务器处理了大概六个命令。trackOperationsPerSecond函数和服务器状态中四个ops_sec开头的属性有关:
struct redisServer {
……
//上次一进行抽样的时间
long long ops_sec_last_sample_time;
//上次一抽样时,服务器已执行命令的数量
long long ops_sec_last_sample_ops;
//REDIS_OPS_SEC_SAMPLES大小(默认值为16)的环形数组
long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES];
//标记上边数组索引
//每次抽样后将值加1
//在值等于16时重置为0
//让ops_sec_samples数组构成一个环形数组
int ops_sec_idx;
……
};
trackOperationsPerSecond函数每次运行,都会根据ops_sec_last_sample_time记录的上一次抽样时间和服务器的当前时间,以及ops_sec_last_sample_time记录的上一次抽样已执行命令数量和服务器当前已执行命令数量,计算出两次trackOperationsPerSecond调用之间,服务器平均每一毫秒处理了多少个命令请求,然后将这个平均值乘以1000,这就得到了服务器在一秒钟内能处理多少个命令你请求的估计值,这个估计值会被作为一个新的数组项被放进ops_sec_samples环形数组里面
当客户端执行INFO命令时,服务器就会调用getOperationsPerSecond函数,根据ops_sec_samples环形数组中的抽样结果,计算出instantaneous_ops_per_sec属性的值,以下是getOperationsPerSecond函数的实现代码:
long long getOperationsPerSecond(void) {
int j;
long long sum = 0;
//计算所有取样值的总和
for (j = 0; j < REDIS_OPS_SEC_SAMPLES; j++)
sum += server.ops_sec_samples[j];
//计算取样的平均值
return sum / REDIS_OPS_SEC_SAMPLES;
}
更新服务器内存峰值记录
服务器状态中的stat_peak_memory属性记录了服务器内存峰值大小:
struct redisServer {
……
//已使用内存峰值
size_t stat_peak_memory;
……
};
每次serverCron函数执行时,程序都会查看服务器当前使用的内存数量,并与stat_peak_memory保存的数值进行比较,如果当前使用的内存数量比stat_peak_memory属性记录的值要大,那么程序就将当前使用的内存数量记录到stat_peak_memory属性里面
INFO memory命令的和used_memory_peak_human两个域分别以两种格式记录了服务器的内存峰值:
127.0.0.1:6379> INFO memory
# Memory
……
used_memory_human:848.87K
used_memory_peak:870552
……
处理SIGTERM信号
在启动服务器时,Redis会为服务器进程的SIGTERM信号关联处理器sigtermHandler函数,这个信号处理器负责在服务器接到SIGTERM信号时,打开服务器状态的shutdown_asap标识:
//SIGTERM信号的处理器
static void sigtermHandler(int sig) {
REDIS_NOTUSED(sig);
//打印日志
redisLogFromHandler(REDIS_WARNING,"Received SIGTERM, scheduling shutdown...");
//打开关闭标识
server.shutdown_asap = 1;
}
每次serverCron函数运行时,程序都会对服务器状态的shutdown_asap属性进行检查,并根据属性的值决定是否关闭服务器:
struct redisServer {
……
//关闭服务器的标识
//值为1时,关闭服务器
//值为0时,不做动作
int shutdown_asap;
……
};
以下代码展示了服务器在接到SIGTERM信号之后,关闭服务器并打印相关日志的过程:
[207 | signal handler] (1380450274) Received SIGTERM, scheduling shutdown...
[207] 29 Sep 18:24:34.899 * Saving the final RDB snapshot before exiting.
[207] 29 Sep 18:24:35.050 * DB saved on disk
[207] 29 Sep 18:24:35.050 * Removing the pid file.
[207] 29 Sep 18:24:35.150 # Redis is now ready to exit, bye bye...
从日志里面可以看到,服务器在关闭自身之前会进行RDB持久化操作,这也是服务器拦截SIGTERM信号的原因,如果服务器一接到SIGTERM信号就立即关闭,那么它就没办法执行持久化操作了。
管理客户端资源
serverCron函数每次执行都会调用clientsCron函数,clientsCron函数会对一定数量的客户端进行以下两个检查:
管理数据库资源
serverCron函数每次执行都会调用databasesCron函数,这个函数会对服务器中的一部分数据库进行检查,删除其中的过期键,并在有需要时,对字典进行收缩操作
执行被延迟的BGREWRITEAOF
在服务器执行BGSAVE命令的期间,如果客户端向服务器发来BGREWRITEAOF命令,那么服务器会将BGREWRITEAOF命令的执行延迟到BGSAVE命令执行完毕之后。服务器的aof_rewrite_scheduled标识记录了服务器是否延迟了BGREWRITEAOF命令
struct redisServer {
……
//如果值为1,那么表示有BGREWRITEAOF命令被延迟了
int aof_rewrite_scheduled; /* Rewrite once BGSAVE terminates. */
……
};
每次serverCron函数执行时,函数都会检查BGSAVE命令或BGREWRITEAOF命令是否正在执行,如果这两个命令都没在执行,并且aof_rewrite_scheduled属性的值为1,那么服务器就会执行之前被推延的BGREWRITEAOF命令
检查持久化操作的运行状态
服务器状态使用rdb_child_pid属性和aof_child_pid属性记录执行BGSAVE命令和BGREWRITEAOF命令的子进程ID,这两个属性也可以用于检查BGSAVE命令或BGREWRITEAOF命令是否正在执行:
struct redisServer {
……
//记录执行BGSAVE命令的子进程ID
//如果服务器没有执行BGSAVE
//那么这个属性的值为-1
pid_t aof_child_pid;
……
//记录执行BGREWRITEAOF命令的子进程ID
//如果服务器没有执行BGREWRITEAOF
//那么这个属性的值为-1
pid_t rdb_child_pid;
……
};
每次serverCron函数执行时,程序都会检查rdb_child_pid和aof_child_pid两个属性的值,只要其中一个属性的值不为-1,程序就会执行一次wait3函数,检查子进程是否有信号发来服务器进程:
如果rdb_child_pid和aof_child_pid两个属性的值都为-1,那么表示服务器没有在进行持久化操作,在这种情况下,程序执行以下三个检查:
如果服务器开启了AOF持久化功能,并且AOF缓冲区里面还有待写入的数据,那么serverCron函数会调用相应的程序,将AOF缓冲区中的内容写入到AOF文件中
关闭异步客户端
服务器会关闭那些输出缓冲区大小超出限制的客户端
增加cronloops计数器的值
服务器状态的cronloops属性记录了serverCron函数执行的次数:
struct redisServer {
……
//serverCron函数的运行次数计数器
//serverCron函数每执行一次,这个属性的值就加1
int cronloops;
……
};
cronloops属性目前在服务器中唯一的作用,就是在复制模块中实现“每执行serverCron函数N次就执行一次指定代码”的功能
14.3 初始化服务器
一个Redis服务器从启动到能够接受客户端的命令请求,需要经过一系列的初始化和设置过程,比如初始化服务器状态、接受用户指定的服务器配置、创建相应的数据结构和网络连接等等,本节接下来的内容将对服务器的整个初始化过程做详细介绍
初始化服务器状态结构
初始化服务器的第一步就是创建一个struct redisServer类型的实例变量server作为服务器的状态,并为结构中的各个属性设置默认值。初始化server变量的工作由redis.c/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;
...
}
以下是initServerConfig函数完成的主要工作:
initServerConfig函数设置的服务器状态属性基本都是一些整数、浮点数、或者字符串属性,除了命令表外,initServerConfig函数没有创建服务器状态的其他数据结构,数据库、慢查询日志、Lua环境、共享对象这些数据结构在之后的步骤才会被创建出来。当initServerConfig函数执行完毕之后,服务器就可以进入初始化的第二个阶段——载入配置选项
载入配置选项
在启动服务器时,用户可以通过给定配置参数或指定配置文件来修改服务器的默认配置举个栗子
# redis-server redis.conf
我们就通过给定配置参数的方式,修改了服务器的运行端口号
# redis-server redis.conf
我们就通过指定配置文件的方式修改了服务器的数据库数量,以及RDB持久化模块的压缩功能。服务器在用initServerConfig函数初始化完server变量之后,就会开始载入用户给定的配置参数和配置文件,并根据用户设定的配置,对server变量相关属性的值进行修改。
初始化服务器数据库结构
在之前执行initServerConfig函数初始化server状态时,程序只创建了命令表一个数据结构,不过除了命令表之外,服务器状态还包含其他数据结构,比如:
当初始化服务器进行到这一步,服务器将调用initServer函数,为以上提到的数据结构分配内存,并在有需要时,为这些数据结构设置或关联初始化值。服务器到现在才初始化数据结构的原因在于,服务器必须先载入用户指定的配置选项,然后才能正确地对数据结构进行初始化。如果在执行initServerConfig函数时就对数据结构进行初始化,那么一旦用户通过配置项修改了和数据结构相关的服务器状态属性,服务器就要重新调整和修改已创建的数据结构。为了避免出现这种麻烦的情况,服务器选择了将server状态的初始化分为两步进行,initServerConfig函数主要负责初始化一般属性,而initServer函数主要负责初始化数据结构
除了初始化数据结构之外,initServer还进行了一些非常重要的操作,其中包括:
还原数据库状态
在完成了对服务器状态server变量的初始化之后,服务器需要载入RDB文件或AOF文件,并根据文件记录的内容来还原服务器的数据库状态
根据服务器是否启用AOF持久化功能,服务器载入数据时所使用的目标文件会有所不同:
当服务器完成数据库状态还原工作之后,服务器将在日志中打印出载入文件并还原数据库状态所耗费的时长:
[5244] 21 Nov 22:43:49.084 * DB loaded from disk: 0.068 seconds
在初始化最后一步,服务器将打印出日志:
[5244] 21 Nov 22:43:49.084 * The server is now ready to accept connections on port 6379
并开始执行服务器的事件循环(loop)。至此,服务器的初始化工作圆满完成,服务器现在开始可以接受客户端的连接请求,并处理客户端发来的命令请求了