目录
服务器中的数据库
切换数据库
数据库键空间
添加新键
删除键
更新键
对键取值
其他键空间操作
读写键空间时的维护操作
设置键的生存时间或过期时间
设置过期时间
保存过期时间
移除过期时间
计算并返回剩余生存时间
过期键的判定
过期键删除策略
定时删除
惰性删除
定期删除
Redis的过期键删除策略
惰性删除策略的实现
定期删除策略的实现
AOF、RDB和复制功能对过期键的处理
生成RDB文件
载入RDB文件
AOF文件写入
AOF重写
复制
数据库通知
发送通知
发送通知的实现
重点回顾
Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db 数组中,db数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一 个数据库:
在初始化服务器时,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库:
dbnum属性的值由服务器配置的database选项决定,默认情况下,该选项的值为 16, 所以Redis服务器默认会创建16个数据库,如图
每个Redis客户端都有自己的目标数据库,每当客户端执行数据库写命令或者数据库读 命令的时候,目标数据库就会成为这些命令的操作对象。
默认情况下,Redis客户端的目标数据库为0号数据库,但客户端可以通过执行 SELECT命令来切换目标数据库。
以下代码示例演示了客户端在0号数据库设置并读取键msg, 之后切换到2号数据库 并执行类似操作的过程:
在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的目标数 据库,这个属性是一个指向redisDb结构的指针:
redisClient.db指针指向redisServer.db数组的其中一个元素,而被指向的元 素就是客户端的目标数据库。
比如说,如果某个客户端的目标数据库为1号数据库,那么这个客户端所对应的客户端 状态和服务器状态之间的关系如图
如果这时客户端执行命令SELECT 2, 将目标数据库改为2号数据库,那么客户端状态 和服务器状态之间的关系将更新成图。
通过修改redisClient.db指针,让它指向服务器中的不同数据库,从而实现切换目 标数据库的功能一这就是SELECT命令的实现原理。
如果你在其他语言的客户端中执行Redis命令,并且该客户端没有像redis-cli那 样一直显示目标数据库的号码,那么在数次切换数据库之后,你很可能会忘记自己当前 正在使用的是哪个数据库。当出现这种情况时,为了避免对数据库进行误操作,在执 行Redis命令特别是像FLUSHDB这样的危险命令之前,最好先执行一个SELECT命令, 显式地切换到指定的数据库,然后才执行别的命令。
Redis是一个键值对(key-value pair)数据库服务器,服务器中的每个数据库都由 一个redis.h/redisDb结构表示,其中,redisDb结构的dict字典保存了数据库中的 所有键值对,我们将这个字典称为键空间(key space) :
键空间和用户所见的数据库是直接对应的:
键空间的键也就是数据库的键,每个键都是一个字符串对象。
键空间的值也就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象、 集合对象和有序集合对象中的任意一种Redis对象。
举个例子,如果我们在空白的数据库中执行以下命令:
那么在这些命令执行之后,数据库的键空间将会是图所展示的样子:
alphabet是一个列表键,键的名字是一个包含字符串"alphabet"的字符串对 象,键的值则是一个包含三个元素的列表对象。
book是一个哈希表键,键的名字是一个包含字符串"book"的字符串对象,键的 值则是一个包含三个键值对的哈希表对象。
message是一个字符串键.键的名字是一个包含字符串"message"的字符串对象, 键的值则是一个包含字符串"hello world"的字符串对象。
因为数据库的键空间是一个字典,所以所有针对数据库的操作,比如添加一个键值对到 数据库,或者从数据库中删除一个键值对,又或者在数据库中获取某个键值对等,实际上都 是通过对键空间字典进行操作来实现的,以下几个小节将分别介绍数据库的添加、删除、更 新、取值等操作的实现原理。
添加一个新键值对到数据库,实际上就是将一个新键值对添加到键空间字典里面,其中 键为字符串对象,而值则为任意一种类型的Redis对象。
举个例子,如果键空间当前的状态如图所示,那么在执行以下命令之后:
键空间将添加一个新的键值对,这个新键值对的键是一个包含字符串"date"的字符串对 象,而键值对的值则是一个包含字符串"2013.12.1"的字符串对象,如图
删除数据库中的一个键,实际上就是在键空间里面删除键所对应的键值对对象。 举个例子,如果键空间当前的状态如图所示,那么在执行以下命令之后:
键book以及它的值将从键空间中被删除,如图
对一个数据库键进行更新,实际上就是对键空间里面键所对应的值对象进行更新,根据 值对象的类型不同,更新的具体方法也会有所不同。
举个例子,如果键空间当前的状态如图所示,那么在执行以下命令之后
键message的值对象将从之前包含"hello world"字符串更新为包含"blah blah" 字符串,如图所示。
如果我们继续执行以下命令
那么键空间中book键的值对象(一个哈希对象)将被更新,新的键值对page和320会 被添加到值对象里面,如图
对一个数据库键进行取值,实际上就是在键空间中取出键所对应的值对象,根据值对象 的类型不同,具体的取值方法也会有所不同。
举个例子,如果键空间当前的状态如图所示,那么当执行以下命令时:
GET命令将首先在键空间中查找键message, 找到键之后接着取得该键所对应的字符串对 象值,之后再返回值对象所包含的字符串"hello world", 取值过程如图
当执行以下命令时
LRANGE命令将首先在键空间中查找键alphabet,找到键之后接着取得该键所对应的列 表对象值,之后再返回列表对象中包含的三个字符串对象的值,取值过程如图
除了上面列出的添加、删除、更新、取值操作之外,还有很多针对数据库本身的Redis 命令,也是通过对键空间进行处理来完成的。
比如说,用于清空整个数据库的FLUSHDB命令,就是通过删除键空间中的所有键值对 来实现的。又比如说,用于随机返回数据库中某个键的RANDOMKEY命令,就是通过在键 空间中随机返回一个键来实现的。
另外,用于返回数据库键数量的DBSIZE命令,就是通过返回键空间中包含的键值对的 数量来实现的。类似的命令还有EXISTS、RENA.Me、KEYS等,这些命令都是通过对键空间 进行操作来实现的。
当使用Redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作. 还会执行一些额外的维护操作,其中包括:
在读取一个键之后(读操作和写操作都要对键进行读取),服务器会根据键是否存在 来更新服务器的键空间命中(hit)次数或键空间不命中(miss)次数,这两个值可 以在INFO stats命令的keyspace_hits属性和keyspace_misses属性中查看。
在读取一个键之后,服务器会更新键的LRU (最后一次使用)时间,这个值可以用于 计算键的闲置时间,使用OBJECT idletime
如果服务器在读取一个键时发现该键已经过期,那么服务器会先删除这个过期键,然后才执行余下的其他操作。
如果有客户端使用WATCH命令监视了某个键,那么服务器在对被监视的键进行修 改之后,会将这个键标记为脏(dirty), 从而让事务程序注意到这个键已经被修改 过。
服务器每次修改一个键之后,都会对脏(dirty)键计数器的值增1这个计数器会触 发服务器的持久化以及复制操作。
如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发 送相应的数据库通知。
通过EXPIRE命令或者PEXPIRE命令,客户端可以以秒或者毫秒精度为数据库中的某 个键设置生存时间(Time To Live, TTL), 在经过指定的秒数或者毫秒数之后,服务器就会 自动删除生存时间为0的键:
注意,SETEX命令可以在设置一个字符串键的同时为键设置过期时间,因为这个命令是一个 类型限定的命令(只能用于字符串键),但SETEX命令 设置过期时间的原理和本章介绍的EXPIRE命令设置过期时间的原理是完全一样的。
与EXPIRE命令和PEXPIRE命令类似,客户端可以通过EXPIREAT命令或PEXPIREAT 命令,以秒或者毫秒精度给数据库中的某个键设置过期时间(expire time)。
过期时间是一个UNIX时间戳,当键的过期时间来临时,服务器就会自动从数据库中删 除这个键:
TTL命令和PTTL命令接受一个带有生存时间或者过期时间的键,返回这个键的剩余生 存时间,也就是,返回距离这个键被服务器自动删除还有多长时间:
Redis有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除):
EXPIRE
PEXPIRE
EXPIREAT
PEXPIREAT
虽然有多种不同单位和不同形式的设置命令,但实际上EXPIRE、PEXPIRE、EXPIREAT三个命令都是使用PEXPIREAT命令来实现的:无论客户端执行的是以上四个命令中的哪一 个,经过转换之后,最终的执行效果都和执行PEXPIREAT命令一样。
首先,EXPIRE命令可以转换成PEXPIRE命令:
接着,PEXPIRE命令又可以转换成PEXPIREAT命令:
并且,EXPIREAT命令也可以转换成PEXPIREAT命令:
最终,EXPIRE、PEXPIRE和EXPIREAT三个命令都会 转换成PEXPIREAT命令来执行,如图
RedisDb结构的expires字典保存了数据库中所有键的过期时间,我们称这个字典 为过期字典:
过期字典的键是一个指针,这个指针指向键空间中的某个键对象(也即是某个数据 库键)。
过期字典的值是一个long long类型的整数,这个整数保存了键所指向的数据库 键的过期时间-- 一个毫秒精度的UNIX时间戳。
图展示了一个带有过期字典的数据库例子,在这个例子中,键空间保存了数据库 中的所有键值对,而过期字典则保存了数据库键的过期时间。
注意,为了展示方便,图的键空间和过期字典中重复出现了两次alphabet键对象和book键对象。在实际中,键空间的键和过期字典的键都指向同一个键对象,所以不会出现 任何重复对象,也不会浪费任何空间。
图中的过期字典保存了两个键值对:
第一个键值对的键为alphabet键对象,值为1385877600000, 这表示数据库键 alphabet的过期时间为1385877600000 (2013年12月1日零时)。
第二个键值对的键为book键对象,值为1388556000000, 这表示数据库键book 的过期时间为1388556000000 (2014年1月1日零时)。
当客户端执行PEXPIREAT命令(或者其他三个会转换成PEXPIREAT命令的命令)为一个数据库键设置过期时间时,服务器会在数据库的过期字典中关联给定的数据库键和过期 时间。
举个例子,如果数据库当前的状态如图所示,那么在服务器执行以下命令之后:
过期字典将新增一个键值对,其中键为message键对象,而值则为1331234400000 (2014年2月1日零时),如图
以下是PEXPIREAT命令的伪代码定义
PERSIST命令可以移除一个键的过期时间:
PERSIST命令就是PEXPIREAT命令的反操作: PERSIST命令在过期字典中查找给定的 键,并解除键和值(过期时间)在过期字典中的关联。
举个例子,如果数据库当前的状态如图所示,那么当服务器执行以下命令之后:
可以看到,当PERSIST命令执行之后,过期字典中原来的book键值对消失了,这代 表数据库键book的过期时间已经被移除。
以下是PERSIST命令的伪代码定义:
TTL,命令以秒为单位返回键的剩余生存时间,而PTTL命令则以毫秒为单位返回键的剩 余生存时间:
TTL和PTTL两个命令都是通过计算键的过期时间和当前时间之间的差来实现的,以下 是这两个命令的伪代码实现:
举个例子,对于一个过期时间为1385877600000 (2013年12月1日零时)的键 alphabet来说:
如果当前时间为1383282000000 (2013年11月1日零时),那么对键alphabet 执行PTTL命令将返回2595600000, 这个值是通过用alphabet键的过期时间减 去当前时间计算得出的: 1385877600000 - 1383282000000 = 2595600000。
另一方面,如果当前时间为1383282000000 (2013年11月1日零时),那么对键 alphabet执行TTL命令将返回2595600, 这个值是通过计算alphabet键的过 期时间减去当前时间的差,然后将差值从毫秒转换为秒之后得出的。
通过过期字典,程序可以用以下步骤检查一个给定键是否过期:
1)检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间。
2)检查当前UNIX时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否 则的话,键未过期。
可以用伪代码来描述这一过程:
举个例子,对于一个过期时间为1385877600000 (2013年12月1日零时)的键 alphab吐来说:
如果当前时间为1383282000000 (2013年11月1日零时),那么调用is_ expired(alphabet)将返回False, 因为当前时间小于alphabet键的过期时间。
另一方面,如果当前时间为1385964000000 (2013年12月2日零时),那么调用is_expired(alphabet)将返回True, 因为当前时间大于alphabet键的过期 时间。
注意,实现过期键判定的另一种方法是使用TTL命令或者PTTL命令,比如说,如果对某个键执行TTL命令,并且命令返回的值大于等于0, 那么说明该键未过期。在实际中,Redis检 查键是否过期的方法和is_expired函数所描述的方法一致,因为直接访问字典比执行一 个命令稍微快一些。
我们知道了数据库键的过期时间都保存在过期字典中,又知道了如 何根据过期时间去判断一个键是否过期,现在剩下的问题是:如果一个键过期了,那么它什 么时候会被删除呢?
这个问题有三种可能的答案,它们分别代表了三种不同的删除策略:
定时删除:在设置键的过期时间的同时,创建一个定时器(timer), 让定时器在键的 过期时间来临时,立即执行对键的删除操作。
惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否 过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至 于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
在这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略。
定时删除策略对内存是最友好的:通过使用定时器,定时删除策略可以保证过期键会尽 可能快地被删除,并释放过期键所占用的内存。
另一方面,定时删除策略的缺点是,它对CPU时间是最不友好的:在过期键比较多的 情况下,删除过期键这一行为可能会占用相当一部分CPU时间,在内存不紧张但是CPU时 间非常紧张的情况下,将CPU时间用在删除和当前任务无关的过期键上,无疑会对服务器 的响应时间和吞吐量造成影响。
例如,如果正有大最的命令请求在等待服务器处理,并且服务器当前不缺少内存,那么 服务器应该优先将CPU时间用在处理客户端的命令请求上面,而不是用在删除过期键上面。
除此之外,创建一个定时器需要用到Redis服务器中的时间事件,而当前时间事件的实 现方式——无序链表,查找一个事件的时间复杂度为O(N)--并不能高效地处理大量时间 事件。
因此,要让服务器创建大量的定时器,从而实现定时删除策略,在现阶段来说并不现实。
惰性删除策略对CPU时间来说是最友好的:程序只会在取出键时才对键进行过期检查, 这可以保证删除过期键的操作只会在非做不可的情况下进行,并且删除的目标仅限于当前处 理的键,这个策略不会在删除其他无关的过期键上花费任何CPU时间。
惰性删除策略的缺点是,它对内存是最不友好的:如果一个键已经过期,而这个键又仍 然保留在数据库中,那么只要这个过期键不被删除,它所占用的内存就不会释放。
在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被 访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB), 我们甚至 可以将这种情况看作是一种内存泄漏——无用的垃圾数据占用了大量的内存,而服务器却不 会自己去释放它们,这对于运行状态非常依赖于内存的lledis服务器来说,肯定不是一个好 消息。
举个例子,对于一些和时间有关的数据,比如日志(log), 在某个时间点之后,对它们 的访问就会大大减少,甚至不再访问,如果这类过期数据大量地积压在数据库中,用户以为 服务器已经自动将它们删除了,但实际上这些键仍然存在,而且键所占用的内存也没有释 放,那么造成的后果肯定是非常严重的。
从上面对定时删除和惰性删除的讨论来看,这两种删除方式在单一使用时都有明显的缺陷:
定时删除占用太多CPU时间,影响服务器的响应时间和吞吐量。
惰性删除浪费太多内存,有内存泄漏的危险。 定期删除策略是前两种策略的一种整合和折中:
定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的 时长和频率来减少删除操作对CPU时间的影响。
除此之外,通过定期删除过期键,定期删除策略有效地减少了因为过期键而带来的 内存浪费。
定期删除策略的难点是确定删除操作执行的时长和频率:
如果删除操作执行得太频繁,或者执行的时间太长,定期删除策略就会退化成定时 删除策略,以至于将CPU时间过多地消耗在删除过期键上面。
如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除策 略一样,出现浪费内存的情况。
因此,如果采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作的执行 时长和执行频率。
Redis服 务器实际使用的是惰性删除和定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。
过期键的惰性删除策略由db.c/expireifNeeded函数实现,所有读写数据库的 Redis命令在执行之前都会调用expireifNeeded函数对输人键进行检查:
如果输人键已经过期,那么expireifNeeded函数将输人键从数据库中删除。
如果输人键未过期,那么expireifNeeded函数不做动作。 命令调用expireifNeed.ed函数的过程如图所示。
expireifNeeded函数就像一个过滤器,它可以在命令真正执行之前,过滤掉过期的 输人键,从而避免命令接触到过期键。
另外,因为每个被访问的键都可能因为过期而被expireifNeeded函数删除,所以每 个命令的实现函数都必须能同时处理键存在以及键不存在这两种情况:
当键存在时,命令按照键存在的情况执行。
当键不存在或者键因为过期而被expireifNeeded函数删除时,命令按照键不存 在的情况执行。
举个例子,图展示了GET命令的执行过程,在这个执行过程中,命令需要判断键 是否存在以及键是否过期,然后根据判断来执行合适的动作。
过期键的定期删除策略由redis.c/activeExpireCycle函数实现,每当Redis的 服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字 典中随机检查一部分键的过期时间,并删除其中的过期键。
activeExpireCycle函数的工作模式可以总结如下:
函数每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删 除其中的过期键。
全局变量current_db会记录当前activeExpireCycle函数检查的进度,并 在下一次activeExpireCycle函数调用时,接着上一次的进度进行处理。比如 说,如果当前activeExpireCycle函数在遍历10号数据库时返回了,那么下 次activeExpireCycle函数执行时,将从11号数据库开始查找并删除过期键。
随着activeExpireCycle函数的不断执行,服务器中的所有数据库都会被检 查一遍,这时函数将current_db变量重置为0. 然后再次开始新一轮的检查 工作。
我们将探讨过期键对Redis服务器中其他模块的影响,看看RDB持久化功 能、AOF持久化功能以及复制功能是如何处理数据库中的过期键的。
在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的 键进行检查,已过期的键不会被保存到新创建的RDB文件中。
举个例子,如果数据库中包含三个键kl、k2、k3, 并且k2已经过期,那么当执行 SAVE命令或者BGSAVE命令时,程序只会将kl和k3的数据保存到RDB文件中,而k2 则会被忽略。
因此,数据库中包含过期键不会对生成新的RDB文件造成影响。
在启动Redis服务器时,如果服务器开启了RDB功能,那么服务器将对RDB文件进行 载人:
如果服务器以主服务器模式运行,那么在载人RDB文件时,程序会对文件中保存的 键进行检查,未过期的键会被载人到数据库中,而过期键则会被忽略,所以过期键 对载入RDB文件的主服务器不会造成影响。
如果服务器以从服务器模式运行,那么在载人RDB文件时,文件中保存的所有键, 不论是否过期,都会被载人到数据库中。不过,因为主从服务器在进行数据同步的 时候,从服务器的数据库就会被清空,所以一般来讲,过期键对载人RDB文件的从 服务器也不会造成影响。
举个例子,如果数据库中包含三个键kl、k2、k3, 并且k2已经过期,那么当服务器 启动时:
如果服务器以主服务器模式运行,那么程序只会将kl和k3载人到数据库,k2会 被忽略。
如果服务器以从服务器模式运行,那么kl、k2和k3都会被载人到数据库。
当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被 惰性删除或者定期删除,那么AOF'文件不会因为这个过期键而产生任何影响。
当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加(append)一条DEL 命令,来显式地记录该键已被删除。
举个例子,如果客户端使用GET message命令,试图访问过期的message键,那么
服务器将执行以下三个动作:
1)从数据库中删除message键。
2)追加一条DEL message命令到AOF文件。
3)向执行GET命令的客户端返回空回复。
和生成RDB文件时类似,在执行AOF重写的过程中,程序会对数据库中的键进行检 查,已过期的键不会被保存到重写后的AOF文件中。
举个例子,如果数据库中包含三个键kl、k2、k3, 并且k2已经过期,那么在进行重 写工作时,程序只会对kl和k3进行重写,而k2则会被忽略。
因此,数据库中包含过期键不会对AOF重写造成影响。
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:
主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个DEL命令,告 知从服务器删除这个过期键。
从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而 是继续像处理未过期的键一样来处理过期键。
从服务器只有在接到主服务器发来的DEL命令之后,才会删除过期键。
通过由主服务器来控制从服务器统一地删除过期键,可以保证主从服务器数据的一致性,也正是因为这 个原因,当一个过期键仍然存在于主服务器的数据库 时,这个过期键在从服务器里的复制品也会继续存在。
举个例子,有一对主从服务器,它们的数据库中都保存着同样的三个键message、XXX和yyy, 其中 message为过期键,如图
如果这时有客户端向从服务器发送命令GET message, 那么从服务器将发现message 键巳经过期,但从服务器并不会删除message键,而是继续将message键的值返回给客 户端,就好像message键并没有过期一样,如图
假设在此之后,有客户端向主服务器发送命令GET message, 那么主服务器将发现键 message已经过期:主服务器会删除message键,向客户端返回空回复,并向从服务器 发送DEL message命令,如图所示。
从服务器在接收到主服务器发来的 DEL message命令之后,也会从数据库 中删除message键,在这之后,主从服务 器都不再保存过期键message了,如图
数据库通知是Redis 2.8版本新增加的功能,这个功能可以让客户端通过订阅给定的频 道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。
举个例子,以下代码展示了客户端如何获取0号数据库中针对message键执行的所有 命令:
根据发回的通知显示,先后共有SET、EXPIRE、DEL三个命令对键message进行了 操作。
这一类关注“某个键执行了什么命令”的通知称为键空间通知(key-space notification), 除此之外,还有另一类称为键事件通知(key-event notification)的通知,它们关注的是“某 个命令被什么键执行了"。
以下是一个键事件通知的例子,代码展示了客户端如何获取0号数据库中所有执行 DEL命令的键:
根据发回的通知显示,key、number、message三个键先后执行了DEL命令。 服务器配置的notify-keyspace-events选项决定了服务器所发送通知的类型:
想让服务器发送所有类型的键空间通知和键事件通知,可以将选项的值设置为AKE。
想让服务器发送所有类型的键空间通知,可以将选项的值设置为AK。
想让服务器发送所有类型的键事件通知,可以将选项的值设置为AE。
想让服务器只发送和字符串键有关的键空间通知,可以将选项的值设置为K$。
想让服务器只发送和列表键有关的键事件通知,可以将选项的值设置为El。
关于数据库通知功能的详细用法,以及notify-keyspace-events选项的更多设 置,Redis的官方文档已经做了很详细的介绍,这里不再赘述。
发送数据库通知的功能是由notify.c/notifyKeyspaceEvent函数实现的:
函数的type参数是当前想要发送的通知的类型,程序会根据这个值来判断通知是否 就是服务器配置notify-keyspace-events选项所选定的通知类型,从而决定是否发 送通知。
event、keys和dbid分别是事件的名称、产生事件的键,以及产生事件的数据库号 码,函数会根据type参数以及这三个参数来构建事件通知的内容,以及接收通知的频道名。
每当一个Redis命令需要发送数据库通知的时候,该命令的实现函数就会调用notifyKeyspaceEvent函数,并向函数传递传递该命令所引发的事件的相关信息。
例如,以下是SADD命令的实现函数saddCommand的其中一部分代码:
当SADIJ命令至少成功地向集合添加了一个集合元素之后,命令就会发送通知,该通知 的类型为REDIS_NOTIFY_SET (表示这是一个集合键通知),名称为sadd (表示这是执 行SADIJ命令所产生的通知)。
其他发送通知的函数调用notifyKeyspaceEvent函数的方式也和saddCommand、 delCommand类似,只是给定的参数不同,接下来我们来看看notifyKeyspaceEvent 函数的实现。
以下是notifyKeyspaceEvent函数的伪代码实现:
notifyKeyspaceEvent函数执行以下操作:
1) server.notify_ keys pace_ events属性就是服务器配置notify-keyspaceevents选项所设置的值,如果给定的通知类型type不是服务器允许发送的通知类型,那 么函数会直接返回,不做任何动作。
2)如果给定的通知是服务器允许发送的通知,那么下一步函数会检测服务器是否允许 发送键空间通知,如果允许的话,程序就会构建并发送事件通知。
3)最后,函数检测服务器是否允许发送键事件通知,如果允许的话,程序就会构建并 发送事件通知。
另外,pubsubPublishMessage函数是PUBLISH命令的实现函数,执行这个函数 等同于执行PUBLISH命令,订阅数据库通知的客户端收到的信息就是由这个函数发出的。
Redis服务器的所有数据库都保存在redisServer.db数组中,而数据库的数量则 由redisServer.dbnum属性保存。
客户端通过修改目标数据库指针,让它指向redisServer.db数组中的不同元素 来切换不同的数据库。
数据库主要由dict和expires两个字典构成,其中dict字典负责保存键值对, 而expires字典则负责保存键的过期时间。
因为数据库由字典构成,所以对数据库的操作都是建立在字典操作之上的。
数据库的键总是一个字符串对象,而值则可以是任意一种Redis对象类型,包括字 符串对象、哈希表对象、集合对象、列表对象和有序集合对象,分别对应字符串键、 哈希表键、集合键、列表键和有序集合键。
expires字典的键指向数据库中的某个键,而值则记录了数据库键的过期时间,过 期时间是一个以毫秒为单位的UNIX时间戳。
Redis使用惰性删除和定期删除两种策略来删除过期的键:惰性删除策略只在碰到过 期键时才进行删除操作,定期删除策略则每隔一段时间主动查找并删除过期键。
执行SAVE命令或者BGSAVE命令所产生的新RDB文件不会包含已经过期的键。
执行BGREWRITEAOF命令所产生的重写AOF文件不会包含已经过期的键。
当一个过期键被删除之后,服务器会追加一条DEL命令到现有AOF文件的末尾, 显式地删除过期键。
当主服务器删除一个过期键之后,它会向所有从服务器发送一条DEL命令,显式地 删除过期键。
从服务器即使发现过期键也不会自作主张地删除它,而是等待主节点发来DEL命令, 这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。
当Redis命令对数据库进行修改之后,服务器会根据配置向客户端发送数据库通知。