7. pexpire key milliseconds O(1)
10.SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC | DESC] [ALPHA] [STORE destination]
2. 用做Hash 类型值的其中一种底层实现
事务提供了一种“将多个命令打包,而后一次性、按顺序地执行”的机制,而且事务在执行的期
间不会主动中断——服务器在执行完事务中的全部命令以后,才会继续处理其余客户端的其余
命令
MULTI 命令的执行标记着事务的开始:
redis> MULTI
OK
这个命令惟一作的就是,将客户端的REDIS_MULTI 选项打开,让客户端从非事务状态切换到事
务状态
当客户端进入事务状态以后,服务器在收到来自客户端的命令时,不会当即执行命令,
而是将这些命令所有放进一个事务队列里,事务队列是一个数组,每一个数组项是都包含三个属性:
1. 要执行的命令(cmd)。
2. 命令的参数(argv)。
3. 参数的个数(argc)
举个例子,若是客户端执行如下命令:
redis> MULTI
OK
redis> SET book-name "Mastering C++ in 21 days"
QUEUED
redis> GET book-name
QUEUED
redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis> SMEMBERS tag
QUEUED
那么程序将为客户端建立如下事务队列:

其实并非全部的命令都会被放进事务队列,其中的例外就是EXEC 、DISCARD 、MULTI
和WATCH 这四个命令——当这四个命令从客户端发送到服务器时,它们会像客户端处于非
事务状态同样,直接被服务器执行
DISCARD 命令用于取消一个事务,它清空客户端的整个事务队列,而后将客户端从事务状态
调整回非事务状态,最后返回字符串OK 给客户端,说明事务已被取消。
Redis 的事务是不可嵌套的,当客户端已经处于事务状态,而客户端又再向服务器发送MULTI
时,服务器只是简单地向客户端发送一个错误,而后继续等待其余命令的入队。MULTI 命令
的发送不会形成整个事务失败,也不会修改事务队列中已有的数据。
WATCH 只能在客户端进入事务状态以前执行,在事务状态下发送WATCH 命令会引起一个
错误,但它不会形成整个事务失败,也不会修改事务队列中已有的数据(和前面处理MULTI
的状况同样)。
WATCH 命令的实现
在每一个表明数据库的redis.h/redisDb 结构类型中,都保存了一个watched_keys 字典,字典
的键是这个数据库被监视的键,而字典的值则是一个链表,链表中保存了全部监视这个键的客
户端。
在任何对数据库键空间(key space)进行修改的命令成功执行以后(好比FLUSHDB 、SET
、DEL 、LPUSH 、SADD 、ZREM ,诸如此类),multi.c/touchWatchKey 函数都会被调用
——它检查数据库的watched_keys 字典,看是否有客户端在监视已经被命令修改的键,若是
有的话,程序将全部监视这个/这些被修改键的客户端的REDIS_DIRTY_CAS 选项打开
当客户端发送EXEC 命令、触发事务执行时,服务器会对客户端的状态进行检查:
• 若是客户端的REDIS_DIRTY_CAS 选项已经被打开,那么说明被客户端监视的键至少有一
个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端
返回空回复,表示事务执行失败。
• 若是REDIS_DIRTY_CAS 选项没有被打开,那么说明全部监视键都安全,服务器正式执行
事务。
最后,当一个客户端结束它的事务时,不管事务是成功执行,仍是失败,watched_keys 字典
中和这个客户端相关的资料都会被清除
在传统的关系式数据库中,经常用ACID 性质来检验事务功能的安全性。
Redis 事务保证了其中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)
单个Redis 命令的执行是原子性的,但Redis 没有在事务上增长任何维持原子性的机制,因此
Redis 事务的执行并非原子性的。
若是一个事务队列中的全部命令都被成功地执行,那么称这个事务执行成功。
另外一方面,若是Redis 服务器进程在执行事务的过程当中被中止——好比接到KILL 信号、宿主
机器停机,等等,那么事务执行失败。
当事务失败时,
Redis 也不会进行任何的重试或者回滚动做
Redis 的一致性问题能够分为三部分来讨论:入队错误、执行错误、Redis 进程被终结。
入队错误
在命令入队的过程当中, 若是客户端向服务器发送了错误的命令, 好比命令的参数数量
不对,等等,那么服务器将向客户端返回一个出错信息,而且将客户端的事务状态设为
REDIS_DIRTY_EXEC 。
当客户端执行EXEC 命令时,Redis 会拒绝执行状态为REDIS_DIRTY_EXEC 的事务,并返回失
败信息。
所以,带有不正确入队命令的事务不会被执行,也不会影响数据库的一致性。
执行错误
若是命令在事务执行的过程当中发生错误,好比说,对一个不一样类型的key 执行了错误的操做,
那么Redis 只会将错误包含在事务的结果中,这不会引发事务中断或整个失败,不会影响已执
行事务命令的结果,也不会影响后面要执行的事务命令,因此它对事务的一致性也没有影响。
Redis 进程被终结
若是Redis 服务器进程在执行事务的过程当中被其余进程终结,或者被管理员强制杀死,那么根
据Redis 所使用的持久化模式,可能有如下状况出现:
• 内存模式:若是Redis 没有采起任何持久化机制,那么重启以后的数据库老是空白的,所
以数据老是一致的。
• RDB 模式:在执行事务时,Redis 不会中断事务去执行保存RDB 的工做,只有在事务执
行以后,保存RDB 的工做才有可能开始。因此当RDB 模式下的Redis 服务器进程在事
务中途被杀死时,事务内执行的命令,无论成功了多少,都不会被保存到RDB 文件里。
恢复数据库须要使用现有的RDB 文件,而这个RDB 文件的数据保存的是最近一次的数
据库快照(snapshot),因此它的数据可能不是最新的,但只要RDB 文件自己没有由于
其余问题而出错,那么还原后的数据库就是一致的。
• AOF 模式:由于保存AOF 文件的工做在后台线程进行,因此即便是在事务执行的中途,
保存AOF 文件的工做也能够继续进行,所以,根据事务语句是否被写入并保存到AOF
文件,有如下两种状况发生:
1)若是事务语句未写入到AOF 文件,或AOF 未被SYNC 调用保存到磁盘,那么当进
程被杀死以后,Redis 能够根据最近一次成功保存到磁盘的AOF 文件来还原数据库,只
要AOF 文件自己没有由于其余问题而出错,那么还原后的数据库老是一致的,但其中的
数据不必定是最新的。
2)若是事务的部分语句被写入到AOF 文件,而且AOF 文件被成功保存,那么不完整的
事务执行信息就会遗留在AOF 文件里,当重启Redis 时,程序会检测到AOF 文件并不
完整,Redis 会退出,并报告错误。须要使用redis-check-aof 工具将部分红功的事务命令
移除以后,才能再次启动服务器。还原以后的数据老是一致的,并且数据也是最新的(直
到事务执行以前为止)。
隔离性(Isolation)
Redis 是单进程程序,而且它保证在执行事务时,不会对事务进行中断,事务能够运行直到执
行完全部事务队列中的命令为止。所以,Redis 的事务是老是带有隔离性的。
持久性(Durability)
由于事务不过是用队列包裹起了一组Redis 命令,并无提供任何额外的持久性功能,因此事
务的持久性由Redis 所使用的持久化模式决定:
• 在单纯的内存模式下,事务确定是不持久的。
• 在RDB 模式下,服务器可能在事务执行以后、RDB 文件更新以前的这段时间失败,所
以RDB 模式下的Redis 事务也是不持久的。
• 在AOF 的“老是SYNC ”模式下,事务的每条命令在执行成功以后,都会当即调用fsync
或fdatasync 将事务数据写入到AOF 文件。可是,这种保存是由后台线程进行的,主
线程不会阻塞直到保存成功,因此从命令执行成功到数据保存到硬盘之间,仍是有一段
很是小的间隔,因此这种模式下的事务也是不持久的。
其余AOF 模式也和“老是SYNC ”模式相似,因此它们都是不持久的
• 事务提供了一种将多个命令打包,而后一次性、有序地执行的机制。
• 事务在执行过程当中不会被中断,全部事务命令执行完以后,事务才能结束。
• 多个命令会被入队到事务队列中,而后按先进先出(FIFO)的顺序执行。
• 带WATCH 命令的事务会将客户端和被监视的键在数据库的watched_keys 字典中进行关
联,当键被修改时,程序会将全部监视被修改键的客户端的REDIS_DIRTY_CAS 选项打开。
• 只有在客户端的REDIS_DIRTY_CAS 选项未被打开时,才能执行事务,不然事务直接返回
失败。
• Redis 的事务保证了ACID 中的一致性(C)和隔离性(I),但并不保证原子性(A)和
持久性(D)
redis频道与订阅
每一个Redis 服务器进程都维持着一个表示服务器状态的redis.h/redisServer 结构,结构的
pubsub_channels 属性是一个字典,这个字典就用于保存订阅频道的信息:
struct redisServer {
// ...
dict *pubsub_channels;
// ...
};复制代码
其中,字典的键为正在被订阅的频道,而字典的值则是一个链表,链表中保存了全部订阅这个
频道的客户端。
当客户端调用SUBSCRIBE 命令时,程序就将客户端和要订阅的频道在pubsub_channels 字
典中关联起来。
了解了pubsub_channels 字典的结构以后,解释PUBLISH 命令的实现就很是简单了:当调
用PUBLISH channel message 命令,程序首先根据channel 定位到字典的键,而后将信息发
送给字典值链表中的全部客户端。
使用UNSUBSCRIBE 命令能够退订指定的频道,这个命令执行的是订阅的反操做:它从
pubsub_channels 字典的给定频道(键)中,删除关于当前客户端的信息,这样被退订频道的
信息就不会再发送给这个客户端。
struct redisServer {
// ...
list *pubsub_patterns;
// ...
};复制代码
redisServer.pubsub_patterns 属性是一个链表,链表中保存着全部和模式相关的信息:
链表中的每一个节点都包含一个redis.h/pubsubPattern 结构
typedef struct pubsubPattern {
redisClient *client;
robj *pattern;
} pubsubPattern;复制代码
client 属性保存着订阅模式的客户端,而pattern 属性则保存着被订阅的模式。
每当调用PSUBSCRIBE 命令订阅一个模式时,程序就建立一个包含客户端信息和被订阅模式的
pubsubPattern 结构,并将该结构添加到redisServer.pubsub_patterns 链表中。
做为例子,下图展现了一个包含两个模式的pubsub_patterns 链表,其中client123 和
client256 都正在订阅tweet.shop.* 模式

