redis源码之数据库

未完待续。。

数据库

1.服务器中的数据库

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

/*
*服务器状态
*/
struct redisServer {
...
//一个数组,保存着服务器中的所有数据库,指向redisDb 结构,每个redisDb 结构代表一个数据库
redisDb *db;
//服务器的默认数据库数量,由服务器配置的database 选项决定,默认情况下值为16,
//所以Redis 服务器默认会创建 16 个数据库,
int dbnum;
...
}

2.切换数据库

每个Redis 客户端都有自己的目标数据库作为操作对象。默认为0 号数据库,但客户端可以通过执行SELECT 命令来切换目标数据库。例如:SELECT 2切换到2号数据库。

/*
*客户端状态
*/
typedef struct redisClient {
...
//记录客户端当前正在使用的数据库,这个属性是一个指向 redisDb 结构的指针
redisDb *db;
...
}redisClient;

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

3.数据库键空间

服务器中的每个数据库都由一个redis.h/redisDb 结构表示,其中, redisDb 结构的dict 字典保存了数据库中的所有键值对,我们将这个字典称为键空间( key space ) :

typedef struct redisDb{
...
//数据库键空间,保存着数据库中的所有键值对
dict *dict;
...
}redisDb;

例子如下:
数据库的键空间将会是图9-4 所展示的样子:

redis> SET message "hello world"
OK
redis> RPUSH alphabet "a" "b" "c"
(integer)3
redis> HSET book name "Redis in Action"
(integer) 1
redis> HSET book author "Josiah L. Carlson"
(integer) 1
redis> HSET book publisher "Manning"
(integer) 1

操作——通过对键空间进行处理来完成的:
1.添加键(键-值都添加),删除键(键-值都删除),更新键(值的更新)
2.对键取值
3.用于清空整个数据库的FLUSHDB 命令,随机返回数据库中某个键RANDOMKEY 命令,于返回数据库键数量的DBSIZE 命令,类似的命令还有EXISTS、RENAME、KEYS 等,这些命令都是通过对键空间进行操作来实现的。

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

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

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

通过EXPIRE 命令或者PEXPIRE 命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间( Time To Live, TTL ),在经过指定的秒数或者毫秒数之后,服务器就会自动删除生存时间为0 的键.
同样的,EXPIREAT命令或PEXPIREAT命令,以秒或者毫秒精度给数据库中的某个键设置过期时间( expire time )。当键的过期时间来临时,服务器就会自动从数据库中删除这个键。
TTL 命令和PTTL命令接受一个带有生存时间或者过期时间的键,返回这个键的剩余生存时间。

具体怎么删除呢?是不是通过定时器,然后遍历所有的时间戳?
数据库如何保存键的生存时间和过期时间,以及服务器如何自动删除那些带有生存时间和过期时间的键?

1.实际上EXPIRE、PEXPIRE 、EXPIREAT三个命令都是使用PEXPIREAT命令来实现的(转化为毫秒级的过期时间):
2.redisDb 结构的expires 字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典
过期字典的键是一个指针,指向某个数据库键;值是一个 long long 类型的整数,这个整数保存了键所指向的数据库键的过期时间一一1个毫秒精度的UNIX 时间戳。

typedef struct redisDb {
    //过期字典,保存着键的过期时间
    diet *expires;
}

这里写图片描述
伪代码:

def PEXPIREAT(key, expire_time_in_ms):
//如果给定的键不存在于键空间,那么不能设置过期时间
if key not in redisDb.dict:
returnO
//在过期字典中关联键和过期时间
redisDb.expires[key] = expire_time_in_ms
//过期时间设置成功
return 1

3.移除过期时间
PERSIST message
PERSIST命令就是PEXPIREAl’命令的反操作: PERSIST命令在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联。
伪代码:

def PERSIST(key):
//如果键不存在,或者键没有设置过期时间,那么直接返回
if key not in redisDb.expires:
return O
//移除过期字典中给定键的键值对关联
redisDb.expires.remove(key)
//键的过期时间移除成功
return 1

4.计算并返回剩余生存时间
TTL和PTTL 两个命令都是通过计算键的过期时间和当前时间之间的差来实现的(TTL然后将差值从毫秒转换为秒之后得出的)。

5.过期键的判定
1 )检查给定键是否存在于过期字典z 如果存在,那么取得键的过期时间。
2 )检查当前UNIX 时间戳是否大于键的过期时l曰:如果是的话,那么键已经过期;否则的话,键未过期。
伪代码:

