- redis数据库的简介
- redis数据库中数据的存储
- redis数据库的操作
这篇博客我将对redis的服务器中的数据库进行详细的介绍。我会先对redis数据库进行一个简单的说明,对于数据库来说,主要的操作其实就是两方面,对数据库本身的操作和数据库对数据的操作。在这里我要说明的是笔者安装的是最新的redis(3.2.8)版本的,可能会和其他版本的有些不同。
1. redis数据库的简介
redis服务器将所有的数据库都保存在服务器的配置server.h/redisServer结构的db数据中,db数据中的每一项都是server.h/redisDb结构,每一个redis结构代表一个数据库。我们可以在redis的安装目录下的src目录下的server.h中查看这些配置。
这个是一个数据,保存着redis的所有数据库。
我们知道redis是支持多数据库的,dbnum就是服务器初始化的时候,在服务器中建的数据库的数量。这个值我在上一篇博客中已经提到了,默认是16。
2. redis数据库中数据的存储
- 数据库的键空间
是用来做存储数据的,它最重要的作用也是在这里,所以,我们先来说一下redis服务器中数据库对数据机构。redis我们都知道,redis是一种键值对(key-value)的数据库服务器。redis的数据库的的结构是在server.h/redisDb中定义的。
redisDb结构中的 dict 字典保存了数据库中所有的键值对,也就是我们说的键空间。
下面我们用实际的例子来说一下键空间。
127.0.0.1:6379> SET name wuli
OK
127.0.0.1:6379> EXPIRE name 6
(integer) 1
127.0.0.1:6379> get name
"wuli"
127.0.0.1:6379> TTL name
(nil)
执行完这段脚本之后,我们会生成如下的一个键空间结构。
我们其实从图中就可以看出,这是一个字典结构,对于有编程基础的人来说,这是一个很熟悉的数据结构。所以,针对整个数据库的操作,就可以看做是在操作一个字典结构,比如说,我们向数据库添加一个键值对,或者从数据库中删除一个键值对,对键值对进行修改都想我们操作字典结构一样。
- 过期删除策略
我们一定会遇到只在一小段时间内需要一个对象的情况,redis 中可以通过 EXPIRE 命令或者 PEXXPIRE 命令来设置数据库中某个键的生存时间(以秒或者毫秒为单位),在经过指定的时间之后,服务器就会自动的删除生存时间为0的键。同样我们还可以通过 EXPIRE 命令和 PEXPIRE 设置键的过期时间,过期时间是一个 UNIX 时间戳,当键的过期时间来临时,服务器就会自动从数据库中删除这个键。
redis 键的生存时间
127.0.0.1:6379> set name wuli
OK
127.0.0.1:6379> expireat name 13772677880
(integer) 1
127.0.0.1:6379> time
1)13702719320
2)268092
127.0.0.1:6379> get name //13702719320秒之前
"wuli"
127.0.0.1:6379> time
1)13002582520
2)209012
127.0.0.1:6379> get name //13702719320秒之后
(nil)
以上四种命令都可以设置键的生存时间或者过期时间,但实际上 EXPIRE ,PEXPIRE,EXPIREAT 三个命令都是使用 PEXPIREAT 命令来实现,无论客户端执行哪一条命令,最后都会转换成 PEXPIREAT 命令来执行。
EXPIRE 可以转换成 PEXPIRE 命令:
def EXPIRE():
ttl_in_ms = sec_to_ms(ttl_in_sec)
PEXPIRE(key, ttl_in_ms)
PEXPIRE 命令又可以转换为 PEXPIREAT 命令:
def PEXPIRE(key, ttl_in_ms):
now_ms = get_current_unix_timestamp_in_ms()
PEXPIREAT(key, now_ms+ttl_in_ms)
EXPIREAT 也可以转成 PEXPIREAT 命令:
def EXPIREAT(key, expire_time_in_sec):
expire_time_in_ms = sec_to_ms(expire_time_in_sec)
PEXPIREAT(key, expire_time_in_ms)
你一定会问,这些信息又是在哪保存着呢?其实它就在redisDb结构中的expires字典中,过期字典的键是一个指针,很明显的这个指针指向了键空间的某个对象,过期字典的值是一个long long类型的整数,
当然,可以添加过期时间,也可以移除过期时间,使用 PERSIST 可以在过期字典中找到给定的键,并解除键和值的关联。
127.0.0.1:6379> PEXPIREAT name wuli
(Integer) 1
127.0.0.1:6379> TTL name
(integer) 13121451
127.0.0.1:6379> PERSIST name
(Integer) 1
127.0.0.1:6379> TTL name
(Integer) -1
至于判断一个键是否为过期键的过程也很简单,首先去过期字典表中查找是否存在这个给定的键,如果存在,检查当前的时间戳是否比它的时间戳大,如果大的话,那么键已过期,可以进行删除。
接着重点就来了,redis 是在什么时候对过期的键进行删除的呢?首先,我们可以自己想一下可能的删除策略,一般来说有三种策略。
定时删除:我们最希望看到的结果应该就是,但这个过期的键出现的那一刻,就将它删除点,所以,我们在创建这个键的时候,就需要另外创建一个定时器,定时器的时间就是这个键的生存时间或者过期时间,这样我们就可以把没有用的空间释放出来。但是这种方法对 CPU 时间是最不友好的,在过期键比较多的情况下,删除过期键这一操作可能会占用相当大的一部分 CPU 时间,当内存资源不紧张 CPU 资源紧张的时候,将 CPU 用在删除和当前任务无关的过期键上,无疑会对服务器的相应和吞吐量造成影响。
惰性删除:这种删除策略,我相信 大多数人也用过,就是过期之后,我们不去管它,只有再一次用到它的时候,才会检查它的状态,然后进行删除。这个于定时删除策略相比正好相反。它对 CPU 是最友好的,但是对内存的是最不友好的。
定期删除:这种方法是综合定时删除和惰性删除而产生的一种删除策略。服务器每隔一段时间进行一次检测,删除其中过期的键。至于多长时间进行删除操作,就需要根据实际情况来定了,这也是这种策略最难的地方。
redis 采用的过期删除策略是惰性删除和定期删除这两种策略。
redis惰性删除策略
redis 的过期珊瑚策略由db.c/expireIfNeeded 函数实现,所以读写数据库的redis命令在执行之前都会调用 expireIfNeeded 函数对输入键进行检查:如果输入键已经过期,那么expireIfNeeded 函数将输入键从数据库删除;如果输入键未过期,那么expireIfNeeded 函数不做任何操作。
仔细看上面的图,你是否会觉得却少了什么。没错,因为每个被访问的键都有可能因为过期而被 expireIfNeeded 函数删除,所以,每个命令实现函数的时候都必须先检测键是否存在。当键存在的时候,命令按照键存在的情况执行;当键不存在或者被删除的时候,命令按照键不存在的情况执行。
redis定期删除策略
过期键的定期删除策略在redis.c/activeExpireCycle 函数中实现,其实现过程为:每当 redis 的服务器周期性的操作 redis.c/serverCron 函数时,activeExpireCycle 函数就会被调用,它在规定的时间内,分多次遍历 redis 服务器中数据库,从 expires 过期字典中随机检查一部分键的过期时间,并删除其中的过期键。
整个过程的代码如下:
#初始化要检查的数据库数量
#如果服务器的数据库比默认每次要检查的数据库数量小
#那么以服务器的数据库数量为准
if (dbs_per_call > server.dbnum || timelimit_exit)
dbs_per_call = server.dbnum;
#遍历各个数据库
for (j = 0; j < dbs_per_call; j++) {
int expired;
#获取当前要处理的数据库
redisDb *db = server.db+(current_db % server.dbnum);
#数据库索引+1,指向下一个数据库
current_db++;
do {
unsigned long num, slots;
long long now, ttl_sum;
int ttl_samples;
if ((num = dictSize(db->expires)) == 0) {
db->avg_ttl = 0;
break;
}
slots = dictSlots(db->expires);
now = mstime();
if (num && slots > DICT_HT_INITIAL_SIZE &&
(num*100/slots < 1)) break;
expired = 0;
ttl_sum = 0;
ttl_samples = 0;
if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
#检查当前数据库的键
while (num--) {
dictEntry *de;
long long ttl;
#随机取数据库的键,如果为null跳出
if ((de = dictGetRandomKey(db->expires)) == NULL) break;
ttl = dictGetSignedIntegerVal(de)-now;
#删除键
if (activeExpireCycleTryExpire(db,de,now)) expired++;
if (ttl < 0) ttl = 0;
ttl_sum += ttl;
ttl_samples++;
}
if (ttl_samples) {
long long avg_ttl = ttl_sum/ttl_samples;
if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
db->avg_ttl = (db->avg_ttl+avg_ttl)/2;
}
iteration++;
if ((iteration & 0xf) == 0) {
long long elapsed = ustime()-start;
latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
if (elapsed > timelimit) timelimit_exit = 1;
}
#已经达到时间上线,停止处理
if (timelimit_exit) return;
2. redis数据库的操作
切换数据库
每个 reids 客户端都有自己的目标数据库,每当客户端执行数据库写命令或者读命令的时候,目标数据库就会成为这些命令的操作对象。默认情况下,redis客户端的目标数据库为 0 号数据库,但客户端可以执行 SELECT 命令来切换数据库。
127.0.0.1:6379> set name zhihui
OK
127.0.0.1:6379> SELETE 3
OK
127.0.0.1:6379> GET name
(nil)
数据库通知
redis 的数据库通知是在 redis 2.8 版本新增的功能,这个功能使得客户端可以获得数据库中键的变化,以及数据库中命令的执行情况。发送通知的功能由notify.c/notifyKeyspaceEvent函数来实现的。
type:是当前要发送通知的类型,程序会根据这个值来和 notify_keyspace_events 比较,判断这个值是否为服务器配置的通知类型,从而决定是否发送通知。
event:事件的名称
keys:产生事件的键
dbid:产生事件的数据库号
server.notify_keyspace_events:就是服务器配置 notify_keyspace_events 选项所附的值(具体有哪些类型可以自己去查一下)。
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) {
sds chan;
robj *chanobj, *eventobj;
int len = -1;
char buf[24];
#如果给定的通知不是服务器允许发送的通知,那么直接返回
if (!(server.notify_keyspace_events & type)) return;
eventobj = createStringObject(event,strlen(event));
#发送键空间通知
if (server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE) {
#将通知发送给频道
#内容为键所发生的事件
#构建频道名字
chan = sdsnewlen("__keyspace@",11);
len = ll2string(buf,sizeof(buf),dbid);
chan = sdscatlen(chan, buf, len);
chan = sdscatlen(chan, "__:", 3);
chan = sdscatsds(chan, key->ptr);
chanobj = createObject(REDIS_STRING, chan);
#发送通知
pubsubPublishMessage(chanobj, eventobj);
decrRefCount(chanobj);
}
#发送键事件通知
if (server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT) {
#将通知发送给频道
#内容为键所发生的事件
#构建频道名字
chan = sdsnewlen("__keyevent@",11); if (len == -1) len = ll2string(buf,sizeof(buf),dbid); chan = sdscatlen(chan, buf, len); chan = sdscatlen(chan, "__:", 3);
chan = sdscatsds(chan, eventobj->ptr); chanobj = createObject(REDIS_STRING, chan);
#发送通知
pubsubPublishMessage(chanobj, key); decrRefCount(chanobj); } decrRefCount(eventobj);
redis 的数据库通知有分为两种,键空间通知(key-space notification),键事件通知(key-event notification)。
键空间通知:其关注的是某个键执行了什么命令
127.0.0.1:6379> SUBSCRIBE _ _keyspace@0_ _:name
Reading message... {press Ctrl-C to quit}
1) "subscribe"
2) "_ _keyspace@0_ _:name"
3) (integer) 1
1) "name"
2) "_ _keyspace@0_ _:name"
3) "set"
1) "name"
2) "_ _keyspace@0_ _:name"
3) "expire"
1) "name"
2) "_ _keyspace@0_ _:name"
3) "del"
键事件通知:它关注的是某个命令被什么键执行了
127.0.0.1:6379> SUBSCRIBE _ _keyspace@0_ _:set
Reading message... {press Ctrl-C to quit}
1) "subscribe"
2) "_ _keyspace@0_ _:set"
3) (integer) 1
1) "message"
2) "_ _keyspace@0_ _:set"
3) "name"
1) "message"
2) "_ _keyspace@0_ _:set"
3) "age"
1) "message"
2) "_ _keyspace@0_ _:set"
3) "love"