Redis设计与实现之数据库

1,数据库
Redis使用redis.h/redisServer结构保存数据库状态。其中的数组redisDb *db,保存数据库中所有的数据库。另一个变量 int dbnum属性记录当前服务器中数据库的数量。dbnum默认为16,也就是redis初始化时会默认创建16个数据库。
2,切换数据库
每一个redis客户端都有自己的目标数据库,客户端执行数据库的写/读命令时目标数据库就是成为命令操作的对象。默认情况下,redis客户端使用0号数据库,并可以使用select命令来切换目标数据库。切换数据库的过程也是redisClient数据结构中 redisDb *db指针重新赋值的过程。
3,数据库键空间
Redis是一个键值对数据库服务器,每一个数据库使用redis.h/redisDb结构描述,其中有一个dict字典保存了该数据库的所有键值对,成为键空间;

typedef struct redisDb {
    dict *dict; /* The keyspace for this DB */
} redisDb;

键空间的键也就是数据库的键,每一个键都是一个字符串对象
键空间的值也就是数据库的值,每一个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象的任意一种。
添加、删除、更新和获取数据库中的键,就是对dict字典的操作。注意每一种值对象对应着不同的键获取命令,如get,lrange,hget,smembers,zrange等。
其他一些针对数据库本身的命令,也是通过对键空间进行处理来完成的。如flushdb用于删除键空间中所有键值对,dbsize返回数据库键数量等。
Redis对键空间的一些维护操作:
1,读取键时,根据键是否存在跟新键空间命中次数和不命中次数,并用keyspace_hits和keyspace_misses属性记录。可用info stats命令查看。
2,更新键的lru(最后一次使用)时间,用于计算闲置时间。使用object idletime 命令查看闲置时间。
3,读取键时,如果过期,要先删除过期键在执行后续操作。
4,客户端使用watch命令,监视一个键,如果该键被修改,就会被标记为脏(dirty),以使用事务程序对其处理。
5,服务器每修改一个键都会讲脏键计数器+1,该计数器会触发服务器的持久化以及复制操作。
6,如果服务器开启了数据库通知功能,对键修改后,服务器会按配置发送相应的数据库通知。
4,键的生存周期
使用expire,pexpire命令(单位分别是s和ms)设置键的生存时间TTL。expire ;经过指定的时间后,数据库会自动产出ttl为0的键。可以使用ttl或者pttl命令查看带有ttl键的剩余时间。
使用expireat,pexpireat命令将过期时间设置为相应的时间戳。expireat ;
实际上expire,pexpire,expireat最终都是调用了pexpireat实现了设置ttl的功能。
expire(将s转化为ms)->pexpire(将ttl转换为timestamp)->->pexpireat
expireat(将s级timestamp转化为ms级)->pexpireat
redisDb结构使用名为expires的字典保存数据库中所有键的过期时间,也称为过期字典。

typedef struct redisDb {
    dict *expires; /* Timeout of keys with a timeout set */
} redisDb;

expires的键时指针,指向键空间的某个键对象
expires的值是longlong类型的整数,保存了指向数据库键的过期时间,一个ms级unix时间戳。
使用persist可以解除给定键的过期时间。
使用ttl或者pttl命令获得键的剩余生存时间,实现在db.c/ttlGenericCommand(),伪码如下:

def ttlGenericCommand(key):
    if key 不在数据库中
          return -2
    if key 没有设置expire
          return -1
    ttl = expire时间戳 - 当前时间戳
    if ttl 小于 0
          ttl = 0
    按照ms标示将ttl的单位设置为转化为ms或者s
    返回 ttl