经过遍历整个pubsub_patterns 链表,程序能够检查全部正在被订阅的模式,以及订阅这些模
式的客户端。
退订模式
使用PUNSUBSCRIBE 命令能够退订指定的模式,这个命令执行的是订阅模式的反操做:程序
会删除redisServer.pubsub_patterns 链表中,全部和被退订模式相关联的pubsubPattern
结构,这样客户端就不会再收到和模式相匹配的频道发来的信息。
• 订阅信息由服务器进程维持的redisServer.pubsub_channels 字典保存,字典的键为被
订阅的频道,字典的值为订阅频道的全部客户端。
• 当有新消息发送到频道时,程序遍历频道(键)所对应的(值)全部客户端,而后将消息
发送到全部订阅频道的客户端上。
• 订阅模式的信息由服务器进程维持的redisServer.pubsub_patterns 链表保存,链表的
每一个节点都保存着一个pubsubPattern 结构,结构中保存着被订阅的模式,以及订阅该
模式的客户端。程序经过遍历链表来查找某个频道是否和某个模式匹配。
• 当有新消息发送到频道时,除了订阅频道的客户端会收到消息以外,全部订阅了匹配频
道的模式的客户端,也一样会收到消息。
• 退订频道和退订模式分别是订阅频道和订阅模式的反操做。
慢日志
每条慢查询日志都以一个slowlog.h/slowlogEntry 结构定义:
typedef struct slowlogEntry {
// 命令参数
robj **argv;
// 命令参数数量
int argc;
// 惟一标识符
long long id;
// 执行命令消耗的时间,以纳秒(1 / 1,000,000,000 秒)为单位
long long duration;
// 命令执行时的时间
time_t time;
} slowlogEntry;
记录服务器状态的redis.h/redisServer 结构里保存了几个和慢查询有关的属性:
struct redisServer {
// ... other fields
// 保存慢查询日志的链表
list *slowlog;
// 慢查询日志的当前id 值
long long slowlog_entry_id;
// 慢查询时间限制
long long slowlog_log_slower_than;
// 慢查询日志的最大条目数量
unsigned long slowlog_max_len;
// ... other fields
};
slowlog 属性是一个链表,链表里的每一个节点保存了一个慢查询日志结构,全部日志按添加时
间重新到旧排序,新的日志在链表的左端,旧的日志在链表的右端。
slowlog_entry_id 在建立每条新的慢查询日志时增一,用于产生慢查询日志的ID (这个ID
在执行SLOWLOG RESET 以后会被重置)。
slowlog_log_slower_than 是用户指定的命令执行时间上限,执行时间大于等于这个值的命令
会被慢查询日志记录。
slowlog_max_len 慢查询日志的最大数量,当日志数量等于这个值时,添加一条新日志会形成
最旧的一条日志被删除。
在每次执行命令以前,Redis 都会用一个参数记录命令执行前的时间,在命令执行完以后,再
计算一次当前时间,而后将两个时间值相减,得出执行命令所耗费的时间值duration ,并将
duration 传给slowlogPushEntryIfNeed 函数。
针对慢查询日志有三种操做,分别是查看、清空和获取日志数量:
• 查看日志:在日志链表中遍历指定数量的日志节点,复杂度为O(N) 。
• 清空日志:释放日志链表中的全部日志节点,复杂度为O(N) 。
• 获取日志数量:获取日志的数量等同于获取server.slowlog 链表的数量,复杂度为
O(1) 。
• Redis 用一个链表以FIFO 的顺序保存着全部慢查询日志。
• 每条慢查询日志以一个慢查询节点表示,节点中记录着执行超时的命令、命令的参数、命
令执行时的时间,以及执行命令所消耗的时间等信息。
【第五章 内部运做机制】
Redis 中的每一个数据库,都由一个redis.h/redisDb 结构表示:
typedef struct redisDb {
// 保存着数据库以整数表示的号码
int id;
// 保存着数据库中的全部键值对数据
// 这个属性也被称为键空间(key space)
dict *dict;
// 保存着键的过时信息
dict *expires;
// 实现列表阻塞原语,如BLPOP
// 在列表类型一章有详细的讨论
dict *blocking_keys;
dict *ready_keys;
// 用于实现WATCH 命令
// 在事务章节有详细的讨论
dict *watched_keys;
} redisDb;
redisDb 结构的id 域保存着数据库的号码。
这个号码很容易让人将它和切换数据库的SELECT 命令联系在一块儿,可是,实际上,id 属性
并非用来实现SELECT 命令,而是给Redis 内部程序使用的。
当Redis 服务器初始化时, 它会建立出redis.h/REDIS_DEFAULT_DBNUM 个数据库, 并
将全部数据库保存到redis.h/redisServer.db 数组中, 每一个数据库的id 为从0 到
REDIS_DEFAULT_DBNUM - 1 的值。
当执行SELECT number 命令时,程序直接使用redisServer.db[number] 来切换数据库。
可是,一些内部程序,好比AOF 程序、复制程序和RDB 程序,须要知道当前数据库的号码,
若是没有id 域的话,程序就只能在当前使用的数据库的指针,和redisServer.db 数组中所
有数据库的指针进行对比,以此来弄清楚本身正在使用的是那个数据库。
在数据库中,全部键的过时时间都被保存在redisDb 结构的expires 字典里:
typedef struct redisDb {
// ...
dict *expires;
// ...
} redisDb;复制代码
expires 字典的键是一个指向dict 字典(键空间)里某个键的指针,而字典的值则是键所指
向的数据库键的到期时间,这个值以long long 类型表示。
下图展现了一个含有三个键的数据库,其中number 和book 两个键带有过时时间:
Redis 有四个命令能够设置键的生存时间(能够存活多久)和过时时间(何时到期):
• EXPIRE 以秒为单位设置键的生存时间;
• PEXPIRE 以毫秒为单位设置键的生存时间;
• EXPIREAT 以秒为单位,设置键的过时UNIX 时间戳;
• PEXPIREAT 以毫秒为单位,设置键的过时UNIX 时间戳。
虽然有那么多种不一样单位和不一样形式的设置方式,可是expires 字典的值只保存“以毫秒为单
位的过时UNIX 时间戳” ,这就是说,经过进行转换,全部命令的效果最后都和PEXPIREAT
命令的效果同样。
举个例子,从EXPIRE 命令到PEXPIREAT 命令的转换能够用伪代码表示以下:
def EXPIRE(key, sec):
# 将TTL 从秒转换为毫秒
ms = sec_to_ms(sec)
# 获取以毫秒计算的当前UNIX 时间戳
ts_in_ms = get_current_unix_timestamp_in_ms()
# 毫秒TTL 加上毫秒时间戳,就是key 到期的时间戳
PEXPIREAT(ms + ts_in_ms, key)
其余函数的转换方式也是相似的。
做为例子,下图展现了一个expires 字典示例,字典中number 键的过时时间是2013 年2 月
10 日(农历新年),而book 键的过时时间则是2013 年2 月14 日(情人节):