def is_expired(key):
#取得键的过期时间
expire_time_in_ms = redisDb.expires.get(key)
#键没有设置过期时间
if expire_time_in_ms is None:
return False
#取得当前时间的UNIX 时间戳
now_ms = get_current_unix_timestamp_in_ms ()
#检查当前时间是否大于键的过期时间
if now_ms > expire_time_in_ms:
#是,键已经过期
return True
else:
#否,键未过期
return False

6.过期键删除策略
三种不同的删除策略:

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

在这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略。

1)定时删除
定时删除策略对内存是最友好的:通过使用定时器,定时删除策略可以保证过期键会尽可能快地被删除,并释放过期键所占用的内存。
另一方面,定时删除策略的缺点是,它对CPU 时间是最不友好的:删除过期键这一行为可能会占用相当一部分CPU 时间,在内存不紧张但是CPU 时间非常紧张的情况下,将CPU 时间用在删除和当前任务无关的过期键上,无疑会对服务器的响应时间和吞吐量造成影响。
除此之外,创建一个定时器需要用到 Redis 服务器中的时间事件,而当前时间事件的实现方式一一无序链表,查找一个事件的时间复杂度为O(N)一一并不能高效地处理大量时间事件。因此,要让服务器创建大量的定时器,从而实现定时删除策略,在现阶段来说并不现实。(为什么不使用效率更高的定时器)
2)惰性删除
惰性删除策略对CPU 时间来说是最友好的,但是它对内存是最不友好的。
如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除(除非用户手动执行FLUSHDB ),我们甚至可以将这种情况看作是一种内存泄漏。
举个例子,对于一些和时间有关的数据,比如日志( log ),在某个时间点之后,对它们的访问就会大大减少,甚至不再访问,如果这类过期数据大量地积压在数据库中,用户以为服务器已经自动将它们删除了,但实际上这些键仍然存在,而且键所占用的内存也没有释放,那么造成的后果肯定是非常严重的。
3)定期删除
从上面对定时删除和惰性删除的讨论来看,这两种删除方式在单一使用时都有明显的缺陷:

  • 定时删除占用太多CPU 时间,影响服务器的响应时间和吞吐量。
  • 惰性删除浪费太多内存,有内存泄漏的危险。

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

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

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

  • 如果删除操作执行得太频繁,或者执行的时间太长,定期删除策略就会退化成定时删除策略,以至于将CPU 时间过多地消耗在删除过期键上面。
  • 如果删除操作执行得太少,或者执行的时间太短,定期删除策略又会和惰性删除策
    略一样,出现浪费内存的情况。

5.Redis 的过期键删除策略

Redis 服务器实际使用的是惰性删除和定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU 时间和避免浪费内存空间之间取得平衡。
1.惰性删除策略的实现:
db.c/expireifNeeded
所有读写数据库的Redis 命令在执行之前都会调用expireifNeeded 函数对输入键进行检查:

  • 如果输入键已经过期,那么expireIfNeeded 函数将输入键从数据库中删除。
  • 如果输入键未过期,那么expireifNeeded 函数不做动作。
  • 当键不存在或者键因为过期而被expireifNeeded 函数删除时,命令按照键不存在的情况执行。
    这里写图片描述

2.定期删除策略的实现:
redis.c/activeExpireCycle
每当Redis 的服务器周期性操作redis.c/serverCron 函数执行时, activeExpireCycle 函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的e xpires 字典中随机检查一部分键的过期时间,并删除其中的过期键。

