Redis设计与实现第9章 -- 数据库 总结(键空间 过期策略 过期键的影响)
9.1 服务器中的数据库
Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer
结构的db
数组里,db
数组的每一项都是一个redis.h/redisDb
结构,每个redisDb
结构代表一个数据库
struct redisServer
//一个数组,保存着服务器中的所有数据库
redisDb *db;
};
初始化服务器的时候,程序会根据服务器状态的dbnum
属性来决定应该创建多少个数据库,该属性的值由服务器配置的database
选项决定,默认情况下为16
9.2 切换数据库
每个Redis
客户端都有自己的目标数据库,每当客户端执行数据库读写命令的时候,目标数据库就会成为操作对象。
默认情况下,目标数据库是0
号数据库,客户端使用SELECT num
命令来切换到num
数据库
在服务器内部,客户端状态redisClient
结构的db
属性记录了客户端当前的目标数据库,这是一个指向redisDb
结构的指针
typedef struct redisClient{
//记录客户端当前正在使用的数据库
redisDb *db;
}redisClient;
redisClient.db
指针指向redisServer.db
数组的其中一个元素,这个元素就是客户端的目标数据库
通过修改redisClient.db
指针,让它指向服务器中的不同数据库,从而实现切换目标数据库的功能,这也就是SELECT
命令的实现原理
注意:
Redis没有可以返回客户端目标数据库的命令,为了避免误操作,最好先执行
SELECT
命令显式地切换到指定的数据库
9.3 数据库键空间
Redis
是一个键值对数据库服务器,服务器中的每个数据库都由一个redis.h/redisDb
结构表示,其中,dict
字典保存了数据库里的所有键值对
typedef struct redisDb(
//数据库键空间,保存着数据库中的所有键值对
dict *dict;
} redisDb;
这个字典就叫做键空间
-
键空间的键就是数据库的键,每个键都是一个字符串对象
-
键空间的值就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中的任意一种
Redis
对象
对数据库的操作,都是通过对键空间字典进行操作来实现的
9.3.1 添加新键
实际上是将一个新键值对添加到键空间字典里面,键是字符串对象,值是任意一种类型的Redis
对象
举个例子,在执行以下命令之后:
redis>sET date"2013.12.1"
OK
键空间将添加一个新的键值对,这个新键值对的键是一个包含字符串"date"的字符串对象,而键值对的值则是一个包含字符串"2013.12.1"的字符串对象,如下图所示。
9.3.2 删除键
删除数据库的一个键,实际上是在键空间里删除键对应的键值对对象
9.3.3 更新键
对键空间里键所对应的值对象进行更新,根据值对象的类型不同,更新的具体方法也不同
9.3.4 对键取值
在键空间里取出键所对应的值对象,根据值的类型不同,具体的取值方法也不同。
9.3.5 其他
其他的命令也是通过对键空间进行处理来完成的,比如清空整个数据库的FLUSHDB
命令,就是通过删除键空间的所有键值对实现的
9.3.6 读写键空间时的维护操作
当对数据库进行读写时,还需要执行一些额外的维护操作,比如
-
读取一个键之后,服务器会根据键是否存在来更新服务器的键空间命中
hit
次数或键空间不命中miss
次数,可以通过INFO stats
命令的keyspace_hits
属性和keyspace_misses
属性查看 -
读取一个键以后,服务器会更新键的
LRU
最后一次使用时间,这个值可以用于计算键的闲置时间,使用OBJECT idletime <key>
命令可以查看键key
的闲置时间 -
读取一个键时发现该键已经过期,服务器会先删除这个过期键
-
如果有客户端使用
WATCH
命令监视了这个键,服务器在修改了被监视的键之后,**会把键标记为脏dity**
,让事务程序注意到这个键已经被修改过 -
服务器每次修改一个键之后,都会对脏
dity
键计数器的值增加1,这个计数器会触发服务器的持久化以及复制操作 -
数据库通知功能
9.4 设置键的生存时间或过期时间
使用EXPIRE/PEXPIRE
命令,可以以秒/毫秒精度为数据库中的某个键设置生存时间TTL,在经过指定的时间后,服务器就会自动删除生存时间为0的键
注意:
SETEX
命令可以设置一个字符串键的同时设置过期时间,只用于字符串键
客户端还可以通过EXPIREAT/PEXPIREAT
命令,以秒/毫秒精度为某个键设置过期时间
过期时间是一个UNIX
时间戳
TTL/PTTL
命令接受一个带有生存时间或过期时间的键,返回这个键的剩余生存时间
9.4.1 设置过期时间
实际上有4个命令可以设置
-
EXPIRE <key> <ttl>
将键key
的生存时间设置为ttl
秒 -
PEXPIRE <key> <ttl>
将键key
的生存时间设置为ttl
毫秒 -
EXPIREAT <key> <timestamp>
将键key
的生存时间过期时间为timestamp
所指定的秒级时间戳 -
PEXPIREAT <key> <timestamp>
将键key
的生存时间过期时间为timestamp
所指定的毫秒级时间戳
实际上都是用PEXPIREAT命令实现的
9.4.2 保存过期时间
redisDb
结构的expires
字典保存了数据库里所有键的过期时间,称这个字典为过期字典:
-
过期字典的键是一个指针,指向键空间的某个键对象
-
过期字典的值是一个
long long
类型的整数,保存了过期时间,毫秒精度的UNIX
时间戳
PEXPIREAT
命令的伪代码如下:
def PEXPIREAT(key,expire_time_in_ms):
#如果给定的键不存在于键空间,那么不能设置过期时间
if key not in redisDb.dict:
return 0
#在过期字典中关联键和过期时间
redisDb.expires[key]=expire_time_in_ms
#过期时间设置成功
return 1
9.4.3 移除过期时间
PERSIST
命令可以移除一个键的过期时间,相当于PEXPIREAT
的反操作:在过期字典里查找给定的键,解除键和值的关联
def PERSIST(key):
#如果给定的键不存在于键空间,那么不能设置过期时间
if key not in redisDb.dict:
return 0
#移除
redisDb.expires.remove(key)
return 1
9.4.4 计算并返回剩余生存时间
TTL/PTTL
以秒/毫秒为单位返回键的剩余生存时间,是通过计算键的过期时间和当前时间之间的差来实现的
返回值里,-2
表示不存在,-1
表示永久有效
def PTTL(key):
#键不存在于数据库
if key not in redisDb.dict:
return -2
# 尝试取得键的过期时间#
#如果键没有设置过期时间,那么expire_time_in_ms将为None
expire_time_in_ms = redisDb.expires.get(key)
#键没有设置过期时间
if expire_time_in_ms is None:
return -1
#获得当前时间
now_ms =get_current_unix_timestamp_in_ms()
#过期时间减去当前时间,得出的差就是键的剩余生存时间
return(expire_time_in_ms - now_ms )
def TTL(key):
#获取以毫秒为单位的剩余生存时间
ttl_in_ms = PTTL(key)
if ttl_in_ms <0:
#处理返回值为-2和-1的情况
return ttl_in_ms
else:
#将毫秒转换为秒
return ms_to_sec(ttl_in_ms)
9.4.5 过期键的判定
检查过期字典,程序通过下面步骤来检查一个给定的键是否过期
-
检查给定键是否存在于过期字典:如果存在,取得键的过期时间
-
检查当前
UNIX
时间戳是否大于键的过期时间:是的话,键过期
def is expired(key):
#取得键的过期时间
expire_time_in_ms=redisDb.expires.get(key)
#键没有设置过期时间
if expire_time_in_ms is None:
return False
#取得当前时间的UNIX时间戳
now_ms=get_current_unix_timestamp_in_ms()
#检查当前时间是否大于键的过期时间
if now_ms>expire_time_in_ms :
#是,键已经过期
return True
else:
#否,键未过期
return False
9.5 过期删除策略
如果一个键过期了,什么时候会被删除?有三种不同的删除策略:
-
定时删除:设置键的过期时间的同时创建一个定时器
timer
,让定时器在键的过期时间来临时立即执行对键的删除操作 -
惰性删除:放任键过期不管,但是每次从键空间获取键的时候,都检查取得的键是否过期;过期的话删除,不过期返回
-
定期删除:每隔一段时间,程序对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及检查多少个数据库由算法决定。
1和3为主动删除策略,2是被动删除策略
9.5.1 定时删除
对内存友好,过期键会尽可能快的被删除并释放内存
但是对CPU时间最不友好,在过期键比较多的情况下,删除过期键这一行为可能会占用相当一部分CPU时间,对服务器的响应时间和吞吐量造成影响。
另外,创建一个定时器需要用到Redis服务器的时间事件,实现方式为无序链表,查找一个时间复杂度为O(N)
9.5.2 惰性删除
对CPU时间最友好,保证了删除过期键的操作只会在非做不可的情况下进行,而且不会再删除其他无关的过期键上花费任何CPU时间。
但是对内存最不友好,过期键占用的内存一直不会释放,甚至可以看作内存泄漏 – 无用的垃圾数据占用了大量内存
9.5.3 定期删除
是前两种策略的整合和折中
-
每个一段时间执行一次删除过期键操作,通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响
-
有效的减少了因为过期键带来的内存浪费
难点是确定删除操作执行的时长和频率
-
执行太频繁或执行时间太长,就会退化为定时删除策略
-
执行太少或执行时间太短,和惰性策略一样出现了浪费内存的情况
9.6 Redis的过期键删除策略
Redis
服务器实际上使用的是惰性删除和定期删除两种策略,在合理使用CPU时间和避免浪费内存空间之间取得平衡
9.6.1 惰性删除策略的实现
由db.c/expireIfNeeded
函数实现,所有读写数据库的Redis
命令在执行前都会调用该函数对输入键进行检查
-
如果输入键过期,该函数将输入键从数据库里删除
-
输入键未过期,不做操作
每个命令的实现函数要同时处理键存在和键不存在两种情况
9.6.2 定期删除策略的实现
由redis.c/activeExpireCycle
函数实现,每当Redis
的服务器周期性操作redis.c/serverCron
函数执行时,activeExpireCycle
函数就会被调用,在规定的时间里,分多次遍历服务器中的各个数据库,从数据库的expires
字典里随机检查一部分键的过期时间,并删除其中的过期键。
伪代码如下:
#默认每次检查的数据库数量
DEFAULT_DB_NUMBERS = 16
#默认每个数据库检查的键数量
DEFAULT_KEY_NUMBERS = 20
#全局变量,记录检查进度
current_db = 0
def activeExpireCycle():
#初始化要检查的数据库数量
#如果服务器的数据库数量比DEFAULTDBNUMBERS要小
#那么以服务器的数据库数量为准
if server.dbnum < DEFAULT_DB_NUMBERS :
db_numbers = server.dbnum
else:
db_numbers = DEFAULT_DB_NUMBERS
#遍历各个数据库
for i in range(db_numbers ):
#如果current_db 的值等于服务器的数据库数量
#这表示检查程序已经遍历了服务器的所有数据库一次
#将current_db 重置为0,开始新的一轮遍历
if current_db == server.dbnum:
current_db = 0
#获取当前要处理的数据库
redisDb =server.db[currentdb]
#将数据库索引增1,指向下一个要处理的数据库
current_db += 1
#检查数据库键
for j in range(DEFAULT_KEY_NUMBERS):
#如果数据库中没有一个键带有过期时间,那么跳过这个数据库
if redisDb.expires.size() == 0 :
break
#随机获取一个带有过期时间的键
key_with_ttl = redisDb.expires.get_random_key
#检查键是否过期,如果过期就删除它
if is_expired(key_with_ttl):
delete_key(key_with_ttl)
#已达到时间上限,停止处理
if reach_time_limit():
return
整体的工作流程如下:
-
函数运行时,都从一定数量的数据库里取出一定数量的随机键检查,并删除其中的过期键
-
全局变量
current_db
记录了检查进度,下一次调用时也是接着上一次的进度进行处理 -
如果所有的数据库都被检查完了,
current_db
被设置为0,重新开始
9.7 AOF、RDB和复制功能对过期键的处理
9.7.1 生成RDB文件
在执行SAVE/BGSAVE
命令创建一个新的RDB
文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB
文件中
数据库中包含过期键不会对生成新的RDB
文件造成影响
9.7.2 载入RDB文件
启动Redis
服务器时,如果开启了RDB
功能,会对RDB
文件进行载入
-
服务器以主服务器模式运行,程序会对文件中保存的键进行检查,只有未过期的键才会被载入到数据库
-
服务器以从服务器模式运行,文件中保存的所有键,不论是否过期都会被载入到数据库里。但是因为主从服务器在进行数据同步的时候,从服务器的数据库就会被情况,所以一般也不会造成影响。
9.7.3 AOF文件写入
当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但是它还没有被惰性删除或定期删除,AOF文件不会因为这个过期键而产生任何影响
当过期键被惰性删除/定期删除后,程序会向AOF文件追加一条DEL
命令,显式记录该键已经被删除
9.7.4 AOF重写
在执行AOF重写的过程里,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件里。
因此数据库中包含过期键不会对AOF重写造成影响
9.7.5 复制
当服务器运行在复制模式下,从服务器的过期键删除动作由主服务器控制:
-
主服务器在删除一个过期键后,显式地向所有从服务器发送一个
DEL
命令,告知从服务器删除过期键 -
从服务器在执行客户端发送的读命令,即使碰到过期键也不会将过期键删除,而且按照正常流程返回值。只有收到主服务器发来地
DEL
命令之后,才会删除过期键
由主服务器来控制从服务器统一删除过期键,可以保证主从服务器数据地一致性,但是当一个过期键仍然存在主服务器地数据库里,过期键地复制品也依旧存在从服务器。
9.8 数据通知
Redis2.8版本新增加的功能,可以让客户端通过订阅给定的频道或模式来获知数据库中键的变化以及数据库中命令的执行情况
-
键空间通知:某个键执行了什么命令
-
键事件通知:某个命令被什么键执行了
服务器配置的 notify-keyspace-events
选项决定了服务器所发送通知的类型
-
想让服务器发送所有类型的键空间通知和键事件通知,可以将选项的值设置为AKE。
-
想让服务器发送所有类型的键空间通知,可以将选项的值设置为AK。
-
想让服务器发送所有类型的键事件通知,可以将选项的值设置为AE。
-
想让服务器只发送和字符串键有关的键空间通知,可以将选项的值设置为KS。
-
想让服务器只发送和列表键有关的键事件通知,可以将选项的值设置为E1。
9.8.1 发送通知
由notify.c/notifyKeyspaceEvent
函数实现,定义如下:
void notifyKeyspaceEvent(int type, char *event,robj *key,
int dbid);
其中type
参数是当前想要发送的通知的类型,程序会根据这个值来判断通知是否就是服务器配置notify-keyspace-events
选项所选定的通知类型,从而决定是否发送通知。
event
表示事件的名称,key
表示产生事件的键,dbid
表示产生事件的数据库号码
当一个Redis
命令需要发送数据库通知的时候,该命令的实现函数就会调用notifyKeyspaceEvent
函数,并且传入相关信息
比如SADD
命令的实现函数里的其中一部分代码
Voids saddCommand(red1sclient *c){
//如果至少有一个元素被成功添加,那么执行以下程序
if(added){
//发送事件通知
notifyKeyspaceEvent(REDIS_NOTIFY_SET,"sadd",
c->argv[1],c->db->id);
}
}
当SADD
命令至少成功地向集合里添加了一个集合元素之后,命令就会发送通知,类型为REDIS_NOTIFY_SET
,表示这是一个集合键通知,名称为sadd
,表示这是执行SADD
命令产生地通知
9.8.2 发送通知的实现
notifyKeyspaceEvent
的伪代码如下:
def notifyKeyspaceEvent(type,event,key,dbid):
#如果给定的通知不是服务器允许发送的通知,那么直接返回
if not(server.notify_keyspace_events & type):
return
#发送键空间通知
if server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE:
#将通知发送给频道keyspace@<dbid>_:<key>
#内容为键所发生的事件<event>
#构建频道名字
chan="_keyspace@{dbid)_:{key}".format (dbid=dbid, key=key)
#发送通知
pubsubPublishMessage(chan,event)
#发送键事件通知
if server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT :
#将通知发送给频道keyevent@<dbid>:<event>#内容为发生事件的键<key>
#构建频道名字
chan ="_keyevent@(dbid}_:{event}".format(dbid=dbid,event=event)
#发送通知
pubsubPublishMessage(chan,key)
执行以下操作
-
server.notify_keyspace_events
属性是服务器配置notify-keyspace-events
选项设置的值,如果给的通知类型type
不是服务器允许发送的,直接返回 -
检测是否允许发送键空间通知,允许的话,构建并发送
-
检测是否允许发送键事件通知,允许的话,构建并发送
han,event)
#发送键事件通知
if server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT :
#将通知发送给频道keyevent@:#内容为发生事件的键
#构建频道名字
chan =“keyevent@(dbid}:{event}”.format(dbid=dbid,event=event)
#发送通知
pubsubPublishMessage(chan,key)
执行以下操作
- `server.notify_keyspace_events` 属性是服务器配置`notify-keyspace-events`选项设置的值,如果给的通知类型`type`不是服务器允许发送的,直接返回
- 检测是否允许发送键空间通知,允许的话,构建并发送
- 检测是否允许发送键事件通知,允许的话,构建并发送