经过expires 字典,能够用如下步骤检查某个键是否过时:
1. 检查键是否存在于expires 字典:若是存在,那么取出键的过时时间;
2. 检查当前UNIX 时间戳是否大于键的过时时间:若是是的话,那么键已通过期;不然,
键未过时。
能够用伪代码来描述这一过程:
def is_expired(key):
key_expire_time = expires.get(key)
if expire_time is not None and current_timestamp() > key_expire_time:
return True
return False复制代码
咱们知道了过时时间保存在expires 字典里,又知道了该如何断定一个键是否过时,如今剩下
的问题是,
过时键删除
若是一个键是过时的,那它何时会被删除?
这个问题有三种可能的答案:
1. 定时删除:在设置键的过时时间时,建立一个定时事件,当过时时间到达时,由事件处理
器自动执行键的删除操做。
2. 惰性删除:听任键过时无论,可是在每次从dict 字典中取出键值时,要检查键是否过
期,若是过时的话,就删除它,并返回空;若是没过时,就返回键值。
3. 按期删除:每隔一段时间,对expires 字典进行检查,删除里面的过时键。
定时删除
定时删除策略对内存是最友好的:由于它保证过时键会在第一时间被删除,过时键所消耗的内
存会当即被释放。
这种策略的缺点是,它对CPU 时间是最不友好的:由于删除操做可能会占用大量的CPU 时间
——在内存不紧张、可是CPU 时间很是紧张的时候(好比说,进行交集计算或排序的时候),
将CPU 时间花在删除那些和当前任务无关的过时键上,这种作法毫无疑问会是低效的。
除此以外,目前Redis 事件处理器对时间事件的实现方式——无序链表,查找一个时间复杂度
为O(N) ——并不适合用来处理大量时间事件。
惰性删除
惰性删除对CPU 时间来讲是最友好的:它只会在取出键时进行检查,这能够保证删除操做只
会在非作不可的状况下进行——而且删除的目标仅限于当前处理的键,这个策略不会在删除其
他无关的过时键上花费任何CPU 时间。
惰性删除的缺点是,它对内存是最不友好的:若是一个键已通过期,而这个键又仍然保留在数
据库中,那么dict 字典和expires 字典都须要继续保存这个键的信息,只要这个过时键不被
删除,它占用的内存就不会被释放。
在使用惰性删除策略时,若是数据库中有很是多的过时键,但这些过时键又正好没有被访问的
话,那么它们就永远也不会被删除(除非用户手动执行),这对于性能很是依赖于内存大小的
Redis 来讲,确定不是一个好消息。
举个例子,对于一些按时间点来更新的数据,好比日志(log),在某个时间点以后,对它们的访
问就会大大减小,若是大量的这些过时数据积压在数据库里面,用户觉得它们已通过期了(已
经被删除了),但实际上这些键却没有真正的被删除(内存也没有被释放),那结果确定是很是
糟糕。
按期删除
从上面对定时删除和惰性删除的讨论来看,这两种删除方式在单一使用时都有明显的缺陷:定
时删除占用太多CPU 时间,惰性删除浪费太多内存。
按期删除是这两种策略的一种折中:
• 它每隔一段时间执行一次删除操做,并经过限制删除操做执行的时长和频率,籍此来减
少删除操做对CPU 时间的影响。
• 另外一方面,经过按期删除过时键,它有效地减小了因惰性删除而带来的内存浪费。
Redis 使用的过时键删除策略是惰性删除加上按期删除,这两个策略相互配合,能够很好地在
合理利用CPU 时间和节约内存空间之间取得平衡。
实现过时键惰性删除策略的核心是db.c/expireIfNeeded 函数——全部命令在读取或写入数
据库以前,程序都会调用expireIfNeeded 对输入键进行检查,并将过时键删除
对过时键的按期删除由redis.c/activeExpireCycle 函执行:每当Redis 的例行处理程序
serverCron 执行时,activeExpireCycle 都会被调用——这个函数在规定的时间限制内,尽
可能地遍历各个数据库的expires 字典,随机地检查一部分键的过时时间,并删除其中的过时
键。
过时键对AOF 、RDB 和复制的影响
更新后的RDB 文件
在建立新的RDB 文件时,程序会对键进行检查,过时的键不会被写入到更新后的RDB 文件
中。
所以,过时键对更新后的RDB 文件没有影响
AOF 文件
在键已通过期,可是尚未被惰性删除或者按期删除以前,这个键不会产生任何影响,AOF 文
件也不会由于这个键而被修改。
当过时键被惰性删除、或者按期删除以后,程序会向AOF 文件追加一条DEL 命令,来显式地
记录该键已被删除。
举个例子,若是客户端使用GET message 试图访问message 键的值,但message 已通过期了,
那么服务器执行如下三个动做:
1. 从数据库中删除message ;
2. 追加一条DEL message 命令到AOF 文件;
3. 向客户端返回NIL
AOF 重写
和RDB 文件相似,当进行AOF 重写时,程序会对键进行检查,过时的键不会被保存到重写
后的AOF 文件。
所以,过时键对重写后的AOF 文件没有影响。
复制
当服务器带有附属节点时,过时键的删除由主节点统一控制:
• 若是服务器是主节点,那么它在删除一个过时键以后,会显式地向全部附属节点发送一
个DEL 命令。
• 若是服务器是附属节点,那么当它碰到一个过时键的时候,它会向程序返回键已过时的
回复,但并不真正的删除过时键。由于程序只根据键是否已通过期、而不是键是否已经被
删除来决定执行流程,因此这种处理并不影响命令的正确执行结果。当接到从主节点发来
的DEL 命令以后,附属节点才会真正的将过时键删除掉。
附属节点不自主对键进行删除是为了和主节点的数据保持绝对一致,由于这个缘由,当一个过
期键还存在于主节点时,这个键在全部附属节点的副本也不会被删除。
这种处理机制对那些使用大量附属节点,而且带有大量过时键的应用来讲,可能会形成一部分
内存不能当即被释放,可是,由于过时键一般很快会被主节点发现并删除,因此这实际上也算
不上什么大问题。
数据库空间的收缩和扩展
由于数据库空间是由字典来实现的,因此数据库空间的扩展/收缩规则和字典的扩展/收缩规则
彻底同样,具体的信息能够参考《字典》章节。
由于对字典进行收缩的时机是由使用字典的程序决定的, 因此Redis 使用
redis.c/tryResizeHashTables 函数来检查数据库所使用的字典是否须要进行收缩:每次
redis.c/serverCron 函数运行的时候,这个函数都会被调用。
tryResizeHashTables 函数的完整定义以下
void tryResizeHashTables(void) {
int j;
for (j = 0; j < server.dbnum; j++) {
// 缩小键空间字典
if (htNeedsResize(server.db[j].dict))
dictResize(server.db[j].dict);
// 缩小过时时间字典
if (htNeedsResize(server.db[j].expires))
dictResize(server.db[j].expires);
}
}
• 数据库主要由dict 和expires 两个字典构成,其中dict 保存键值对,而expires 则
保存键的过时时间。
• 数据库的键老是一个字符串对象,而值能够是任意一种Redis 数据类型,包括字符串、哈
希、集合、列表和有序集。
• expires 的某个键和dict 的某个键共同指向同一个字符串对象,而expires 键的值则
是该键以毫秒计算的UNIX 过时时间戳。
• Redis 使用惰性删除和按期删除两种策略来删除过时的键。
• 更新后的RDB 文件和重写后的AOF 文件都不会保留已通过期的键。
• 当一个过时键被删除以后,程序会追加一条新的DEL 命令到现有AOF 文件末尾。
• 当主节点删除一个过时键以后,它会显式地发送一条DEL 命令到全部附属节点。
• 附属节点即便发现过时键,也不会自做主张地删除它,而是等待主节点发来DEL 命令,
这样能够保证主节点和附属节点的数据老是一致的。
• 数据库的dict 字典和expires 字典的扩展策略和普通字典同样。它们的收缩策略是:当
节点的填充百分比不足10% 时,将可用节点数量减小至大于等于当前已用节点数量。
持久化
在Redis 运行时,RDB 程序将当前内存中的数据库快照保存到磁盘文件中,在Redis 重启动
时,RDB 程序能够经过载入RDB 文件来还原数据库的状态
RDB 功能最核心的是rdbSave 和rdbLoad 两个函数,前者用于生成RDB 文件到磁盘,然后
者则用于将RDB 文件中的数据从新载入到内存中:

