Redis单机数据库的实现,持久化以及过期淘汰策略

服务器中的数据库

服务器会将所有的数据库都保存在 RedisServer 的 db数组中,每一个 redis db 都代表一个数据库。

struct redisServer {
	...
    // 数据库
    redisDb *db;
    //服务器初始化时,创建多少个数据库,默认16
    int dbnum; 
    ...	
   };

在这里插入图片描述
所以切换数据库就相当于改变了redisClient 中的一个redisDb指针。

数据库的实现

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

在这里插入图片描述
增删改查自然就不用说了。

另外,对于操作键空间时,redis 还会做一些额外的操作。

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

接下来我们一点一点的来看,通过一个点,整体打入 Redis 。

3. 键的过期时间(生存时间)

使用的命令如下:
在这里插入图片描述
最终的底层实现都是:PEXPIREAT

通过命令,就能够设置一个键的过期时间(TTL),在到时间后,服务器就会自动删除TTL为0的键。

在 redisDb 中也有一个字典保存着所有键的过期时间。称这个字典为过期字典。

typedef struct redisDb {
	...
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;  //称为键空间    
        
 	// 键的过期时间,字典的键为指针,指向某个键对象
  	//字典的值为过期事件 UNIX 时间戳  
    dict *expires;     
    ...
};
(1)过期键的删除策略
  • 定时删除
  • 惰性删除 (SDS)
  • 定期删除
定时删除

定时器 。如果很多会占用CPU时间哦!

惰性删除 (类似于SDS)

当开始取键的时候才检查,如果过期就删除。如果永远取不到它,那就会造成内存泄漏喽!

定期删除
  • 每隔一段时间执行删除,控制时长和频率。(类似于TCP的拥塞控制,负载因子)。主要难点在与如何定义“限制“。
Redis采用的删除策略

惰性删除 + 定期删除
在这里插入图片描述

4. watch 命令

先浏览:Redis事务的实现

WATCH命令是-一个 乐观锁( optimistic locking),它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。

如何实现的呐?

typedef struct redisDb {
	...
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;  //称为键空间    
        
 	// 键的过期时间,字典的键为指针,指向某个键对象
  	//字典的值为过期事件 UNIX 时间戳  
    dict *expires;   
      
	// 正在被 WATCH 命令监视的键
	dict *watched_keys; 
    ...
};

具体结构:
在这里插入图片描述如何触发:会有标识 REDIS_DIRTY_CAS
在这里插入图片描述

数据结构持久化(将数据存储到硬盘)

Redis 中,键的数据类型是字符串,但是为了丰富数据存储的方式,方便开发者使用,值的数据类型有很多,常用的数据类型有这样几种,它们分别是字符串、列表、字典、集合、有序集合。

Redis 的数据格式由“键”和“值”两部分组成。而“值”又支持很多数据类型,比如字符串、列表、字典、集合、有序集合。像字典、集合等类型,底层用到了散列表,散列表中有指针的概念,而指针指向的是内存中的存储地址。 那 Redis 是如何将这样一个跟具体内存地址有关的数据结构存储到磁盘中的呢?

主要有两种解决思路:

  • 第一种是清除原有的存储结构,只将数据存储到磁盘中。当我们需要从磁盘还原数据到内存的时候,再重新将数据组织成原来的数据结构。实际上,Redis 采用的就是这种持久化思路

不过,这种方式也有一定的弊端。那就是数据从硬盘还原到内存的过程,会耗用比较多的时间。比如,我们现在要将散列表中的数据存储到磁盘。当我们从磁盘中,取出数据重新构建散列表的时候,需要重新计算每个数据的哈希值。如果磁盘中存储的是几 GB 的数据,那重构数据结构的耗时就不可忽视了。

  • 第二种方式是保留原来的存储格式,将数据按照原有的格式存储在磁盘中。我们拿散列表这样的数据结构来举例。我们可以将散列表的大小、每个数据被散列到的槽的编号等信息,都保存在磁盘中。有了这些信息,我们从磁盘中将数据还原到内存中的时候,就可以避免重新计算哈希值。
