目录
1 服务器中的数据库
Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构体的db数组中,db数组的每个项都是一个redis.h/redisDb结构体,每个redisDb结构体代表一个数据库
redis.h
struct redisServer {
……
//一个数组,保存着服务器中所有数据库
redisDb *db;
//服务器的数据库数量
int dbnum;
……
};
dbnum属性由服务器配置的database选项决定,默认情况下,该选项的值为16,所以Redis服务器默认会创建16个数据库,如图1-1所示:
2 切换数据库
Redis客户端都有自己的目标数据库,每当客户端执行数据库写命令或者数据库读命令的时候,目标数据库就会成为这些命令的操作对象。默认情况下,Redis客户端的目标数据库为0号数据库,但客户端可以通过执行SELECT命令来切换目标数据库。
127.0.0.1:6379> SET msg "hello world"
OK
127.0.0.1:6379> GET msg
"hello world"
127.0.0.1:6379> SELECT 2
OK
127.0.0.1:6379[2]> GET msg
(nil)
127.0.0.1:6379[2]> SET msg "another world"
OK
127.0.0.1:6379[2]> GET msg
"another world"
在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针:
redis.h
typedef struct redisClient {
……
//记录客户端当前正在使用的数据库
redisDb *db;
……
} redisClient;
redisClient.db指针指向redisServer.db数组的其中一个元素,而被指向的元素就是客户端你的目标数据库。如果某个客户端的目标数据库为1号数据库,那么这个客户端所对应的客户端状态和服务器状态之间的关系如图2-1所示。
3 数据库的键空间
Redis是一个键值对数据库服务器,服务器中的每个数据库都由redis.h/redisDb结构体表示,其中,redisDb结构体的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间。
redis.h
typedef struct redisDb {
//数据库键空间,保存着数据库中所有键值对
dict *dict;
……
} redisDb;
键空间和用户所见的数据库是直接对应的:
- 键空间的键也就是数据库的键,每个键都是一个字符串对象。
- 键空间的值也就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中任意一种Redis对象、集合对象和有序集合对象中的任意一种Redis对象。
3.1 读写键空间的维护操作
当使用Redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作,其中包括:
- 在读取一个键之后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中次数或键空间不命中次数,这两个值可以在INFO stats命令的keyspace_hits属性和keyspace_misses属性中查看
- 在读取一个键之后,服务器会更新键的LRU(最后一次使用)时间,这个值可以用于计算键的闲置时间,使用OBJECT idletime<key>命令可以查看键key的闲置时间
- 如果服务器在读取一个键时发现该键已过期,那么服务器会先删除这个过期键,然后才执行余下的其他操作
- 如果客户端使用WATCH命令监视某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏,从而让事物程序注意到这个键已经被修改了
- 服务器每次修改一个键之后,都会对脏键计数器的值加1,这个计数器会触发服务器的持久化以及复制操作
- 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知
4 设置键的生存时间
Redis有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候会被删除):
- EXPIRE <key> <ttl>命令用于将键key的生存时间设置为ttl秒
- PEXPIRE <key> <ttl>命令用于将键key的生存时间设置为ttl毫秒
- EXPIREAT <key> <timestamp>命令用于将键key的过期时间设置为timestamp所指定的秒数时间戳
- PEXPIREAT <key> <timestamp>命令用于将键key的过期时间设置为timestamp所指定的毫秒数时间戳
4.1 保存过期时间
redisDb结构体的expires字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典:
- 过期字典的键是一个指针,这个指针指向键空间中的某个键对象(也即是某个数据库键)
- 过期字典的值是一个long long类型的整数,这个整数保存了键所指定的数据库键的过期时间——一个毫秒精度的Unix时间戳
redis.h
typedef struct redisDb {
……
//过期字典,保存着键的过期时间
dict *expires;
……
} redisDb;
4.2 过期判定
通过过期字典,程序可以用以下步骤检查一个给定键是否过期:
- 检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间
- 检查当前Unix时间戳是否大于键的过期时间:如果是的话,那么键已过期,否则键未过期
可以用伪代码来描述这一过程:
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
5 过期删除策略
redis主要有三种删除策略:
- 定时删除:在设置键的过期时间的同时,创建一个定时器,让键的过期时间来临时,立即执行对键的删除操作(不常用)
- 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期。如果过期的话,就删除该键;如果没有过期,就返回该键
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定
5.1 惰性删除策略的实现
过期键的惰性删除策略由db.c/expireIfNeeded函数实现,所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查:
- 如果输入键已经过期,那么expireIfNeeded函数将输入键从数据库中删除
- 如果输入键未过期,那么expireIfNeeded函数不做任何动作
5.2 定期删除策略的实现
过期键的定期删除策略由redis.c/activeExpireCycle函数实现,每当Redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。activeExpireCycle函数的工作模式可以总结如下:
- 函数每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键
- 全局变量current_db会记录当前activeExpireCycle函数检查的进度,并在下一次activeExpireCycle函数调用时,接着上一次的进度进行处理。比如说,如果当前activeExpireCycle函数在遍历10号数据库时返回了,那么下次activeExpireCycle函数执行时,将从11号数据库开始查找并删除过期键
- 随着activeExpireCycle函数的不断执行,服务器中的所有数据库都会被检查一遍,这时函数将current_db变量重置为0,然后再次开始新一轮的检查工作
6 AOF、RDB和复制功能对过期键的处理
6.1 生成RDB文件
在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。举个栗子,如果数据库中包含三个键k1、k2、k3,并且k2已经过期,那么当执行SAVE命令或者BGSAVE命令时,程序只会将k1和k3的数据保存到RDB文件中,而k2则会被忽略。因此,数据库中包含过期键不会对生成新的RDB文件造成影响。
6.2 载入RDB文件
在启动Redis服务器时,如果服务器开启了RDB功能,而且数据库中包含三个键k1、k2、k3,并且k2已经过期,那么当服务器启动时:
- 如果服务器以主服务器模式运行,首先对文件中保存的键进行检查,那么程序只会将k1、k3载入到数据库,k2会被忽略
- 如果服务器以从服务器模式运行,主从同步会清空从服务器的数据库,过期也无影响,那么k1、k2、k3都会被载入到数据库
6.3 AOF文件写入
当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或定期删除,那么AOF文件不会因为这个过期键而产生任何影响。当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加(append)一条DEL命令,来显示地记录该键已被删除。举个栗子,如果客户端使用GET message命令,试图访问过期的message键,那么服务器将执行以下三个动作:
- 从数据库中删除message键
- 追加一条DEL message命令到AOF文件
- 向执行GET命令的客户端返回空回复
6.4 AOF重写
和生成RDB文件类似,在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。举个栗子,如果数据库包含三个键k1、k2、k3,并且k2已经过期,那么在进行重写工作时,程序只会对k1、k3进行重写,而k2则会被忽略。因此,数据库中包含过期键不会对AOF重写造成影响。
6.5 复制
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:
- 主服务器在删除一个过期键之后,会显示地向所有服务器发送一个DEL命令,告知从服务器删除这个过期键
- 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像未过期键一样来处理过期键
- 从服务器只有在接收到主服务器发来的DEL命令之后,才会删除过期键