10.数据库及过期策略

2020-02-23  本文已影响0人  xMustang

数据库及过期策略

1. 服务器中的数据库

Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中,db数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库:

struct redisServer{

    // ...

    // 一个数组,保存服务器中的所有数据库
    redisDb *db;

    // ...
}

在初始化服务器时,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库:

struct redisServer{

    // ...

    // 服务器的数据库数量
    int dbnum;

    // ...
};

dbnum属性的值由服务器配置的database选项决定,默认情况下,该选项的值为16,Redis默认会创建16个数据库。

Redis服务器数据库示例

2. 切换数据库

Redis客户端的默认数据库为0号数据库,客户端可以通过执行SELECT命令切换目标数据库。

SELECT 2   // 切换到2号数据库

客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针。

typedef struct redisClient{

    // ...

    // 记录客户端当前正在使用的数据库
    redisDb *db;

    // ...
} redisClient;

redisClient.db指针指向redisServer.db数组的其中一个元素,而被指向的元素就是客户端的目标数据库。

如下图,反应了客户端与服务器端数据库的关系。

客户端的目标数据库

3. 数据库键空间

服务器中的每个数据库都由一个redis.h/redisDb表示。

typedef struct redisDb{
    // ...

    // 数据库键空间,保存数据库中所有的键值对
    dict *dict;

    // ...
} redisDb

3.1 添加新键

添加一个新键值对到数据库,实际上就是将一个新键值对添加到键空间字典里面,其中键为字符串对象,值为任意一种类型的Redis对象。

3.2 删除键

删除数据库中的一个键,实际上就是在键空间里面删除键所对应的键值对对象。

3.3 更新键

对一个数据库键进行更新,实际上就是对键空间里面键所对应的值对象进行更新。

3.4 对键取值

对一个数据库键进行取值,实际上就是在键空间中取出键所对应的值对象。

3.5 其他键空间操作

还有很多针对数据库本身的Redis命令,也是通过对键空间进行处理来完成的,如:

  1. FLUSHDB,通过删除键空间中所有键值对来实现
  2. RANDOMKEY,通过在键空间随机返回一个键来实现
  3. DBSIZE,通过返回键空间中包含的键值对的数量来实现
  4. EXISTS、RENAME、KEYS等命令也是通过对键空间操作实现的

3.6 读写键空间时的维护操作

当使用Redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作,其中包括:

  1. 在读取一个键之后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中(hit)次数或键空间不命中(miss)次数,这两个值可以在INFO stats命令的keyspace_hits属性和keyspace_misses属性查看。
  2. 在读取一个键之后,服务器会更新键的LRU(最后一次使用)时间,这个值可以用于计算键的闲置时间。
  3. 如果服务器在读取一个键时发现这个键已经过期,那么服务器会先删除这个过期键,再执行余下的其他操作。
  4. 如果有客户端使用WATCH命令监视了某个键,那么服务器在对被监视的键进行修改后,会将这个键标记为脏(dirty),从而让事务程序注意到这个键已经被修改过。
  5. 服务器每次修改一个键之后,都会对脏(dirty)键计数器的值增1,这个计数器会触发服务器的持久化及复制操作。
  6. 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知。

4. 设置键的生存时间或过期时间

  1. EXPIRE、PEXPIRE:以秒、毫秒精度为键设置生存时间(Time To Live,TTL)。设置值为几秒、几毫秒。
  2. EXPIREAT、PEXPIREAT:以秒、毫秒精度为键设置生存时间(Time To Live,TTL)。设置值为Unix时间戳。
  3. SETEX:可以在设置一个字符串键的同时为键设置过期时间。
  4. TTL、PTTL:查看键的剩余生存时间。

4.1 保存过期时间

typedef struct redisDb{

    // ...

    // 过期字典,保存键的过期时间
    dict *expires;
} redisDb;

redisDb中的expires字典保存了数据库中所有键的过期时间,称这个字典为过期字典:

  1. 过期字典的键是一个指针,这个指针指向键空间中的某个键对象。
  2. 过期字典的值是一个long long类型的整数,一个毫秒精度的Unix时间戳。

下面是一个带有过期字典的数据库示例。

带有过期字典的数据库示例

上图中键空间、过期字典中重复出现两次alphabat、book键对象。在实际中,键空间的键和过期字典的键都指向同一个键对象,不会出现任何重复对象,不会浪费任何空间。

4.2 移除过期时间

PERSIST:移除一个键的过期时间。

PERSIST在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联。

4.3 计算并返回剩余生存时间

TTL、PTTL两个命令都是通过计算键的过期时间和当前时间之间的差来实现的。

4.4 过期键的判定

Redis检查键是否过期的方法:先取得键的过期时间,检查当前UNIX时间戳是否大于键的过期时间,如果是,那么键已经过期;否则,键未过期。

另一种检查键过期的方法是:使用TTL、PTTL命令,判断返回的值是否大于等于0。但是Redis并未使用此策略,因为直接访问字典比执行一个命令稍微快一些。