#默认每次检查的数据库数量
DEFAULT DB NUMBERS= 16
#默认每个数据库检查的键数量
DEFAULT KEY NUMBERS= 20
#全局变量,记录检查进度
current db= 0
def activeExpireCycle () :
    #初始化要检查的数据库数量
    #如果服务器的数据库数量比DEFAULT DB NUMBERS 要小那么以服务器的数据库数量为准
    if server.dbnum < DEFAULT DB NUMBERS:
        db numbers= server.dbnum
    else:
        db numbers= DEFAULT DB NUMBERS
    #遍历各个数据库
    for i in range(db_numbers):
        #如果current db 的值等于服务器的数据库数量,这表示检查程序已经遍历了服务器的所有数据库一次,
        #将current db 重置为0 ,开始新的一轮遍历
        if current db= server.dbnum:
            current db= 0

        #获取当前要处理的数据库
        redisDb = server.db[current db]
        #将数据库索引增1 ,指向下一个要处理的数据库
        current db+= 1
        #检查数据库键
        for j in range(DEFAULT KEY NUMBERS):
            #如果数据库中没有一个键带有过期时间,那么跳过这个数据库
            if redisDb.expires.size() == 0: break
            #随机获取一个带有过期时间的键
            key with ttl = redisDb.expires.get random key()
            #检查键是否过期,如果过期就删除它
            if is_expired (key_with_ttl):
                delete_key(key_with_ttll
            #已达到时间上限,停止处理
            if reach time limit(): 
                return
  • 函数每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删
    除其中的过期键。
  • 全局变量current db 会记录当前activeExpireCycle 画数检查的进度,并在下一次activeExpireCycle 函数调用时,接着上一次的进度进行处理。比如说,如果当前activeExpireCycle 函数在遍历10 号数据库时返回了,那么下次activeExpireCycle 画数执行时,将从11 号数据库开始查找并删除过期键。
  • 随着activeExpireCycle 函数的不断执行,服务器中的所有数据库都会被检查一遍,这时画数将current db 变量重置为0 ,然后再次开始新一轮的检查工作。

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

探讨过期键对Redis 服务器中其他模块的影响,看看RDB 持久化功能、AOF 持久化功能以及复制功能是如何处理数据库中的过期键的。

1.生成RDB 文件
在执行SAVE 命令或者BGSAVE 命令创建一个新的RDB 文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB 文件中。因此,数据库中包含过期键不会对生成新的RDB 文件造成影响

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

  • 如果服务器以主服务器模式运行,,程序会对文件中保存的键进行检查,未过期的键会被载入到数据库过期键则会被忽略,所以过期键对载入RDB 文件的主服务器不会造成影响。
  • 如果服务器以从服务器模式运行,文件中保存的所有键不论是否过期,都会被载入到数据库中因为主从服务器在进行数据同步的时候,从服务器的数据库就会被清空,所以一般来讲,过期键对载人RDB 文件的从服务器也不会造成影响。

3.AOF 文件写入
当服务器以AOF 持久化模式运行时,

  • 如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF 文件不会因为这个过期键而产生任何影响。
  • 当过期键被惰性删除或者定期删除之后,程序会向AOF 文件追加( append )一条DEL命令,来显式地记录该键已被删除。
    举个例子,如果客户端使用GET message 命令,试图访问过期的message 键,那么服务器将执行以下三个动作:
    1 )从数据库中删除message 键。
    2 )追加一条DEL message 命令到AOF 文件。
    3 )向执行GET命令的客户端返回空回复。

4. AOF 重写
和生成RDB 文件时类似,在执行AOF 重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF 文件中。因此,数据库中包含过期键不会对AOF 重写造成影响。

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

  • 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个DEL 命令,告知从服务器删除这个过期键。
  • 从服务器只有在接到主服务器发来的DEL 命令之后,才会删除过期键。
  • 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键。
    因此可以保证主从服务器数据的一致性,也正是因为这个原因,当一个过期键仍然存在于主服务器的数据库时,这个过期键在从服务器里的复制品也会继续存在。

例如:
如果这时有客户端向从服务器发送命令GET message ,那么从服务器将发message键已经过期,但从服务器并不会删除message 键,而是继续将message 键的值返回给客户端,就好像message 键并没有过期一样;
假设在此之后,有客户端向主服务器发送命令GET message ,那么主服务器将发现键message 已经过期:主服务器会删除message 键,向客户端返回空回复,并向从服务器发送DEL message 命令;从服务器在接收到主服务器发来的DEL message 命令之后,也会从数据库中删除message 键,在这之后,主从服务器都不再保存过期键message 了。

6. 数据库通知

这个功能可以让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。
举个例子,以下代码展示了客户端如何获取 0 号数据库中针对message 键执行的所有命令:
SUBSCRIBE_ _keyspace@0_ _ :message#关注键空间:message 键
根据发回的通知显示,先后共有SET、EXPIRE、DEL 三个命令对键message 进行了操作。
这一类关注“某个键执行了什么命令”的通知称为键空间通知( key-space notification),除此之外,还有另一类称为键事件通知( key-event notification )的通知,它们关注的是“某个命令被什么键执行了”。
以下是一个键事件通知的例子,代码展示了客户端如何获取0 号数据库中所有执行DEL 命令的键:
SUBSCRIBE _ _ keyevent@0 _ _ :del#关注命令:del
根据发回的通知显示, key 、number 、message 三个键先后执行了DEL 命令。

服务器配置的notify-keyspace-events 选项决定了服务器所发送通知的类型:

  • 想让服务器发送所有类型的键空间通知和键事件通知,可以将选项的值设置为AKE。
  • 想让服务器发送所有类型的键空间通知,可以将选项的值设置为AK。
  • 想让服务器发送所有类型的键事件通知,可以将选项的值设置为AE。
  • 想让服务器只发送和字符串键有关的键空间通知,可以将选项的值设置为K$。
  • 想让服务器只发送和列表键有关的键事件通知,可以将选项的值设置为El。
    (一)发送通知
    发送数据库通知的功能是由notify.c/notifyKeyspaceEvent 函数实现的:
void notifyKeyspaceEvent (int type, char *event, robj *key, int dbid);

函数的 type 参数是当前想要发送的通知的类型,程序会根据这个值来判断通知是否就是服务器配置notify-keyspace-events 选项所选定的通知类型,从而决定是否发送通知。
event 、keys 和dbid 分别是事件的名称、产生事件的键,以及产生事件的数据库号
码,函数会根据type 参数以及这三个参数来构建事件通知的内容,以及接收通知的频道名。

每当一个Redis 命令需要发送数据库通知的时候,该命令的实现画数就会调用notifyKeyspaceEvent函数,并向函数传递传递该命令所引发的事件的相关信息。
例如SADD 命令的实现函数和DEL 命令的实现函数,

  • SADD 命令:当SADD 命令至少成功地向集合添加了一个集合元素之后,命令就会发送通知
  • DEL 命令:函数遍历所有输入键,并在删除键成功时,发送通知
    (二)notifyKeyspaceEvent函数的实现
    伪代码:
def notifyKeyspaceEvent(type, event, key, dbid):
    #如果给定的通知不是服务暴允许发送的通知,那么直接返回
    if not(server.notify_keyspace_events & type):
        return
    #发送键空间通知
    if server.notify_keyspace_events & REDIS_NOTIFY_KEYSPACE:
        #将通知发送给频道_keyspace@<dbid>_:<key>
        #内容为键所发生的事件<event>
        #构建频道名字
        chan =”__keyspace@{dbid}_: {key)” .format(dbid=dbid, key=key)
        #发送通知
        pubsubPublishMessage(chan, event)
    #发送键事件通知
    if server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT:
        #将通知发送给频道_keyevent@<dbid>__:<event>
        #内容为发生事件的键<key>
        #构建频道名字
        chan =__keyevent@{dbid}_: {event}".format(dbid=dbid,event=event)
        #发送通知
        pubsubPublishMessage (chan, key)

1)server.notify keys pace_ events 属性就是服务器配置 notify-keyspaceevents 选项所设置的值,如果给定的通知类型 type 不是服务器允许发送的通知类型,那么函数会直接返回,不做任何动作。
2 )如果给定的通知是服务器允许发送的通知,那么下一步函数会检测服务器是否允许发送键空间通知,如果允许的话,程序就会构建并发送事件通知。
3 )最后,函数检测服务器是否允许发送键事件通知,如果允许的话,程序就会构建并发送事件通知。

  • Redis 服务器的所有数据库都保存在redisServer.db 数组中,而数据库的数量则
    由redisServer.dbnum 属性保存。
  • 客户端通过修改目标数据库指针,让它指向redisServer.db 数组中的不同元素
    来切换不同的数据库e
  • 数据库主要由diet 和expires 两个字典构成,其中diet 字典负责保存键值对,
    而expires 字典则负责保存键的过期时间。
  • 因为数据库由字典构成,所以对数据库的操作都是建立在字典操作之上的。
  • 数据库的键总是一个字符串对象,而值则可以是任意一种Redis 对象类型,包括字
    符串对象、晗希表对象、集合对象、列表对象和有序集合对象,分别对应字符键、哈希表键、集合键、列表键和有序集合键。
  • expires 字典的键指向数据库中的某个键,而值则记录了数据库键的过期时间,过
    期时间是一个以毫秒为单位的UNIX 时间戳。
  • Redis 使用惰性删除和定期删除两种策略来删除过期的键:惰性删除策略只在碰到过期键时才进行删除操作,定期删除策略则每隔一段时间主动查找并删除过期键。
  • 执行SAVE 命令或者BGSAVE 命令所产生的新RDB 文件不会包含已经过期的键。
  • 执行BGREWRITEAOF 命令所产生的重写AOF 文件不会包含已经过期的键。
  • 当一个过期键被删除之后,服务器会追加一条DEL 命令到现有AOF 文件的末尾,
    显式地删除过期键。
  • 当主服务器删除一个过期键之后,它会向所有从服务器发送一条DEL 命令,显式地
    删除过期键。
  • 从服务器即使发现过期键也不会自作主张地删除它,而是等待主节点发来DEL 命令,这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。
  • 当Redis 命令对数据库进行修改之后,服务器会根据配置向客户端发送数据库通知。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章