rdbSave 函数负责将内存中的数据库数据以RDB 格式保存到磁盘中,若是RDB 文件已存在,
那么新的RDB 文件将替换已有的RDB 文件。
在保存RDB 文件期间,主进程会被阻塞,直到保存完成为止。
SAVE 和BGSAVE 两个命令都会调用rdbSave 函数,但它们调用的方式各有不一样:
• SAVE 直接调用rdbSave ,阻塞Redis 主进程,直到保存完成为止。在主进程阻塞期间,
服务器不能处理客户端的任何请求。
• BGSAVE 则fork 出一个子进程,子进程负责调用rdbSave ,并在保存完成以后向主
进程发送信号,通知保存已完成。由于rdbSave 在子进程被调用,因此Redis 服务器在
BGSAVE 执行期间仍然能够继续处理客户端的请求
SAVE 、BGSAVE 、AOF 写入和BGREWRITEAOF
除了了解RDB 文件的保存方式以外,咱们可能还想知道,两个RDB 保存命令可否同时使用?
它们和AOF 保存工做是否冲突?
SAVE
前面提到过,当SAVE 执行时,Redis 服务器是阻塞的,因此当SAVE 正在执行时,新的
SAVE 、BGSAVE 或BGREWRITEAOF 调用都不会产生任何做用。
只有在上一个SAVE 执行完毕、Redis 从新开始接受请求以后,新的SAVE 、BGSAVE 或
BGREWRITEAOF 命令才会被处理。
另外,由于AOF 写入由后台线程完成,而BGREWRITEAOF 则由子进程完成,因此在SAVE
执行的过程当中,AOF 写入和BGREWRITEAOF 能够同时进行。
BGSAVE
在执行SAVE 命令以前,服务器会检查BGSAVE 是否正在执行当中,若是是的话,服务器就
不调用rdbSave ,而是向客户端返回一个出错信息,告知在BGSAVE 执行期间,不能执行
SAVE 。
这样作能够避免SAVE 和BGSAVE 调用的两个rdbSave 交叉执行,形成竞争条件。
另外一方面,当BGSAVE 正在执行时,调用新BGSAVE 命令的客户端会收到一个出错信息,告
知BGSAVE 已经在执行当中。
BGREWRITEAOF 和BGSAVE 不能同时执行:
• 若是BGSAVE 正在执行,那么BGREWRITEAOF 的重写请求会被延迟到BGSAVE 执
行完毕以后进行,执行BGREWRITEAOF 命令的客户端会收到请求被延迟的回复。
• 若是BGREWRITEAOF 正在执行,那么调用BGSAVE 的客户端将收到出错信息,表示
这两个命令不能同时执行。
BGREWRITEAOF 和BGSAVE 两个命令在操做方面并无什么冲突的地方,不能同时执行
它们只是一个性能方面的考虑:并发出两个子进程,而且两个子进程都同时进行大量的磁盘写
入操做,这怎么想都不会是一个好主意。
载入
当Redis 服务器启动时,rdbLoad 函数就会被执行,它读取RDB 文件,并将文件中的数据库
数据载入到内存中。
在载入期间,服务器每载入1000 个键就处理一次全部已到达的请求,不过只有PUBLISH 、
SUBSCRIBE 、PSUBSCRIBE 、UNSUBSCRIBE 、PUNSUBSCRIBE 五个命令的请求会被正确地处理,
其余命令一概返回错误。等到载入完成以后,服务器才会开始正常处理全部命令。
Note: 发布与订阅功能和其余数据库功能是彻底隔离的,前者不写入也不读取数据库,因此
在服务器载入期间,订阅与发布功能仍然能够正常使用,而没必要担忧对载入数据的完整性产生
影响。
另外,由于AOF 文件的保存频率一般要高于RDB 文件保存的频率,因此通常来讲,AOF 文
件中的数据会比RDB 文件中的数据要新。
所以,若是服务器在启动时,打开了AOF 功能,那么程序优先使用AOF 文件来还原数据。只
有在AOF 功能未打开的状况下,Redis 才会使用RDB 文件来还原数据
前面介绍了保存和读取RDB 文件的两个函数,如今,是时候介绍RDB 文件自己了。
一个RDB 文件能够分为如下几个部分:
REDIS
文件的最开头保存着REDIS 五个字符,标识着一个RDB 文件的开始。
在读入文件的时候,程序能够经过检查一个文件的前五个字节,来快速地判断该文件是否有可
能是RDB 文件。
RDB-VERSION
一个四字节长的以字符表示的整数,记录了该文件所使用的RDB 版本号。
目前的RDB 文件版本为0006 。
由于不一样版本的RDB 文件互不兼容,因此在读入程序时,须要根据版原本选择不一样的读入方
式。
DB-DATA
这个部分在一个RDB 文件中会出现任意屡次,每一个DB-DATA 部分保存着服务器上一个非空数
据库的全部数据。
• rdbSave 会将数据库数据保存到RDB 文件,并在保存完成以前阻塞调用者。
• SAVE 命令直接调用rdbSave ,阻塞Redis 主进程;BGSAVE 用子进程调用rdbSave ,
主进程仍可继续处理命令请求。
• SAVE 执行期间,AOF 写入能够在后台线程进行,BGREWRITEAOF 能够在子进程进
行,因此这三种操做能够同时进行。
• 为了不产生竞争条件,BGSAVE 执行时,SAVE 命令不能执行。
• 为了不性能问题,BGSAVE 和BGREWRITEAOF 不能同时执行。
• 调用rdbLoad 函数载入RDB 文件时,不能进行任何和数据库相关的操做,不过订阅与
发布方面的命令能够正常执行,由于它们和数据库不相关联。
• RDB 文件的组织方式以下:

• 键值对在RDB 文件中的组织方式以下:

RDB 文件使用不一样的格式来保存不一样类型的值。
Redis 分别提供了RDB 和AOF 两种持久化机制:
• RDB 将数据库的快照(snapshot)以二进制的方式保存到磁盘中。
• AOF 则以协议文本的方式,将全部对数据库进行过写入的命令(及其参数)记录到AOF
文件,以此达到记录数据库状态的目的。
Redis 将全部对数据库进行过写入的命令(及其参数)记录到AOF 文件,以此达到记录数据库
状态的目的,为了方便起见,咱们称呼这种记录过程为同步
为了处理的方便,AOF 文件使用网络通信协议的格式来保存这些命令
缓存追加
当命令被传播到AOF 程序以后,程序会根据命令以及命令的参数,将命令从字符串对象转换
回原来的协议文本。
好比说,若是AOF 程序接受到的三个参数分别保存着SET 、KEY 和VALUE 三个字符串,那么
它将生成协议文本"*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n" 。
协议文本生成以后,它会被追加到redis.h/redisServer 结构的aof_buf 末尾。
redisServer 结构维持着Redis 服务器的状态,aof_buf 域则保存着全部等待写入到AOF 文
件的协议文本:
struct redisServer {
// 其余域...
sds aof_buf;
// 其余域...
};
至此,追加命令到缓存的步骤执行完毕
文件写入和保存
每当服务器常规任务函数被执行、或者事件处理器被执行时,aof.c/flushAppendOnlyFile 函
数都会被调用,这个函数执行如下两个工做:
WRITE:根据条件,将aof_buf 中的缓存写入到AOF 文件。
SAVE:根据条件,调用fsync 或fdatasync 函数,将AOF 文件保存到磁盘中。
两个步骤都须要根据必定的条件来执行,而这些条件由AOF 所使用的保存模式来决定,如下
小节就来介绍AOF 所使用的三种保存模式,以及在这些模式下,步骤WRITE 和SAVE 的调
用条件。
AOF 保存模式
Redis 目前支持三种AOF 保存模式,它们分别是:
1. AOF_FSYNC_NO :不保存。
2. AOF_FSYNC_EVERYSEC :每一秒钟保存一次。
3. AOF_FSYNC_ALWAYS :每执行一个命令保存一次。
如下三个小节将分别讨论这三种保存模式。
不保存
在这种模式下,每次调用flushAppendOnlyFile 函数,WRITE 都会被执行,但SAVE 会被
略过。
在这种模式下,SAVE 只会在如下任意一种状况中被执行:
• Redis 被关闭
• AOF 功能被关闭
• 系统的写缓存被刷新(多是缓存已经被写满,或者按期保存操做被执行)
这三种状况下的SAVE 操做都会引发Redis 主进程阻塞。
每一秒钟保存一次
在这种模式中,SAVE 原则上每隔一秒钟就会执行一次,由于SAVE 操做是由后台子线程调用
的,因此它不会引发服务器主进程阻塞。
注意,在上一句的说明里面使用了词语“原则上” ,在实际运行中,程序在这种模式下对fsync
或fdatasync 的调用并非每秒一次,它和调用flushAppendOnlyFile 函数时Redis 所处的
状态有关。
每当flushAppendOnlyFile 函数被调用时,可能会出现如下四种状况:
• 子线程正在执行SAVE ,而且:
1. 这个SAVE 的执行时间未超过2 秒,那么程序直接返回,并不执行WRITE 或新的
SAVE 。
2. 这个SAVE 已经执行超过2 秒,那么程序执行WRITE ,但不执行新的SAVE 。
注意,由于这时WRITE 的写入必须等待子线程先完成(旧的)SAVE ,所以这里
WRITE 会比平时阻塞更长时间。
• 子线程没有在执行SAVE ,而且:
3. 上次成功执行SAVE 距今不超过1 秒,那么程序执行WRITE ,但不执行SAVE 。
4. 上次成功执行SAVE 距今已经超过1 秒,那么程序执行WRITE 和SAVE

