2_1_Redis设计与实现读书记-数据库

367 阅读11分钟

1. 服务器中的数据库

Redis中服务器所有的数据库都保存着服务器撞他redisServer结构的db数组中。db数组每项都是一个redisDb结构,每个结构代表一个数据库。

struct redisServer{
	// ...
    // 一个数组,保存着Redis服务器中所有的数据库
	redisDb *db;
  	// Redis服务器的数据库数量
 	int dbnum;
    // ...
};

初始化服务器时,根据服务器状态的dbnum属性决定创建多少个数据库。默认情况下,dbnum值为16。

2. 切换数据库

每个redis客户端都有自己的目标数据库,当客户端执行数据库写或者读命令时,目标数据库就会成为这些命令的操作对象。

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

在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,此属性是一个指向redisDb结构的指针。

typedef struct redisClient{
    // ...
    //记录客户端当前正在试用的数据库
    redisDb *db;
    // ...
}redisClient;

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

通过修改redisClient.db指针,指向服务器中不同数据库,从而实现切换目标数据库的功能。

注意: 因为目前还没有返回客户端目标数据库的命令。为了安全起见,在操作数据库时,最好先执行一个SELECT命令显示地切换到指定的数据库。

3. 数据库键空间

Redis是一个键值对数据服务器,每个数据库都有redisDb结构表示,其中redisDb结构的dict字典保存了数据库中的所有键值对,则成这个字典为键空间。

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

键空间和用户所见的数据库是直接对应的:

  • 键空间的键也就是数据库的键,每个键都是一个字符串对象。
  • 键空间的值也就是数据库的值,每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象中的任意一种Redis对象。

数据库空间的例子:

因为数据库键空间是一个字典,所以针对数据库的操作,都是通过对键空间字典进行操作实现的。

3.1 添加新键

添加一个新的键值对到数据库,就是将一个新的键值对添加到键空间字典中。键为字符串对象,值为Redis对象。

3.2 删除键

实际上就是在键空间里面删除键对应的键值对对象。

3.3 更新键

对键空间里面的键所对应的值对象进行更新,更具值对象的类型不同,更新的刚发也有所不同。

3.4 对键取值

就是在键空间中取出键所对应的值对象,根据值对象的类型不同,具体的取值方法也有所不同。

3.5 其他键空间操作

除了CRUD操作,对数据库本身的Redis命令,也通过对键空间进行处理完成。

如:对于清空整个数据库的FLUSHDB命令,通过删除键空间中的所有的键值对来实现。EXISTS、RENAME、KEYS,DBSIZE、RANDOMKEY等命令。

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

对数据库读写时,服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作。

  • 读取一个键之后,服务器会根据键是否存在来更新服务器的键命名空间命中次数或键空间不命中次数。可以再INFO stats命令科keyspace_hits属性和keyspace_misses属性中查看。
  • 读取一个键之后,服务器会更新键的LRU(最后一个被引用)时间,可以用于计算键的闲置时间,OBJECT idletime命令可以查看key的闲置时间。
  • 读取一个键,发现键已经过去,则会先删除过期键,再执行余下的其他操作。
  • 如果watch命令监视了某个键,在这个键修改之后,会将键标记为脏(dirty),让事务程序注意这个键已经被修改过。
  • 服务器每次修改一个键后,都会对脏(dirty)键计数器的值增1,这个计数器会出发服务器的持久化以及复制操作。
  • 如果服务器开启了数据库通知功能,则在对键进行修改之后,服务器将按配置发送对应的数据库通知。

4. 设置键

通过EXPIRE命令或者PEXPIRE命令,设置秒或毫秒精度为数据库中的键的生存时间,在经过指定的时间后,服务器会自动删除生存时间为0的键。

**注意:**SETEX 命令可以设置一个字符串键的同时为键设置过期时间,因为这个命令时一个类型限定的命令(只用于字符串键)。但是SETEX命令设置过期时间的原理与EXPIRE命令设置过期时间的原理是一样的。

过期时间是一个UNIX时间戳。

TLL命令(tll )和PTLL命令(ptll ) 返回这个键剩余的生存时间。

4.1 设置过期时间

四个不同的命令用于设置键的生存时间或者过期时间

  • EXPIRE 用于将键key的是个生存时间设置为tll秒
  • PEXPIRE 用于设置生存时间为tll毫秒
  • EXPIREAT 设置key的过期时间为timestamp指定的秒数时间戳
  • PEXPIREAT 设置键的过期时间为timestamp指定的毫秒数时间戳
def EXPIRE(key,ttl_in_sec):
    ttl_in_ms = sec_to_ms(ttl_in_sec)
    
    PEXPIRE(key, ttl_in_ms)

def PEXPIRE(key,ttl_in_sec):
    now_ms = get_current_unix_timestamp_in_ms()
    
    PEXPIREAT(key, ttl_in_ms+now_ms)
    
def EXPIREAT(key, expire_time_in_sec):
    expire_time_in_ms = sec_to_ms(expire_time_in_sec)
    
    PEXPIREAT(key,expire_time_in_ms)

