redis系列(四)数据库

  • 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"

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值