- 介绍Redis服务器的数据库实现,服务器保存数据库的方法
- 客户端切换数据库的方法,数据库保存键值对的方法,数据库的增,删,改,查实现方法
- 服务器保存键的过期时间的方法,服务器自动删除过期键的方法
- 数据库通知功能实现方法
服务器中的数据库
- redis服务器将所有数据库都保存到服务器状态redis.h/redisServer结构中的db数组中,db数组中的每一个项都是redis.h/redisDb结构,数据库的数量由dbnum属性决定,dbnum是由服务器配置的database选项决定,默认是16所以Redis服务器默认会创建16个数据库
struct redisServer{
//...
//db数组,保存着服务器中所有数据库
redisDb *db;
//服务器的数据库数量
int dbnum;
//...
};
切换数据库
- 默认情况,Redis客户端的目标数据库是0号数据库,客户端可以通过执行
select 2
命令来切换数据库到2号数据库
- 在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针:
typedef struct redisClient{
//...
//记录客户端当前正在使用的数据库
redisDb *db;
//...
}redisClient;
redisClient.db指针指向redisServer.db数组中的其中一个元素,而被指向的元素就是客户端的目标数据库
比如某客户端的目标数据库是1号,执行了select 2,客户端和服务器状态之间的关系将更新,具体如下图所示:
selects实现的原理,就是通过修改redisClient.db的指针,让他指向不同的数据库
数据库键空间
- 服务器中的数据库由redis.h/redisDb结构表示,其中redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间
typedef struct redisDb{
//...
//数据库的键空间,保存着数据库中所有键值对
dict *dict;
//...
}redisDb;
键空间的键:就是数据库的键,每个键对应一个字符串对象
键空间的值:就是数据库的值,每个值可以是字符串对象,列表对象,哈希表对象,集合对象,有序集合对象中的任意一种redis对象
执行下面命令:
SET message "hello world"
RPUSH alphabet "a" "b" "c"
HSET book name "redis in action"
HSET book author "josiah l. carlson"
HSET book publisher "manning"
-
对应的数据库键空间
数据库的增删改查都是基于键空间来实现的
添加新键
SET data "2013.12.1"
删除键
DEL book
更新键
SET message "blah blah"
对键取值
GET message
,就是首先在键空间中找到message,找到之后取得该键所对应的字符串对象值,然后返回值对象包含的字符串
- 其他键空间操作
FLUSHDB
清空整个数据库,通过删除键空间中所有的键值对
RANDOMKEY
随机返回数据库中某个键,就是在键空间中随机返回某个键的
DBSIZE
返回数据库键数量,返回键空间中包含的键值对的数量
EXISTS
RENAME
KEYS
- 读写键空间时的维护操作
- 读取一个键之后,会更新服务器的键空间命中(hit)次数和键空间不命中(miss)次数,这两个值可以在INFO stats命令的keyspace_hits属性和keyspace_miss属性中查看
- 读取键之后,会更新键的LRU(最后一次使用)时间,可以用来计算键的闲置时间
OBJECT idletime <key>
查看key的闲置时间- 服务器在读取一个键的时候,发现键已经过期了,会 先删除这个键,然后执行其他操作
- 客户端使用WATCH命令监视某个键,那么服务器在对被监视的键进行修改过之后,会把这个键标记为脏,从而让事务程序注意到这个键已经被修改过
- 服务器每次修改一个键之后,都会对脏键计数器加1,这个计数器 会触发服务器的持久化以及复制操作
- 如果服务器开启了通知功能,在对键修改之后,服务器按配置发送相应的数据库通知
设置键的生存时间或过期时间
SETEX
这个命令在设置一个字符串键的同时为键设置过期时间,但只对字符串键有用
EXPIRE <KEY> <TTL>
将key生存时间设置为TTL秒PEXPIRE <KEY> <TTL>
将key生存时间设置为TTL毫秒EXPIREAT <KEY> <TIMESTAMP>
将key的过期时间设置为timestamp所指定的秒数时间戳PEXPIREAT <KEY> <TIMESTAMP>
将key的过期时间设置为timestamp所指定的毫秒时间戳
最终执行过期的的命令其实都是
PEXPIREAT
命令来实现的,上面四个命令在经过转换之后,都会执行PEXPIREAT命令的
def EXPIRE(key,ttl_in_sec):
ttl_in_ms=sec_to_ms(ttl_in_sec);//将ttl从秒转为毫秒
PEXPIRE(key,ttl_in_ms);
def PEXPIRE(key,ttl_in_ms):
now_ms=get_current_unix_timestamp_in_ms(ttl_in_ms);//获取以毫秒计算unix时间戳
PEXPIREAT(key,now_ms+ttl_in_ms);//当前时间加ttl,得出毫秒格式的键过期时间
def EXPIREAT(key,expire_time_in_sec):
expire_time_in_ms=sec_to_ms(expire_time_in_sec);//将过期时间转化为毫秒
PEXPIREAT(key,expire_time_in_ms);
- 保存过期时间
typedef struct redisDb{
//..
//过期字典,保存着键的过期时间
dict *expires;
}redisDb;
过期字典的,键是一个指针,执行键空间某个键对象,值是一个long long类型的整数,保存了键所指向的数据库键的过期时间,一个毫秒精度的unix时间戳,实际中键空间的键和过期字典的键都指向同一个键对象,所以不存在浪费空间
def PEXPIREAT(key,expire_time_in_ms):
if key not in redisDb.dict: //如果键不在键空间中,就不能设置过期时间
return 0;
redisDb.expires[key]=expire_time_in_ms; //在过期字典中关联键和过期时间
return 1; //过期时间设置成功
- 移除过期时间
PERSIST key
可以移除过期时间,在过期字典中查找键,并解除键和值在过期字典中的关联
def PERSIST(key):
if key not in redisDb.expires: //如果键不存在,或者键没有设置过期时间,直接返回
return 0;
redisDb.expires.remove(key); //移除过期字典中给定键的键值对关联
return 1; //键的过期时间移除成功
- 计算并返回剩余生存时间
TTL key
或者PTTL key
命令,分别是以秒 和毫秒返回键的剩余生存时间,通过计算过期时间和当前时间差来实现
def PTTL(key):
if key not in redisDb.dict: //键不在数据库中
return -2;
expire_time_in_ms=reidsDb.expires.get(key)//获取键的过期时间,如果没有设置过期时间,expire_time_in_ms就是None
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_int_ms=PTTL(key) //获取以毫秒为单位的剩余生存时间
if ttl_in_ms<0: //处理返回-1,-2的情况
return ttl_in_ms;
else:
return ms_to_sec(ttl_in_ms); //将毫秒转换为秒
- 过期键的判定
- 通过使用TTL或者PTTL来实现,如果返回的值大于等于0,说明没有过期
- 执行is_expired函数,Redis中判断是否过期,和is_expired的过程一样
- 检查当前键是否存在于过期字典中,如果存在,取得过期时间
- 检查当前unix时间戳是否大于键的过期时间,是,过期,否,没有过期
def is_expired(key):
expire_time_in_ms=redisDb.expires.get(key) //取得键的过期时间
if expire_time_in_ms is None: //键没有设置过期时间
return false;
now_ms=get_current_unix_timstamp_in_ms() //取得当前时间的unix时间戳
if now_ms > expire_time_in_ms: //检查当前时间是否大于键的过期时间
return true;
else:
return false;
过期键删除策略
这个过期键什么时候被删除了,有三种策略可以选择
- 定时删除,设置键的同时,创建一个定时器,在过期时间来的时候,立即执行删除
- 惰性删除,每次从键空间获取键,都先检查这个键是否过期,过期,删除,没有,返回
- 定期删除,每隔一段时间,就对数据库进行一次检查,删除里面的过期键
定时删除,占用太多的cpu时间,影响服务器的响应时间和吞吐量
惰性删除,浪费太多内存,有内存泄漏的危险
定期删除,是前两种的整合和折中,服务器必须根据实际情况,合理的设置删除操作执行的时长和指向频率
- Redis的过期键执行策略:
采用惰性删除和定期删除两种策略
- 惰性删除策略实现:
- 由db.c/expireIfNeeded函数实现,所有读写数据库的命令,在执行前都会调用这个函数,对键进行检查:如果键过期,就删除键,没有过期,继续执行实际命令
因为每个命令执行前可能这个键会因为过期被删除,或不存在,所以需要判断键是否存在
- 定期删除策略的实现:
- 由redis.C/activeExpireCycle函数实现,每当redis服务器周期性的执行redis.c/serverCron函数,activeExpireCycle就会被调用,在规定时间内分多次遍历服务器中的各个数据库,从数据库中的expires字典中随机检查一部分键的过期时间,并删除其中的过期键
DEFAULT_DB_NUMBERS=16//默认每次检查的数据库数量
DEFAULT_KEY_NUMBERS=20 //默认每个数据库检查的键数量
current_db=0 //全局变量,记录检查进度
def activeExpireCycle():
//初始化要检查的数据库量,如果服务器的数据库量比默认的小,就用服务器的
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[current_db]
//将数据库索引增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
AOF、RDB和复制功能对过期键的处理
- 生成RDB文件,执行save或者bgsave命令创建一个新的RDB文件,程序会对数据库的键进行检查,已过期的键不会被保存到新的RDB中
- 载入RDB文件,在启动redis服务器的时候,如果启动了RDB功能,服务器会对RDB文件进行载入
- 如果是主服务器模式运行,在载入RDB的时候,会对文件的键进行检查,没有过期的键会被载入到数据库中,过期的会被忽略
- 如果是从服务器模式运行,文件中所有键都会被载入到数据库中,在主从服务器同步的时候,从服务器的数据库会被清空
- AOF文件写入,当服务器以AOF持久化模式运行的时候,如果数据库的键过期了,但没有被删除,AOF文件不会有任何影响,当过期键被删除了,程序会向AOF文件追加append命令,显式记录该键被删除了
GET message
,从数据库中删除message键,追加一条DEL message命令到AOF文件中,向执行GET命令的客户端返回空回复- AOF重写,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中,执行BGREWRITEAOF命令
- 复制,当服务器运行在复制模式下,从服务器的过期键动作由主服务器控制
- 主服务器在删除一个过期键后,会显式的向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键
- 从服务器执行客户端发送的命令,即使碰到过期键也不会删除,继续向处理未过期的键一样
从服务器只有在接到主服务器发来的EDL命令,才会删除过期键
这时客户端向从服务器发送get message,会正常得到value值
这时客户端向主服务器发送get message,主服务器发现message过期了,会删除 message,向客户端返回空回复,并向从服务器发送DEL message,从服务器 收到del,会删除message,这样主从服务器都不保存message了
数据库通知
这个功能是Redis2.8新增的,可以让客户端通过订阅给定的频道或者模式,来获取数据库中键的变化,以及数据库中命令的执行情况
键空间通知,客户端获取0号数据库中针对message键的执行的所有命令,SUBSCRIBE _ _ keyspace@0_ _:message
键事件通知,某个命令被什么键执行了,SUBSCRIBE _ _ keyevent@0 _ _:del
服务器配置的notify-keyspace-events选项决定了服务器所发送通知的类型
- 想让服务器发送的所有类型的键空间通知和键事件通知,设为AKE
- 想让服务器发送的所有类型的键空间通知,设置AK
- 想让服务器发送的所有类型的键事件通知,设置为AE
- 想让服务器只发送和字符串键有关的键空间通知,设置为K$
- 想让服务器只发送和列表键有关的键事件通知,设置为E1
- 发送通知
发送数据库通知的功能是由notify.c/notifyKeyspaceEvent函数实现的
void notifyKeyspaceEvent(int type,char *event,robj *key,int dbid);
- 发送通知的实现
def notifyKeyspaceEvent(type,event,key,dbid):
//如果给定的通知不是服务器允许发送的通知,那么直接返回
if not(server.notify_keyspace_events & type):
return;
//发送键空间通知
if server.notify_keyspace_evetns & REDIS_NOTIFY_KEYSPACE:
//构建频道名字
chan = "__keysapce@{dbid}__:{key}".format(dbid=dbid,key=key)
//发送通知
pubsubPublishMessage(chan,event)
//发送键事件通知
if server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT:
chan="__keyevent@{dbid}__:{event}".format{dbid=dbid,event=event}
pubsubPublishMessage(chan,key)