则,EXPIRE、PEXIRE和EXPIREAT三个命令,最终都会转换成PEXPIREAT命令来执行。

4.2 保存过期时间

redisDb结构的expire字典保存了数据库中所有键的过期时间,成为过期字典。

  • 过期字典就是一个指针,指向键空间中的某个键对象
  • 字典的值是一个long long类型的整数,保存了键所指向的数据库键的过期时间,一个毫秒精度的UNIX时间戳。
typedef struct redisDb{
    // ...
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;
    // 过期字典,保存着键的过期时间
    dict *expires;
}redisDb;

当执行PEXIREPEAT命令为一个键设置过期时间时,服务器会在数据库的过期字典中关联给定的数据库键和过期时间。

4.3 移除过期时间

PERSIST命令可以移除一个键的过期时间,就是对PEXPIREAT命令的反操作,执行PERSIST命令后,会在过期字典中找到给定的键,并解除键和值在过期字典中的关联。

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

TLL命令一秒为单位返回键的剩余生存时间,PTLL则以毫秒为单位返回键的剩余生存时间。都是通过计算键的过期时间和当前时间之间的差来实现。

4.5 过期键的判定

通过过期字典,检测一个给定键是否过期:

  • 检查给定键是否存在于过期字典
  • 检查当前UNIX时间戳是否大于键的过期时间

5. 过期键删除策略

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

  • 定时删除:在设置过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,对键执行删除操作。(主动删除)
  • 惰性删除:放任键过期不管,内次获取键时,都检查键是否过期,过期则执行删除操作。(被动删除)
  • 定期删除:每隔一段时间,对数据库进行一次检查,删除过期的键。(主动删除)
5.1 定时删除

这种方式对内存是最友好的,通过定时器,定时删除策略可以保证过期键会尽可能地被删除,并释放过期键所占用的内存。

缺点:

  • 对CPU时间不友好,在键较多时,删除操作会占用相当一部分的CPU时间,会影响服务器的响应时间和吞吐量。
  • 创建一个定时器需要服务器中的时间事件,实现方式采用过的是无序链表。查找一个事件的复杂度为O(N),并不能高效地处理大量时间事件。
5.2 惰性删除

对CPU时间是友好的,只会在取出键时才对键进行过期检查。删除的目标仅限于当前处理的键,不会在其他无关过期键上花费任何CPU时间。

缺点:

  • 对内存不友好,不删除,键占用的内存就不会释放。
  • 数据库中有非常多的过期键,又没有被访问,则永远不会被删除。可以将这种情况看作是一种内存泄露。大量无用的数据占用内存空间,服务器却不去释放。
5.3 定期删除

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

难点:

  • 删除过于频繁或者时间过长,会退化成定时删除
  • 如果删除操作执行太少或者执行时间过短,会和惰性删除一样,出现内存浪费的情况。

6. Redis的过期键删除策略

6.1 惰性删除策略实现

在所有读写数据库的命令在执行前会调用expireIfNeeded函数对键进行检查,若过期则函数执行删除键操作,否则不做任何操作。

6.2 定期删除策略的实现

当Redis的服务器周期性操作serverCorn函数执行时,activeExpireCycle函数就会被调用,在规定时间内,多次遍历服务器中的各个数据库,从expires字典中随机检查一部分键的过期时间,删除过期键。

activeExpireCycle函数的工作模式:

  • 每次运行,都从一定数量的数据库中取出一定数量的随机键进行检查,删除过期键。
  • 全局遍历current_db会记录当前函数的检查进度,并在下一次函数调用时,接着上一次的进度进行处理。
  • 随着函数的执行,数据库都会被检查一遍。这时current_db变量重置为0,然后进行下一轮的检查。

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

7.1 生成RDB文件

在执行SAVE或者BGSAVE命令生成RDB文件时,进行数据库键的检查,已过期的键不会被保存到新创建的RDB文件中。

7.2 载入RDB文件

如果服务器开启了RDB功能,将对RDB文件进行载入:

  • 如果以主服务器模式运行,在载入RDB文件时,会对文件中保存键进行检查,未过期的载入,过期的键则会被忽略。
  • 如果以从服务器模式运行,在载入RDB文件时,文件中保存的所有键都不会载入到数据库中。因为主服务器在数据同步时,从服务器的数据库就会被清空。
7.3 AOF 文件写入

服务器以AOF模式运行时,如果某个键过期,但是还没有被惰性删除或者定期删除,则AOF文件不会因为这个过期键而产生任何影响。

当过期键被惰性删除或者定期删除后,会在AOF文件中追加一条DEL命令,显示地记录键已被删除。

7.4 AOF重写

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

7.5 复制

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

  • 主服务器删除一个过期键之后,会显示的向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键。
  • 从服务器在执行接收到的命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样处理过期键。
  • **从服务器只有在接到主服务器发来的DEL命令后,才会删除过期键。**在此之前,从服务器的过期键仍然能够正常操作。

8. 数据库通知

9. 回顾