redis数据库
前面我们已经学习了很多redis基本的数据结构,包括SDS、双端链表、压缩列表、整数集合、有序集合、哈希表、跳表等,现在是时候学习redis数据库了。redis是内存数据库,存储了键值对,并提供了操作键值对的方法。redis提供了键的失效机制、持久化机制,接下来将介绍redis数据库概念、键的失效机制。
redis服务器定义
redis数据库的服务器状态都保存在redisServer结构体中,定义如下:
struct redisServer {
//……
//此处省略300行,该结构体保存了redis服务器的很多状态,后面将会介绍一些主要的状态
//……
//服务器保存的数据库数组
redisDb *db;
//数据库数量,可配制的
int dbnum;
};
redisServer
结构体保存了服务器的很多状态,其中的db字段记录了该服务器的数据库数组。redisDb表示数据库,服务器在初始化的时候会根据dbnum属性来决定创建多少个redisDb数据库,默认情况会创建16个数据库,即db数组大小为16。
redis客户端定义和数据库切换
每个客户端连接到服务端时,服务器都会为之创建一个redisClient以保存该客户端的状态。redisClient的定义如下:
typedef struct redisClient {
//……
//该结构体保存了很多客户端的状态
//……
//每个客户端都会使用某个数据库,该指针指向了该客户端使用的哪个数据库
redisDb *db;
} redisClient;
redisClient结构体保存了客户端的相关状态,其中的db属性指向了该客户端的目标数据库,每个客户端都有自己的目标数据库,默认目标数据库为0号库,客户端可以选择使用哪一个数据库。如下例客户端所示:
127.0.0.1:6379> set msg "hello redis"
OK
127.0.0.1:6379> get msg
"hello redis"
127.0.0.1:6379> select 4
OK
127.0.0.1:6379[4]> get msg
(nil)
127.0.0.1:6379[4]> set msg "hello redis in 4th db"
OK
127.0.0.1:6379[4]> get msg
"hello redis in 4th db"
127.0.0.1:6379[4]> select 0
OK
127.0.0.1:6379> get msg
"hello redis"
上例中,首先在0号默认数据库设置键msg的值,然后通过select命令切换到4号库,该库不存在键msg,因为msg存在于0号库中。在4号库中同样可以设置键msg的值,不同的库可以有相同键的键值对。
切换库后,客户端的所有操作将在切换后的数据库中进行。注意,切换到4号库后,redis客户端后面展示了正在操作的库,类似127.0.0.1:6379[4]。
redis数据库定义
redis数据库的定义如下:
typedef struct redisDb {
//……
//数据库键空间,保存着数据库中的所有键值对
dict *dict;
//……
} redisDb;
redis键值对保存在字典dict中。
数据库的键值对空间可以执行添加操作、删除操作、更新操作。
键的过期时间
客户端可以通过expire或者pexpire命令,分别以秒或者毫秒精度为键设置生存时间(Time To Live, TTL)。在经过指定的时间后服务器将自动删除过期的键。
127.0.0.1:6379> set msg "hello"
OK
127.0.0.1:6379> expire msg 5 //设置键的过期时间为5秒
(integer) 1
127.0.0.1:6379> get msg //5秒之内
"hello"
127.0.0.1:6379> get msg //5秒后键过期
(nil)
还可以通过setex命令为键设置值的时候同时设置键的过期时间。
127.0.0.1:6379> setex msg 5 "hello" //设置键的同时,设置键的过期时间为5秒
OK
127.0.0.1:6379> get msg //5秒之内
"hello"
127.0.0.1:6379> get msg //5秒后键过期
(nil)
除了通过expire和pexpire命令给键设置过期时间外,还可以通过expireat和pexpireat命令,以秒或者毫秒精度设置键过期的某个时间点,表示在将来某个时间点键会过期。
expire命令是通过转换成pexpire命令实现的,pexpire命令是通过命令pexpireat命令实现的,expireat命令是通过pexpireat命令实现的。这几个命令的转换关系如下图:
键过期时间的保存
redis键过期时间是保存在redisDb结构的expires字典中的,这个字典称为过期字典。定义如下:
typedef struct redisDb {
//……
//过期字典,保存键的过期时间,精度为毫秒
dict *expires;
//……
}
expires字典保存了所有键的过期时间,该时间是一个unix时间戳,单位是毫秒。
移除过期时间
persist命令用于移除键的过期时间,使得键可以长期存在数据库中,它是expire命令的反命令。例如:
127.0.0.1:6379> setex msg 5 "hello" //设置键msg的过期时间为5秒
OK
127.0.0.1:6379> get msg
"hello"
127.0.0.1:6379> persist msg //移除键msg的过期时间
(integer) 1
127.0.0.1:6379> get msg
"hello"
返回键的剩余生存时间
ttl命令以秒为单位返回键的生存时间,pttl命令以毫秒为单位返回键的生存时间。这两个命令都是通过当前时间和键的过期时间差值来计算的。
过期键的判定
怎么判断一个键已经过期了?通过两个步骤判断:
- 在过期字典expires中查找是否存在键,如果存在取对应键的过期时间,如果不存在则说明该键不会过期。
- 检查步骤1中取到的过期时间和当前时间戳的大小,如果当前时间戳大于键的过期时间,说明键已经过期,否则键没有过期。
过期键的删除策略
redis过期的键需要删除,否则redis内存将会被很多过期键撑满。过期键的删除需要根据一系列的策略来决定怎样来删除,不同的策略在不同的场景下有不同的效果。我们已经知道了怎么样判定一个键是否过期了,至于过期键删除策略,大体有三种:定时删除、惰性删除和定期删除策略。
定时删除策略
在设置键的过期时间时,创建一个定时器,定时器在键的过期时间来临时执行过期键的删除操作。
这种策略可以保证过期键能够尽快被删除,保证redis的内存不被过期键撑满,但是它的缺点也是明显的,有多少个过期键就要创建多少个定时器,CPU执行定时器删除事件时会消耗大量的CPU时间。在有大量过期键同时过期的的情况下,CPU忙于处理过期键的删除,可能无法正常响应客户端的请求,因此该策略并不适合redis。
惰性删除
惰性删除策略只在访问键的时候,去检查该键是否过期,若过期则执行删除操作,否则走正常的逻辑。这种策略只处理当前访问的键,对其他键没有影响。
这种策略对CPU是友好的,但是对内存不是友好的,如果已经过期的键永远不被访问,那该键就一直存在于内存中,导致可用内存减少,这种case其实也可以视为内存溢出。
定期删除
定期删除策略每隔一断时间执行一次过期键删除的操作,通过限制删除操作的时长和频率来减少删除操作对CPU的影响。这种策略的关键是操作的时长和频率,如果时长设置的太长或者频率设置的太频繁,该策略将会退化为定时删除策略;如果时长设置的太短或者频率设置的太低,该策略将会退化为惰性删除策略。因此实际使用时需要合理的设置这两个参数。
redis过期键的删除策略
redis过期键的删除策略结合了惰性删除和定期删除策略,在cpu和内存之间取得了很好的平衡。
惰性删除策略实际上是通过expireIfNeeded函数来实现的,该函数会检查键是否过期,若键已经过期则将该键从数据库中删除,否则该函数什么也不做。所有读写数据库的命令在执行之前都会执行该函数进行过期键的检查。下面是该函数的源代码,并附上源码注释:
int expireIfNeeded(redisDb *db, robj *key) { //取该键的过期时间 mstime_t when = getExpire(db,key); mstime_t now; //该键未设置过期时间 if (when < 0) return 0; //服务器加载的时候不过期任何键 if (server.loading) return 0; //设置now的时间,根据lua确定 now = server.lua_caller ? server.lua_time_start : mstime(); //运行在slave模式下的服务器不会去删除过期键,过期键的操作由master机器执行,master机器执行删除 //过期键操作后会发送消息到slave机器,此时slave机器会删除该过期键。 //因此这里只是返回该键是否过期,并不会执行过期键删除操作。 if (server.masterhost != NULL) return now > when; //说明该键没有过期 if (now <= when) return 0; //到此说明该键已经过期,执行过期键的删除操作 server.stat_expiredkeys++; propagateExpire(db,key); //过期键删除事件通知 notifyKeyspaceEvent(NOTIFY_EXPIRED, "expired",key,db->id); return dbDelete(db,key); }
定期删除策略是由函数activeExpireCycle实现的,每当服务器的周期性操作serverCron函数执行时会调用activeExpireCycle函数执行过期键的定期删除策略。