Redis数据库实现原理(划重点)

Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中,db数组的每一项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库,服务器设置dbnum属性为初始数据库的个数,这个属性一般由数据库服务器配置conf文件中的database节点来配置,默认情况下这个初始值是16。

struct redisServer{    //数据库    redisDb *db;    //服务器数量    int dbnum;};

数据库切换

前面我们说到默认的redis数据库有16个,那么我们如何来切换数据库呢?

127.0.0.1:6379> get name"mango"127.0.0.1:6379> select 1OK127.0.0.1:6379[1]> get name(nil)127.0.0.1:6379[1]> select 0OK127.0.0.1:6379> get name"mango"

默认情况下我们操作的都是第一个数据库,数据库数组索引为[0],我们通过select命令来切换数据库,通过修改客户端的指针,来指向第二个数据库。

注意:这里我们值得注意的是,我们在维护redis的时候执行一些敏感的指令时养成一个习惯就是先切换数据库,然后执行。

数据库键空间

Redis 中的每个数据库,都由一个 redis.h/redisDb 结构表示,下面我们来看看代码。

typedef struct redisDb {    // 保存着数据库以整数表示的号码    int id;    // 保存着数据库中的所有键值对数据,这个属性也被称为键空间(key space)    dict *dict;    // 保存着键的过期信息    dict *expires;    // 实现列表阻塞原语,如 BLPOP    dict *blocking_keys;    dict *ready_keys;    // 用于实现 WATCH 命令,在事务中有提到    dict *watched_keys;} redisDb;

因为Redis本身就是一个字典类型的数据库,我们可以看到在RedisDb中,dict保存数据库的所有键值对数据。

字典的键是一个字符串对象(StringObject)

字典的值则可以包括Redis的五种基础结构的任意一种(字符串、列表、哈希表、集合或有序集),例如:列表就是一个ListObject,哈希就是HashObject等。

如图所示,每个类型对应数据结构不同。

关于Redis数据库的增、删、改、查操作这里就不过多解释了,跟java或是c#中的实现是相似的,只不过对于字典扩容稍有不同,Redis的扩容是一种渐进式的方式,一点点的将旧表迁入新表,不会在扩容的时候阻塞其他操作。

其他操作
除了增删该查的键值操作之外,还有很多针对数据库本身的命令,也是通过对键空间进行处理:
• FLUSHDB 命令:删除键空间中的所有键值对。
• RANDOMKEY 命令:从键空间中随机返回一个键。
• DBSIZE 命令:返回键空间中键值对的数量。
• EXISTS 命令:检查给定键是否存在于键空间中。
• RENAME 命令:在键空间中,对给定键进行改名。

过期设置

数据过期操作命令通过EXPIRE、PEXPIRE、EXPIREAT和PEXPIREAT四个命令,客户端可以给某个存在的键设置过期时间,当键的过期时间到达时,键就不再可用。当然SETEX也是可以的,只不过它是一个限定类型命令(可以说是一个复合命令),跟其他的过期时间设置原理一样。

>set name mango(integer)1>expire name 5    #设置过期时间

过期时间是一个UNIX时间戳,当键的过期时间到了,这个键就会在数据库中被删除。那么我们可以通过TTL和PTTL命令来返回距离过期时间还有多长时间

> SETEX key 10086 valueOK> TTL key(integer) 10082> PTTL key(integer) 10068998

过期时间保存

前面我们给出的RedisDb里面的一个属性dict *expires是保存过期时间的,我们可以看到它也是一个dict键值的指针类型,其中这个过期字典中的是一个long long类型的整数,这个整数保存的是过期时间。下面我们来画一个图加深印象。

注意:这里的过期时间的字典是指向的键空间的,只不过为了区分才这样画的。

如何检测key是否过期?

  1. 检查这个key是否存在expires中

  2. 检查当前时间是否大于过期时间,如果大於则过期,反之未过期

过期key删除策略

Redis设置好过期时间后,如果key的过期时间到期,我们的redis没有及时回收资源可能会导致Redis内存溢出,所以我们需要及时清理过期的key来释放资源。

定时清理:

此方法就是我设置一个定时器,到点了我就去清理已经过期的key。

优点:就是方便简洁,保证key过期及时处理;

缺点:也很明显,扫描整个字典是一个O(n)的时间复杂度,使用这种方式的时候会占用大量的cpu可能会导致服务器卡顿等现象,我们说过Redis尽量避免使用这类的操作,这样处理并不高效。

惰性清理:

这种方式就是客户端请求这个key的时候去判断时间是否过期,过期则清理。

优点:对cup很友好,使用的时候去清理

缺点:对内存不友好,无效key不及时清理内存得不到释放,积压的内存越来越多,如果过期key特别多而且永远不访问,可能会导致内存溢出。

定期清理:

定期删除是结合定是清理和惰性清理的特点选择一个折中处理,一段时间内处理一定量的key,这样减少使用cpu带来的阻塞,一定程度的减少内存积压。

定期清理的难点在于清理算法,也就是说什么时候清理多少key这个量度是很难把握的。

那么我们介绍了这三种清理方式后,Redis的清理过期key的方式是通过惰性清理和定期清理两种策略来实现的。通过这两种方式使Redis在cpu和内存之间取得平衡,这两种方式也是在不同的时期进行的。

过期键对AOF、RDB和复制的影响
前面的内容讨论了过期键对cpu和内存的影响,那么过期键在RDB文件、AOF 文件、AOF重写以及复制中的影响。
RDB文件在创建新的RDB文件时,程序会对键进行检查,过期的键不会被写入到更新后的RDB文件中,过期键对更新后的RDB文件没有影响。

AOF文件在键已经过期,但是还没有被惰性删除或者定期删除之前,这个键不会产生任何影响,AOF文件也不会因为这个键而被修改。当过期键被惰性删除、或者定期删除之后,程序会向AOF文件追加一条DEL命令,来显式地记录该键已被删除。
AOF重写时程序对键进行检查,过期的键不会被保存到重写后的AOF文件。过期键对重写后的AOF文件没有影响。
复制:当服务器带有附属节点时,过期键的删除由主节点统一控制:

  • 如果服务器是主节点,那么它在删除一个过期键之后,会显式地向所有附属节点发送一个DEL命令。
  • 如果服务器是附属节点,那么当它碰到一个过期键的时候,它会向程序返回键已过期的回复,但并不真正的删除过期键。因为程序只根据键是否已经过期、而不是键是否已经被删除来决定执行流程,所以这种处理并不影响命令的正确执行结果。当接到从主节点发来的DEL命令之后,附属节点才会真正的将过期键删除掉。附属节点不自主对键进行删除是为了和主节点的数据保持绝对一致,因为这个原因,当一个过期键还存在于主节点时,这个键在所有附属节点的副本也不会被删除。这种处理机制对那些使用大量附属节点,并且带有大量过期键的应用来说,可能会造成一部分内存不能立即被释放,但是,因为过期键通常很快会被主节点发现并删除

 

总结一下

  1. Redis默认有16个数据库,但如果没有切换那么使用的是第一个默认的数据库

  2. 数据库主要由dict和expires两个字典构成,其中dict保存键值对,而expires则保存键的过期时间。
    数据库的键总是一个字符串对象,而值可以是任意一种 Redis 数据类型,包括字符串、哈希、集合、列表和有序集。

  3. expires的某个键和dict的某个键共同指向同一个字符串对象,而expires 键的值则是该键以毫秒计算的 UNIX 过期时间戳。

  4. Redis使用惰性删除和定期删除两种策略来删除过期的键。

  5. 更新后的RDB文件和重写后的AOF文件都不会保留已经过期的键。

  6. 当一个过期键被删除之后,程序会追加一条新的DEL命令到现有AOF文件末尾。

  7. 当主节点删除一个过期键之后,它会显式地发送一条DEL命令到所有附属节点,而从节点是不会主动删除某个key,这样保证主从一致性问题。

 

一名正在抢救的coder

笔名:mangolove

CSDN地址:https://blog.csdn.net/mango_love

GitHub地址:https://github.com/mangoloveYu

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章