1. 服务器中的数据库与键空间:

Redis中使用 redisServer 结构体表示一个服务器,一个服务器中可以包含多个数据库,使用 dbnum 表示,默认Redis服务器会创建 16个 数据库,保存在 redisDb*db 数组中:
(使用 SELECT 命令切换数据库,如: redis> SELECT 2

//redisServer 结构体表示 Redis服务器:
struct redisServer {
	redisDb 	*db;	//一个数组,保存着服务器中的所有数据库
	int			dbnum;	//服务器中的数据库数量
}

//redisDb 结构体表示 Redis数据库:
typedef struct redisDb {
	dict 		*dict;
};

由于Redis是一个键值对数据库(key-value),所以用一个 dict 类型保存数据库中的所有键值对,将 dict 这个字典称为“键空间”(key space)。


2. 设置键的生存时间和过期时间:

2.1 EPXIRE 命令:

客户端可以通过 EXPIRE 命令 设置数据库中某个键的生存时间(以 秒 为单位),到期后服务器会自动删除生存时间为0的键。(PEXPIRE以 毫秒 为单位)

redis> SET key value
OK

redis> EXPIRE key 5		//设置键的生存时间
(integer) 1

redis> GET key			//6秒之内
"value"

redis> GET key			//5秒之后
(nil)


2.2 EXPIREAT 命令:

设置某个键在某个时间点过期:
同理,PEXPIREAT 则是以 毫秒 为单位设置键的过期时间点。

redis> SET key value
OK

redis> EXPIREAT key 1377257300
(integer) 1
redis> TIME
1)“137725700“		//REDIS中的TIME命令可用于查询系统时间,通过比较这个时间值,可以知道键是否已经过期
2)"761687

总结:
Redis有四个不同的命令可以用于设置键的生存时间或过期时间:

EXPIRE		<key>	<ttl>
PEXPIRE		<key>	<ttl>			//将键的生存时间设置为 TTL 秒 或 毫秒

EXPIREAT	<key>	<timestamp>
PEXPIREAT	<key>	<timestamp>		//将键的过期时间设置为 timestamp 所指定的时间戳

而实际上不管使用哪种命令,最后都会转换为 PEXPIREAT 来实现。

typedef struct redisDb {
	dict 	*dict;		//键空间
	dict	*expires;	//过期字典,保存着键的过期时间
};

当客户端执行 PEXPIREAT 命令为一个数据库键设置过期时间时,服务器就会在 *expires 过期字典中加入数据库键及其过期时间。

2.3 PERSIST 命令:

PERSIST 命令是 PEXPIREAT 命令的逆操作,用于移除一个键的过期时间:

redis> PEXPIREAT message 1391234400000
(integer) 1

redis> TTL message
(integer) 13893281

redis> PERSIST message
(integer) 1

redis> TIL message
(integer) -1			//TTL命令 可用于查看键的剩余生存时间,-1 表示 无限长

2.4 TTL 命令:

TTL命令 用于查询键的剩余生存时间,以 秒 为单位;
PTTL命令 则以 毫秒 为单位返回键的剩余生存时间。


3. 数据库对过期键的删除策略:

对于判断某个键是否过期的步骤,首先检查给定键是否存在于 *expires 过期字典中,如果存在则取得该键的过期时间点,再与Unix系统时间进行比对。

如果键已经过期,则需要对其进行删除。

有三种对过期键的删除策略:

(注意只是三种可供数据库采用的删除策略,并不全是Redis使用的删除策略)

(1)定时删除:

在设置键的过期时间的同时,创建一个定时器,定时器到期则立即执行删除操作;

定时删除的优点是及时释放,最大限度的避免过期键对内存的占用,节省内存;
缺点是浪费CPU,当过期键较多时可能会占用相当一部分的CPU时间,导致影响到服务器的性能(响应时间和吞吐量);
对于Redis来说,Redis中的时间事件实现方式是一个无序链表,查找一个事件的时间复杂度是O(n),如果创建大量的定时器显然是不现实的。(时间轮是怎么实现的?)

(2)惰性删除:

对过期的键放任不管,只有当需要访问此键时,才检查是否已过期;
过期则删除,未过期则正常访问;

惰性删除的优点是节省CPU;
缺点是浪费内存甚至相当于内存泄漏,如果一个过期键始终没有被访问,那么它将永远不会被删除。
而大量的过期键也会消耗大量的数据库的内存。

(3)定期删除:

每隔一段时间,程序对数据库进行一次检查,删除里面的过期键。
同时通过算法制定每次执行删除时要删除的数量以及要对多少个数据库进行检查。

由于定时删除浪费CPU,惰性删除浪费内存,定期删除是二者的折中。

定期删除的难点在于如何确定一个合适的删除频率和每次执行删除的时长:
太频繁或者执行时间太长则浪费CPU;
太少或执行时间太短则浪费内存。

因此服务器必须根据实际情况设置合理的定期删除的执行时长和执行频率。

在这三种删除策略中,定时删除和定期删除是 主动删除策略,惰性删除是 被动删除策略。

4. Redis对过期键的删除策略:

Redis实际使用的是 惰性删除 和 定期删除 两种策略,通过配合这两种策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。

4.1 惰性删除策略的实现:

所有读写数据库的Redis命令在执行前,都会先调用 db.c/expirelfNeeded 函数对输入的键进行检查:

(1)如果键已过期,则 expireIfNeeded 函数会将其删除;

(2)如果键未过期,则 expirelfNeedde 函数不做任何操作,对键可以正常访问。

具体操作是:

先查找键是否存在于 redisDb->dict *expires 过期键字典中,如果存在,则取出键的过期时间when,与当前时间now进行比较。

4.2 定期删除策略的实现:

在Redis的时间事件函数 redis.c/serverCron 函数中会调用负责定期删除操作的函数: redis.c/activeExpireCycle

activeExpireCycle 函数会去 redisDb->dict *epxires 过期键字典中取出一部分键,判断其是否过期,如果过期则进行删除,每删除一个键就判断一次本轮删除已经到了时间限制(if reach_time_limit()),如果时间到了就退出,等待下次继续删除。

5. AOF、RDB和复制功能对过期键的处理:

5.1 RDB文件对过期键的处理:

生成RDB文件:

在执行 SAVE 或者 BGSAVE 命令创建一个新的 RDB文件 时,Redis会先对数据库中的键进行检查,对于已过期的键不会被保存到新创建的RDB文件中。

载入RDB文件:

如果服务器是主服务器,则Redis会对RDB文件中保存的键进行检查,如果已过期则忽略,只载入未过期的键;
如果服务器是从服务器,则不论是否过期,载入RDB文件中的所有键。

因为主从复制的时候 从服务器的数据库会被清空,所以载入过期键对从服务器没有影响。

总之,先检查,再载入/删除。

5.2 AOF文件对过期键的处理:

已经过期的键不会对AOF文件造成影响,AOF文件在写入或重写时,会先对键进行检查是否过期,对于已经过期的键不会被写入到AOF文件中;

如果键在写入AOF文件之后过期,则服务器在删除键之后,还写向AOF文件中写入一条命令 DEL Message,用于显式的记录该键已被删除。


6. 主从模式下对过期键的处理:

(1)主服务器在删除一个过期键之后,会显式的向所有从服务器发送一个 DEL 命令,告知从服务器删除这个过期键;

(2)从服务器不会主动的删除过期键,当有客户端发送读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期键一样来处理;

(3)从服务器只有在收到主服务器发来的 DEL 命令之后,才会删除过期键。