课后思考:
(1)你有没有发现,在数据量比较小的情况下,Redis 中的很多数据类型,比如字典、有序集合等,都是通过多种数据结构来实现的,为什么会这样设计呢?用一种固定的数据结构来实现,不是更加简单吗?

答:redis的数据结构由多种数据结构来实现,主要是出于时间和空间的考虑,当数据量小的时候通过数组下标访问最快、占用内存最小,而压缩列表只是数组的升级版;

因为数组需要占用连续的内存空间,所以当数据量大的时候,就需要使用链表了,同时为了保证速度又需要和数组结合,也就有了散列表

对于数据的大小和多少采用哪种数据结构,相信redis团队一定是根据大多数的开发场景而定的。

(2)我们讲到数据结构持久化有两种方法。对于二叉查找树这种数据结构,我们如何将它持久化到磁盘中呢?

答:只存数据,可以填充节点使之成为完全二叉树的模样,然后以数组存储下来,之后恢复的时候,插入恢复就行了!!还是比较高效的.

5. 持久化 和 dirty 计数器的值的关系

RDB 持久化

在这里插入图片描述
创建:SAVE(阻塞服务器), BGSAVE (子进程创建RDB文件)
载入:服务器会一直处于阻塞状态,直到载入完成。

什么情况下会进行持久化:

  • save 配置文件(dirty 计数器和 lastsave 属性 )

save 配置文件
在这里插入图片描述
实现:

struct redisServer{
	// 保存条件的数组
	struct saveparam *saveparams;
};


// 服务器的保存条件(BGSAVE 自动执行的条件)
struct saveparam {

    // 多少秒之内
    time_t seconds;

    // 发生多少次修改
    int changes;
};

dirty 计数器和 lastsave 属性

  • dirty 计数器记录的是距离上一次成功执行SAVE等命令之后,进行了多少次修改。
  • lastsave 上一次SAVE成功执行的时间。

RDB 文件结构:

在这里插入图片描述

  • REDIS:判断是不是RDB文件 。
  • db_version:版本
  • database:所有实际的 键值对 数据
  • EOF:结束标记
  • check_sum:8字节,校验和。与前四个部分有关。

那么他是如何处理过期键的呐

生成 RDB 时会过滤
载入时,分两种情况:

  • 主服务器:过滤
  • 从服务器:不过滤

还记得它们是如何完成数据同步的吗?发送过来住服务器的整个RDB文件哦。相当于直接从 0 重新刷新数据。

AOF 持久化

保存的是 服务器 执行的写命令。
在这里插入图片描述

AOF 是一种 写后日志形式

优势:

  • 在命令执行后才记录日志,所以不会阻塞当前的写操作
  • 避免出现记录错误命令的情况。因为是:先让系统执行命令,只有命令能执行成功,才会被记录到日志中

劣势:

  • 刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险
  • AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。这是因为,AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了

AOF的三种写回策略

AOF 机制给我们提供了三个选择,也就是 AOF 配置项 appendfsync 的三个可选值。

  1. Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘
  2. Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
  3. No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘

AOF 文件太大了怎么办?

针对于过大文件,AOF 提供了重写功能来进行压缩。

AOF 重写功能:就是将很多条“相似”命令合并,但是这种合并是通过读取当前数据库的状态来实现的。会大幅度减少命令的冗余。

这个功能交给子进程实现。但是就会出现:在完成 AOF 重写后,新的命令又改变了数据的窘境。如何解决呐?

其实和EPOLL的实现(ovflist)一样,就是找个地方先把这些命令放着,然后重写完之后,在把它们搞进去。
AOF写入:在后面过期删除后会向 AOF文件中追加一条DEL命令。
AOF重写:重写时过滤。

复制

见:Redis 复制

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

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

这里我想到的就是:如果请求到从服务器,明明过期的数据,你还返回不是有问题的吗?

6. 数据库通知略

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值