根据以上说明能够知道,在“每一秒钟保存一次”模式下,若是在状况1 中发生故障停机,那么
用户最多损失小于2 秒内所产生的全部数据。
若是在状况2 中发生故障停机,那么用户损失的数据是能够超过2 秒的。
Redis 官网上所说的,AOF 在“每一秒钟保存一次”时发生故障,只丢失1 秒钟数据的说法,实
际上并不许确。
每执行一个命令保存一次
在这种模式下,每次执行完一个命令以后,WRITE 和SAVE 都会被执行。
另外,由于SAVE 是由Redis 主进程执行的,因此在SAVE 执行期间,主进程会被阻塞,不能
接受命令请求。
AOF 保存模式对性能和安全性的影响
在上一个小节,咱们简短地描述了三种AOF 保存模式的工做方式,如今,是时候研究一下这
三个模式在安全性和性能方面的区别了。
对于三种AOF 保存模式,它们对服务器主进程的阻塞状况以下:
1. 不保存(AOF_FSYNC_NO):写入和保存都由主进程执行,两个操做都会阻塞主进程。
2. 每一秒钟保存一次(AOF_FSYNC_EVERYSEC):写入操做由主进程执行,阻塞主进程。保存
操做由子线程执行,不直接阻塞主进程,但保存操做完成的快慢会影响写入操做的阻塞
时长。
3. 每执行一个命令保存一次(AOF_FSYNC_ALWAYS):和模式1 同样。
由于阻塞操做会让Redis 主进程没法持续处理请求,因此通常说来,阻塞操做执行得越少、完
成得越快,Redis 的性能就越好。
模式1 的保存操做只会在AOF 关闭或Redis 关闭时执行,或者由操做系统触发,在通常状况
下,这种模式只须要为写入阻塞,所以它的写入性能要比后面两种模式要高,固然,这种性能
的提升是以下降安全性为代价的:在这种模式下,若是运行的中途发生停机,那么丢失数据的
数量由操做系统的缓存冲洗策略决定。
模式2 在性能方面要优于模式3 ,而且在一般状况下,这种模式最多丢失很少于2 秒的数据,
因此它的安全性要高于模式1 ,这是一种兼顾性能和安全性的保存方案。
模式3 的安全性是最高的,但性能也是最差的,由于服务器必须阻塞直到命令信息被写入并保
存到磁盘以后,才能继续处理请求。
综合起来,三种AOF 模式的操做特性能够总结以下:
AOF 文件保存了Redis 的数据库状态,而文件里面包含的都是符合Redis 通信协议格式的命
令文本。
这也就是说,只要根据AOF 文件里的协议,从新执行一遍里面指示的全部命令,就能够还原
Redis 的数据库状态了。
Redis 须要对AOF 文件进行重写(rewrite):建立一个新的AOF 文件
来代替原有的AOF 文件,新AOF 文件和原有AOF 文件保存的数据库状态彻底同样,但新
AOF 文件的体积小于等于原有AOF 文件的体积
所谓的“重写”实际上是一个有歧义的词语,实际上,AOF 重写并不须要对原有的AOF 文件进行
任何写入和读取,它针对的是数据库中键的当前值。
根据键的类型,使用适当的写入命令来重现键的当前值,这就是AOF 重写的实现原理。
AOF 后台重写
上一节展现的AOF 重写程序能够很好地完成建立一个新AOF 文件的任务,可是,在执行这
个程序的时候,调用者线程会被阻塞。
很明显,做为一种辅佐性的维护手段,Redis 不但愿AOF 重写形成服务器没法处理请求,因此
Redis 决定将AOF 重写程序放到(后台)子进程里执行,这样处理的最大好处是:
1. 子进程进行AOF 重写期间,主进程能够继续处理命令请求。
2. 子进程带有主进程的数据副本,使用子进程而不是线程,能够在避免锁的状况下,保证数
据的安全性。
不过,使用子进程也有一个问题须要解决:由于子进程在进行AOF 重写期间,主进程还须要
继续处理命令,而新的命令可能对现有的数据进行修改,这会让当前数据库的数据和重写后的
AOF 文件中的数据不一致。
为了解决这个问题,Redis 增长了一个AOF 重写缓存,这个缓存在fork 出子进程以后开始启
用,Redis 主进程在接到新的写命令以后,除了会将这个写命令的协议内容追加到现有的AOF
文件以外,还会追加到这个缓存中
AOF 后台重写的触发条件
AOF 重写能够由用户经过调用BGREWRITEAOF 手动触发。
另外,服务器在AOF 功能开启的状况下,会维持如下三个变量:
• 记录当前AOF 文件大小的变量aof_current_size 。
• 记录最后一次AOF 重写以后,AOF 文件大小的变量aof_rewirte_base_size 。
• 增加百分比变量aof_rewirte_perc 。
每次当serverCron 函数执行时,它都会检查如下条件是否所有知足,若是是的话,就会触发
自动的AOF 重写:
1. 没有BGSAVE 命令在进行。
2. 没有BGREWRITEAOF 在进行。
3. 当前AOF 文件大小大于server.aof_rewrite_min_size (默认值为1 MB)。
4. 当前AOF 文件大小和最后一次AOF 重写后的大小之间的比率大于等于指定的增加百分
比。
默认状况下,增加百分比为100% ,也便是说,若是前面三个条件都已经知足,而且当前AOF
文件大小比最后一次AOF 重写时的大小要大一倍的话,那么触发自动AOF 重写。
• AOF 文件经过保存全部修改数据库的命令来记录数据库的状态。
• AOF 文件中的全部命令都以Redis 通信协议的格式保存。
• 不一样的AOF 保存模式对数据的安全性、以及Redis 的性能有很大的影响。
• AOF 重写的目的是用更小的体积来保存数据库状态,整个重写过程基本上不影响Redis
主进程处理命令请求。
• AOF 重写是一个有歧义的名字,实际的重写工做是针对数据库的当前值来进行的,程序
既不读写、也不使用原有的AOF 文件。
• AOF 能够由用户手动触发,也能够由服务器自动触发。
事件是Redis 服务器的核心,它处理两项重要的任务:
1. 处理文件事件:在多个客户端中实现多路复用,接受它们发来的命令请求,并将命令的执
行结果返回给客户端。
2. 时间事件:实现服务器常规操做(server cron job)。
Redis 将这类由于对套接字进行多路复用而产生的事件称为文件事件(file event),文件事件可
以分为读事件和写事件两类
读事件
读事件标志着客户端命令请求的发送状态。
当一个新的客户端链接到服务器时,服务器会给为该客户端绑定读事件,直到客户端断开链接
以后,这个读事件才会被移除。
写事件
写事件标志着客户端对命令结果的接收状态。
和客户端自始至终都关联着读事件不一样,服务器只会在有命令结果要传回给客户端时,才会为
客户端关联写事件,而且在命令结果传送完毕以后,客户端和写事件的关联就会被移除
由于在同一次文件事件处理器的调用中,单个客户端只能执行其中一种事件(要么读,要么写,
但不能又读又写),当出现读事件和写事件同时就绪的状况时,事件处理器优先处理读事件。
这也就是说,当服务器有命令结果要返回客户端,而客户端又有新命令请求进入时,服务器先
处理新命令请求。
时间事件记录着那些要在指定时间点运行的事件,多个时间事件以无序链表的形式保存在服务
器状态中。
每一个时间事件主要由三个属性组成:
• when :以毫秒格式的UNIX 时间戳为单位,记录了应该在什么时间点执行事件处理函数。
• timeProc :事件处理函数。
• next 指向下一个时间事件,造成链表。
根据timeProc 函数的返回值,能够将时间事件划分为两类:
• 若是事件处理函数返回ae.h/AE_NOMORE ,那么这个事件为单次执行事件:该事件会在指
定的时间被处理一次,以后该事件就会被删除,再也不执行。
• 若是事件处理函数返回一个非AE_NOMORE 的整数值,那么这个事件为循环执行事件:该
事件会在指定的时间被处理,以后它会按照事件处理函数的返回值,更新事件的when 属
性,让这个事件在以后的某个时间点再次运行,并以这种方式一直更新并运行下去。
能够用伪代码来表示这两种事件的处理方式:
def handle_time_event(server, time_event):
# 执行事件处理器,并获取返回值
# 返回值能够是AE_NOMORE ,或者一个表示毫秒数的非符整数值
retval = time_event.timeProc()
if retval == AE_NOMORE:
# 若是返回AE_NOMORE ,那么将事件从链表中删除,再也不执行
server.time_event_linked_list.delete(time_event)
else:
# 不然,更新事件的when 属性
# 让它在当前时间以后的retval 毫秒以后再次运行
time_event.when = unix_ts_in_ms() + retval
当时间事件处理器被执行时,它遍历全部链表中的时间事件,检查它们的到达事件(when 属
性),并执行其中的已到达事件:
def process_time_event(server):
# 遍历时间事件链表
for time_event in server.time_event_linked_list:
# 检查事件是否已经到达
if time_event.when >= unix_ts_in_ms():
# 处理已到达事件
handle_time_event(server, time_event)
Note: 无序链表并不影响时间事件处理器的性能
在目前的版本中,正常模式下的Redis 只带有serverCron 一个时间事件,而在benchmark 模
式下,Redis 也只使用两个时间事件。
在这种状况下,程序几乎是将无序链表退化成一个指针来使用,因此使用无序链表来保存时间
事件,并不影响事件处理器的性能。
时间事件应用实例:服务器常规操做
对于持续运行的服务器来讲,服务器须要按期对自身的资源和状态进行必要的检查和整理,从
而让服务器维持在一个健康稳定的状态,这类操做被统称为常规操做(cron job)。
在Redis 中,常规操做由redis.c/serverCron 实现,它主要执行如下操做:
• 更新服务器的各种统计信息,好比时间、内存占用、数据库占用状况等。
• 清理数据库中的过时键值对。
• 对不合理的数据库进行大小调整。
• 关闭和清理链接失效的客户端。
• 尝试进行AOF 或RDB 持久化操做。
• 若是服务器是主节点的话,对附属节点进行按期同步。
• 若是处于集群模式的话,对集群进行按期同步和链接测试。
Redis 将serverCron 做为时间事件来运行,从而确保它每隔一段时间就会自动运行一次,又
由于serverCron 须要在Redis 服务器运行期间一直按期运行,因此它是一个循环时间事件:
serverCron 会一直按期执行,直到服务器关闭为止。
在Redis 2.6 版本中,程序规定serverCron 每隔10 毫秒就会被运行一次。从Redis 2.8 开始,
10 毫秒是serverCron 运行的默认间隔,而具体的间隔能够由用户本身调整
实际处理时间事件的时间,一般会比时间事件所预约的时间要晚,至于延迟的
时间有多长,取决于时间事件执行以前,执行文件事件所消耗的时间
文件事件先于时间事件处理,根据状况,若是处理文件事件耗费了很是多的时间,serverCron 被推迟到一两秒以后才能执行,也是有可能的。
• Redis 的事件分为时间事件和文件事件两类。
• 文件事件分为读事件和写事件两类:读事件实现了命令请求的接收,写事件实现了命令
结果的返回。
• 时间事件分为单次执行事件和循环执行事件,服务器常规操做serverCron 就是循环事
件。
• 文件事件和时间事件之间是合做关系:一种事件会等待另外一种事件完成以后再执行,不
会出现抢占状况。
• 时间事件的实际执行时间一般会比预约时间晚一些。
从启动Redis 服务器,到服务器能够接受外来客户端的网络链接这段时间,Redis 须要执行一
系列初始化操做。
整个初始化过程能够分为如下六个步骤:
1. 初始化服务器全局状态。
2. 载入配置文件。
3. 建立daemon 进程。
4. 初始化服务器功能模块。
5. 载入数据。
6. 开始事件循环。
1. 初始化服务器全局状态
redis.h/redisServer 结构记录了和服务器相关的全部数据,这个结构主要包含如下信息:
• 服务器中的全部数据库。
• 命令表:在执行命令时,根据字符来查找相应命令的实现函数。
• 事件状态。
• 服务器的网络链接信息:套接字地址、端口,以及套接字描述符。
• 全部已链接客户端的信息。
• Lua 脚本的运行环境及相关选项。
• 实现订阅与发布(pub/sub)功能所需的数据结构。
• 日志(log)和慢查询日志(slowlog)的选项和相关信息。
• 数据持久化(AOF 和RDB)的配置和状态。
• 服务器配置选项:好比要建立多少个数据库,是否将服务器进程做为daemon 进程来运
行,最大链接多少个客户端,压缩结构(zip structure)的实体数量,等等。
• 统计信息:好比键有多少次命令、不命中,服务器的运行时间,内存占用,等等。
在这一步,程序建立一个redisServer 结构的实例变量server 用做服务器的全局状态,并将
server 的各个属性初始化为默认值。
2. 载入配置文件
在初始化服务器的上一步中,程序为server 变量(也便是服务器状态)的各个属性设置了默
认值,但这些默认值有时候并非最合适的:
• 用户可能想使用AOF 持久化,而不是默认的RDB 持久化。
• 用户可能想用其余端口来运行Redis ,以免端口冲突。
• 用户可能不想使用默认的16 个数据库,而是分配更多或更少数量的数据库。
• 用户可能想对默认的内存限制措施和回收策略作调整。
等等。
为了让使用者按本身的要求配置服务器,Redis 容许用户在运行服务器时,提供相应的配置文
件(config file)或者显式的选项(option),Redis 在初始化完server 变量以后,会读入配置
文件和选项,而后根据这些配置来对server 变量的属性值作相应的修改:
1. 若是单纯执行redis-server 命令,那么服务器以默认的配置来运行Redis 。
2. 另外一方面,若是给Redis 服务器送入一个配置文件,那么Redis 将按配置文件的设置来
更新服务器的状态。
好比说,经过命令redis-server /etc/my-redis.conf ,Redis 会根据my-redis.conf
文件的内容来对服务器状态作相应的修改。
3. 除此以外,还能够显式地给服务器传入选项,直接修改服务器配置。
举个例子,经过命令redis-server --port 10086 ,可让Redis 服务器端口变动为
10086 。
4. 固然,同时使用配置文件和显式选项也是能够的,若是文件和选项有冲突的地方,那么优
先使用选项所指定的配置值。
举个例子,若是运行命令redis-server /etc/my-redis.conf --port 10086 ,而且
my-redis.conf 也指定了port 选项,那么服务器将优先使用--port 10086 (其实是
选项指定的值覆盖了配置文件中的值)
3. 建立daemon 进程
Redis 默认以daemon 进程的方式运行。
当服务器初始化进行到这一步时,程序将建立daemon 进程来运行Redis ,并建立相应的pid
文件。
4. 初始化服务器功能模块
在这一步,初始化程序完成两件事:
• 为server 变量的数据结构子属性分配内存。
• 初始化这些数据结构。
为数据结构分配内存,并初始化这些数据结构,等同于对相应的功能进行初始化。
好比说,当为订阅与发布所需的链表分配内存以后,订阅与发布功能就处于就绪状态,随时可
觉得Redis 所用了。
在这一步,程序完成的主要动做以下:
• 初始化Redis 进程的信号功能。
• 初始化日志功能。
• 初始化客户端功能。
• 初始化共享对象。
• 初始化事件功能。
• 初始化数据库。
• 初始化网络链接。
• 初始化订阅与发布功能。
• 初始化各个统计变量。
• 关联服务器常规操做(cron job)到时间事件,关联客户端应答处理器到文件事件。
• 若是AOF 功能已打开,那么打开或建立AOF 文件。
• 设置内存限制。
• 初始化Lua 脚本环境。
• 初始化慢查询功能。
• 初始化后台操做线程
虽然全部功能已经就绪,但这时服务器的数据库仍是一片空白,程序还须要将服务器上一次执
行时记录的数据载入到当前服务器中,服务器的初始化才算真正完成。
5.载入数据
在这一步,程序须要将持久化在RDB 或者AOF 文件里的数据,载入到服务器进程里面。
若是服务器有启用AOF 功能的话,那么使用AOF 文件来还原数据;不然,程序使用RDB 文
件来还原数据。
当执行完这一步时,服务器打印出一段载入完成信息:
[6717] 22 Feb 11:59:14.830 * DB loaded from disk: 0.068 seconds
6. 开始事件循环
到了这一步,服务器的初始化已经完成,程序打开事件循环,开始接受客户端链接。
如下是服务器在这一步打印的信息:
[6717] 22 Feb 11:59:14.830 * The server is now ready to accept connections on port 6379
如下是初始化完成以后,服务器状态和各个模块之间的关系图:

Redis 以多路复用的方式来处理多个客户端,为了让多个客户端之间独立分开、不互相干扰,
服务器为每一个已链接客户端维持一个redisClient 结构,从而单独保存该客户端的状态信息。
redisClient 结构主要包含如下信息:
• 套接字描述符。
• 客户端正在使用的数据库指针和数据库号码。
• 客户端的查询缓存(query buffer)和回复缓存(reply buffer)。
• 一个指向命令函数的指针,以及字符串形式的命令、命令参数和命令个数,这些属性会在
命令执行时使用。
• 客户端状态:记录了客户端是否处于SLAVE 、MONITOR 或者事务状态。
• 实现事务功能(好比MULTI 和WATCH)所需的数据结构。
• 实现阻塞功能(好比BLPOP 和BRPOPLPUSH)所需的数据结构。
• 实现订阅与发布功能(好比PUBLISH 和SUBSCRIBE)所需的数据结构。
• 统计数据和选项:客户端建立的时间,客户端和服务器最后交互的时间,缓存的大小,等
等。
命令的请求、处理和结果返回
当客户端连上服务器以后,客户端就能够向服务器发送命令请求了。
从客户端发送命令请求,到命令被服务器处理、并将结果返回客户端,整个过程有如下步骤:
1. 客户端经过套接字向服务器传送命令协议数据。
2. 服务器经过读事件来处理传入数据,并将数据保存在客户端对应redisClient 结构的查
询缓存中。
3. 根据客户端查询缓存中的内容,程序从命令表中查找相应命令的实现函数。
4. 程序执行命令的实现函数,修改服务器的全局状态server 变量,并将命令的执行结果保
存到客户端redisClient 结构的回复缓存中,而后为该客户端的fd 关联写事件。
5. 当客户端fd 的写事件就绪时,将回复缓存中的命令结果传回给客户端。至此,命令执行
完毕。
命令请求实例:SET 的执行过程
为了更直观地理解命令执行的整个过程,咱们用一个实际执行SET 命令的例子来说解命令执
行的过程。
假设如今客户端C1 是链接到服务器S 的一个客户端,当用户执行命令SET YEAR 2013 时,客
户端调用写入函数,将协议内容*3\r\n$3\r\nSET\r\n$4\r\nYEAR\r\n$4\r\n2013\r\n" 写
入链接到服务器的套接字中。
当S 的文件事件处理器执行时,它会察觉到C1 所对应的读事件已经就绪,因而它将协议文本
读入,并保存在查询缓存。
经过对查询缓存进行分析(parse),服务器在命令表中查找SET 字符串所对应的命令实现函数,
最终定位到t_string.c/setCommand 函数,另外,两个命令参数YEAR 和2013 也会以字符串
的形式保存在客户端结构中。
接着,程序将客户端、要执行的命令、命令参数等送入命令执行器:执行器调用setCommand
函数,将数据库中YEAR 键的值修改成2013 ,而后将命令的执行结果保存在客户端的回复缓存
中,并为客户端fd 关联写事件,用于将结果回写给客户端。
由于YEAR 键的修改,其余和数据库命名空间相关程序,好比AOF 、REPLICATION 还有事
务安全性检查(是否修改了被WATCH 监视的键?)也会被触发,当这些后续程序也执行完毕之
后,命令执行器退出,服务器其余程序(好比时间事件处理器)继续运行。
当C1 对应的写事件就绪时,程序就会将保存在客户端结构回复缓存中的数据回写给客户端,
当客户端接收到数据以后,它就将结果打印出来,显示给用户看。
以上就是SET YEAR 2013 命令执行的整个过程
• 服务器通过初始化以后,才能开始接受命令。
• 服务器初始化能够分为六个步骤:
1. 初始化服务器全局状态。
2. 载入配置文件。
3. 建立daemon 进程。
4. 初始化服务器功能模块。
5. 载入数据。
6. 开始事件循环。
• 服务器为每一个已链接的客户端维持一个客户端结构,这个结构保存了这个客户端的全部
状态信息。
• 客户端向服务器发送命令,服务器接受命令而后将命令传给命令执行器,执行器执行给
定命令的实现函数,执行完成以后,将结果保存在缓存,最后回传给客户端。
2.为了防止A意外释放B的锁,val的值能够设置成该机器的惟一标识,例如时间+请求号。在解锁时须要校验是否是解锁的请求来自于同一个服务器,若是不是说明这是别人的锁不能解