判断键是否过期:
1,检查指定键是否存在于过期字典;如果存在,取得过期时间,否则返回false
2,检查当前时间戳是否大于过期时间戳;如果大于,键已过期返回true,否则返回false
5,过期键的删除策略
过期键的删除策略有三种:
1,定时删除
设置过期时间的同时创建一个定时器,让定时器在键的过期时间来临时立即执行对键的删除。
该策略对内存比较友好,保证过期键会被尽快删除,但是它对cpu时间最不友好,大量的过期键需要进行删除操作时会占用大量的cpu时间。此外定时器使用时间事件处理,redis的时间事件存放在一个无序链表中,查找时间复杂度为O(N),并不高效。
2,惰性删除
放任过期键不管,但是每当从键空间中获取到一个键时,都要检查是否过期,如果过期执行删除操作,否则返回该键。
该策略对cpu时间最友好,不会在删除其他无关的过期键上花费cpu时间,但是对内存最不友好。大量的过期键得不到删除会占用大量的内存,甚至是内存泄露。
3,定期删除
每隔一段时间对数据库进行一次检查,删除遇到的过期键。每次检查多少个键,多少个数据库,由算法决定。
该策略是前两种策略的折中;
1,每隔一段时间执行一次过期键的删除,并通过限制删除操作执行的时长和频率来减少对cpu时间的影响
2,定期删除,也有效的减少了大量过期键带来的内存浪费。
该策略的难度在于删除操作执行的时长和频率:
1,太频繁,退化为定时删除,占用大量cpu时间
2,太少,退化为惰性删除,内存浪费
6,redis的过期键删除策略
redis同时使用惰性删除和定期删除两种策略。在CPU时间和内存之间取得平衡。
惰性删除。所有读写数据库的redis命令在执行之前都会调用db.c/expireIfNeeded检查是否过期,实现如下:
1,如果键过期了,expireIfNeeded函数将输入键从数据库中删除
2,如果键未过期,expireIfNeeded不做操作
注:
1,当键存在且未过期时,命令按照键存在的情况执行
2,当键不存在或者过期时,命令按照键不存在的情况执行
定期删除。redis.c/activeExpireCycle函数实现了定期删除,当服务器周期性操作redis.c/serverCron函数执行时activeExpireCycle被执行:
1,函数每次运行都从一定数量的数据库中随机选取一定数量的键进行检查并删除其中的过期键。数量分别由REDIS_DBCRON_DBS_PER_CALL和ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP指定。
2,全局变量current_db记录当前检查进度。每检查一个数据库,current_db+1,下一次执行同样从current_db开始
3,当所有数据库都被检查一遍后,current_db置0
7,AOF、RDB和复制功能对过期键的处理
生成RDB文件。使用save或者bgsave命令创建一个新的RGB文件时,程序会对数据库中的键进行检查,过期的键并不会写入到新创建的RDB文件中。
载入RDB文件。启动redis数据库时,如果启用了RDB功能,那么服务器将对RDB文件进行载入:
1,redis服务器以主服务器模式运行,载入RDB文件时,程序会对文件中保存的键进行检查,过期键不会被载入到数据库中
2,redis服务器以从服务器模式运行,载入RDB文件时,文件中的所有键无论是否过期都会被载入到数据库。但是主从同步时,从数据库的数据会被清空,所以过期键对载入RDB文件也不会有影响。
AOF文件写入。在AOF持久化模式下,如果键已过期但是还没有被删除,那么对AOF文件不会有影响。当过期键被删除时,程序会向AOF文件append一条del命令,显式记录该键已被删除。
AOF文件重写时的情况和生成RDB文件的情况是一样的。
主从复制模式下服务器的过期键删除由主服务器控制,从而保证了主从数据一致性:
1,主服务器删除过期键时,向所有从服务器发送del命令,告知删除这个过期键
2,从服务器执行客户端命令时,及时遇到过期键也不会将过期键删除,而是将过期键当做非过期键处理
3,从服务器只有收到主服务器的del通知之后,才会删除相应的过期键
5,数据库通知
使用subscribe命令订阅针对键或者命令的操作信息,来获知键的变化。如:

subscribe __keyspace@0__:

此外,服务器配置notify_keyspace_events选项决定了服务器发送通知的类型:
AKE———-服务器发送所有键空间通知和键事件通知
AK————服务器发送所有键空间通知
AE————服务器发送所有键事件通知
K$————服务器发送所有和字符串相关的键空间通知
El————-服务器发送所有和列表键相关的键事件通知
发送通知。当命令被执行的时候,如果执行成功后会调用notify.c/notifyKeyspaceEvent函数发送通知。伪代码如下:

def notifyKeyspaceEvent(type,event,key dbid):
    if 通知类型type不是server允许发送通知类型:
          return
    #发送键空间通知
    if server.notify_keyspace_events & NOTIFY_KEYSPACE
          #将通知发送给频道__keyspace@__:,内容为发生的事件通知
          chan = __keyspace@__:
          pubsubPublishMessage(chanobj, eventobj);
    #发送键事件通知
    if server.notify_keyspace_events & NOTIFY_KEYEVENT
          #将通知发送给频道__keyspace@__:,内容为发生的事件通知
          chan = __keyspace@__:
          pubsubPublishMessage(chanobj, keyobj);

你可能感兴趣的:(数据库)