4.5 过期键删除策略

过期键有3种不同的删除策略:

  1. 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除策略。(Redis实际未使用该策略)
  2. 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期,就删除该键,如果没有过期,就返回该键。
  3. 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。

上面第1种、第3种为主动删除策略,第2种为被动删除策略。

4.5.1 定时删除(Redis未使用此策略)

  1. 定时删除策略对内存是友好的:可以保证过期键会尽快被删除,并释放过期键所占用的内存。
  2. 定时删除策略对CPU时间是不友好的:在过期键比较多的情况下,删除过期键可能会占用相当一部分CPU时间。

创建一个定时器,需要用到Redis服务器中的时间事件,而Redis当前时间事件的实现方式是无序链表,查找一个事件的时间复杂度为O(N),并不能高效地处理大量时间事件。

因此,要让服务器创建大量的定时器,从而实现定时删除策略,在现阶段来说并不现实。

4.5.2 惰性删除

  1. 惰性删除策略对CPU时间是友好的:程序只会在取出键时才对键进行过期检查。
  2. 惰性删除策略对内存是不友好的:如果一个键已经过期,只要这个过期键不被删除,所占用的内存就不会释放。

4.5.3 定期删除

定期删除策略是前两种策略的一种整合和折中:

  1. 定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
  2. 定期删除策略也能有效地减少因为过期键带来的内存浪费。

定期删除策略的难点是确定删除操作执行的时长和频率。

5. Redis的过期键删除策略

Redis服务器实际使用的是惰性删除、定期删除两种策略。

5.1 惰性删除策略的实现

过期键的惰性删除策略由db.c/expireIfNeeded函数实现。

所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded对输入键进行检查。

5.2 定期删除策略的实现

过期键的定期删除策略由redis.c/activeExpireCycle函数实现。

每当Redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。在遍历过程中,全局变量current_db会记录activeExpireCycle函数检查的进度。

6. RDB、AOF和复制功能对过期键的处理

下面介绍RDB持久化功能、AOF持久化功能、复制功能是如何处理数据库中的过期键的。

6.1 生成RDB文件

在执行SAVE、BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。

6.2 载入RDB文件

在启动Redis服务器时,如果服务器启动了RDB功能,那么服务器将对RDB文件进行载入:

  1. 如果服务器以"主服务器模式"运行,那么在载入RDB文件时,程序会对文件中保存的键进行检查,未过期的键会被载入数据库中,过期键会被忽略。
  2. 如果服务器以"从服务器模式"运行,那么在载入RDB文件时,文件中保存的所有键,无论是否过期,都会被载入数据库。不过,因为主从服务器在进行数据同步时,"从服务器"的数据库会被清空。

6.3 AOF文件写入

当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而产生任何影响。

当过期键被惰性删除或定期删除后,程序会向AOF文件追加一条DEL命令,来显式记录该键已被删除。

6.4 AOF重写

在执行AOF重写时,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。

6.5 复制

当服务器运行在复制模式下,"从服务器"的过期键删除动作由"主服务器"控制:

  1. 主服务器在删除一个过期键之后,会显式向所有"从服务器"发送一个DEL命令,告知"从服务器"删除这个过期键。
  2. "从服务器"在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键。
  3. "从服务器"只有在接到"主服务器"发来DEL命令后,才会删除过期键。

这种由"主服务器"控制"从服务器"统一删除过期键,可以保证主从服务器数据的一致性。

7. 数据库通知

这个功能可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。

关注"某个键执行了什么命令"的通知称为"键空间通知"。

SUBSCRIBE _ _keyspace@0_ _:message

获取0号数据库中针对message键执行的所有命令。

关注"某个命令被什么键执行了"的通知称为"键事件通知"。

SUBSCRIBE _ _keyevent@0_ _:del

获取0号数据库中所有执行DEL命令的键。

notify-keyspace-events决定了服务器发送通知的类型:

  1. 想让服务器发送所有类型的键空间通知和键事件通知,可将选项值设为AKE。
  2. 想让服务器发送所有类型的键空间通知,可将选项值设为AK。
  3. 想让服务器发送所有类型的键事件通知,可将选项值设为AE。
  4. 想让服务器只发送和字符串有关的键空间通知,可将选项值设为K$。
  5. 想让服务器只发送和列表键有关的键事件通知,可将选项值设为El。

8.1 发送通知

发送数据库通知的功能由notify.c/notifyKeyspaceEvent函数实现。

void notifyKeyspaceEvent(int type, char *event, robj *key,int dbid);

type参数是当前想要发送的通知的类型,程序会根据这个值来判断通知是否就是notifyKeyspaceEvent设定的通知类型,从而决定是否发送通知。

events、key、dbid分别是事件的名称、产生事件的键、产生事件的数据库号码。

上一篇下一篇

猜你喜欢

热点阅读