redis服务器会将所有的数据库保存在redisServer结构的db数组中,每一个redisDb结构代表一个数据库。如下:
struct redisServer {
...
redisDb *db; //一个数组,保存服务器中所有的数据库
...
int dbnum; //服务器中的数据库数量
);
1. 切换数据库
当需要切换目标数据库时,可以使用SELECT命令。而在客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针:
typedef struct redisClient {
...
//记录客户端当前正在使用的数据库
redisDb *db;
...
} redisClient;
这个指针redisClient.db就是指想redisServer.db数组的其中一个元素,而被指向的元素就是客户端的目标数据库。如图所示,客户端此时使用的是数据库2,所以它里面的db指针指向了redisServer.db[2]。
当然有时候我们在编写程序时不会像命令行那样会提示正在使用哪个数据库,所以最好是在操作redis数据库前先执行一次SELECT命令,确保使用的是目标数据库。
2. 数据库键空间
上面说到每个数据库都是一个redisDb结构,而redisDb结构的dict字典保存了数据库中所有键值对,我们将这个字典称为键空间,它和用户所见的数据库是直接对应的:
typedef struct redisDb {
...
//数据库键空间,保存数据库中所有的键值对
dict *dict;
...
} redisDb;
当用户对数据库进行操作时,实质上是在对键空间字典进行操作的。例如添加新键、删除键、更新键等。如图所示是dict字典的结构
在读写键时还会执行一些维护操作,包括:
- 读取一个键后,服务器会根据键是否存在来更新服务器的键空间命中(hit)次数或键空间不命中(miss)次数
- 读取一个键后,服务器会更新键的LRU(最后一次使用)时间,它可以用于计算键的闲置时间。
- 若服务器在读取一个键时发现该键已经过期,那么服务器会先删除这个过期键
- 若有客户端使用WATCH命令监视某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty)。
- 若服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知。
3. 设置键的生存时间或过期时间
设置生存时间可以使用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
127.0.0.1:6379> get key //5秒内
"value"
127.0.0.1:6379> get key //5秒后
(nil)
设置过期时间可以使用EXPIREAT命令或者PEXPIREAT命令,以秒或者毫秒精度给数据库中某个键设置过期时间,过期时间是一个UNIX时间戳。键的过期时间来临时,服务器自动从数据库中删除这个键。
127.0.0.1:6379> set key value
OK
127.0.0.1:6379> EXPIREAT key 1573569782
(integer) 1
127.0.0.1:6379> get key
"value"
127.0.0.1:6379> time
1) "1573569708"
2) "836442"
127.0.0.1:6379> get key //1573569782以前
"value"
127.0.0.1:6379> get key //1573569782以后
(nil)
TTL命令和PTTL命令接受一个带有生存时间或者过期时间的键,返回这个键的剩余生存时间。
127.0.0.1:6379> set key value
OK
127.0.0.1:6379> expire key 1000
(integer) 1
127.0.0.1:6379> TTL key
(integer) 996
127.0.0.1:6379> PTTL key
(integer) 938743
3.1 设置过期时间
设置生存时间或者过期时间有四个命令:
- EXPIRE <key> <ttl>:用于将键key的生存时间设置为ttl秒。
- PEXPIRE <key> <ttl>:用于将键key的生存时间设置为ttl毫秒。
- EXPIREAT <key> <timestamp>:用于将键key的过期时间设置为timestamp所指定的秒数时间戳。
- PEXPIREAT <key> <timestamp>:用于将键key的过期时间设置为timestamp所指定的毫秒数时间戳。
当然其实他们都是用最后一个命令PEXPIREAT实现的。
3.2 保存过期时间
redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称之为过期字典,其中
- 过期字典的键是一个指针,它指向键空间的某个键对象
- 过期字典的值是一个long long类型的整数,保存了键所指向的数据库键的过期时间—一个毫秒精度的UNIX时间戳
typedef struct redisDb {
...
//过期字典,保存键的过期时间
dict *expires;
...
} redisDb;
图中键空间和过期字典的键都指向同一个键对象,所以不会出现重复对象。当执行PEXPIREAT命令时,便会对过期字典进行修改。
3.3 移除过期时间
可以使用PERSIST命令移除一个键的过期时间,它和PEXPIREAT命令是相反的。会在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联。
127.0.0.1:6379> SET message value
OK
127.0.0.1:6379> time
1) "1573608244"
2) "886893"
127.0.0.1:6379> PEXPIRE message 1573608278 //设置过期时间
(integer) 1
127.0.0.1:6379> TTL message
(integer) 1573603
127.0.0.1:6379> PERSIST message //移除过期时间
(integer) 1
127.0.0.1:6379> TTL message
(integer) -1
3.4 计算并返回剩余生存时间
TTL以秒为单位返回键的剩余生存时间,而PTTL命令则以毫秒为单位返回键的剩余生存时间。他们两个都是通过计算键的过期时间和当前时间之间的差来实现的。
3.5 过期键的判定
一般是通过直接访问过期字典来判断一个键是否过期,主要有下面两个步骤:
- 检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间。
- 检查当前UNIX时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则键未过期。
3. 过期键删除策略
知道如何判断一个键是否过期后,那么什么时候要删除这些过期键呢,有三种删除策略:
- 定时删除:设置一个键的同时创建一个定时器,让定时器在键的过期时间来临时立即执行对键的删除。此策略对内存是友好的,因为保证过期的键不会长时间停留在内存中,但对CPU是不友好的,需要循环使用CPU检查自己是否过期。
- 惰性删除:不管过期键,但每次从键空间中获取键时都检查取得的键是否过期,如果过期就删除,否则返回该键。它对CPU是友好的,但对内存不友好。如果键都过期了,但还是一直没访问它,那它会常驻内存,甚至发生内存泄漏。
- 定期删除:每隔一段时间就对数据库进行一次检查,删除里面的过期键。综合了上述两者的优点,当然删除操作执行的时长和频率比较难确定。
4. Redis的过期键删除策略
redis里面使用的是惰性删除和定期删除两种策略。
惰性删除
过期键的惰性删除策略由db.c/expireIfNeeded函数实现,所有读写数据库的Redis命令在执行之前都会调用expiredIfNeeded函数对输入键进行检查:
- 若输入键已经过期,则expireIfNeeded函数将输入键从数据库中删除
- 若输入键未过期,那么expireIfNeeded函数不做动作
当然正式因为每个被访问的键都可能因为过期而被expireIfNeeded函数删除,所以每个命令的实现函数都必须能同时处理键存在以及键不存在两种情况:
- 键存在时,命令将按照键存在的情况执行。
- 键不存在或者键因为过期而被expireIfNeeded函数删除时,命令按照键不存在的情况执行。
定期删除
定期删除策略由redis.c/activeExpireCycle函数实现,每当Redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定时间内分多次遍历服务器中的各个数据库,从数据库的expire字典中随机检查一部分键的过期时间,并删除其中的过期键。activeExpireCycle函数执行具体如下:
- 每次从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。
- 全局变量current_db会记录当前activeExpireCycle函数检查的进度(检查到第几个数据库了),并在下一次activeExpireCycle函数调用时接着上一次的进度进行处理。
- 随着activeExpireCycle函数不断执行,服务器中所有的数据库都会被检查以便,==这时函数将current_db变量重置为0,然后再次开始新一轮的检查工作。
5. AOF、RDB和复制功能对过期键的处理
生成RDB文件
执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。
载入RDB文件
- 如果服务器以主服务器模式运行,那么在载入RDB文件时,程序会对文件中保存的键进行检查,未过期的键会被载入到数据库中,而过期键则会忽略,过期键不会对载入RDB文件的主服务器造成影响。
- 如果服务器以从服务器模式运行,载入RDB文件时,文件中保存的所有键不论是否过期,都会被载入数据库中。不过当主从服务器间进行同步时,从服务器的数据库会被清空,所以实质上过期键也不会对从服务器造成影响。
AOF文件写入
当某个过期键还未被惰性删除或者定期删除时,AOF文件不会因为这个过期键而产生任何影响。党国期间被惰性删除或者定时删除时,程序会向AOF文件追加(append)一条DEL命令,来显示地记录该键已被删除。加入某个键message过期了,现在有一个GET命令,则服务器的动作如下:
- 从数据库中删除message键
- 在AOF文件中追加一条DEL message命令
- 向执行GET命令的客户端返回空回复。
AOF重写
在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。
复制
服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制。
- 当主服务器在删除一个过期键之后,会显示地向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键。
- 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续想处理未过期的键一样来处理过期键。
- 从服务器只有在接收到主服务器发来的DEL命令时,才会删除过期键。
这也就会导致当一个过期键仍然处于主服务器的数据库时,这个过期键在从服务器的复制品也会继续存在。只有当客户端访问主服务器时,主服务器发现键过期了,显示地发送DEL命令给从服务器,那从服务器才会删除过期键。