服务器中的数据库
服务器会将所有的数据库都保存在 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 持久化
保存的是 服务器执行的写命令。
那么还记得 InnoDB引擎是怎么做的吗?基于语句的复制+基于行的复制
AOF重写功能:就是将很多条“相似”命令合并,但是这种合并是通过读取当前数据库的状态来实现的。会大幅度减少命令的冗余。
这个功能交给子进程实现。这是就会出现:在完成AOF重写后,新的命令又改变了数据的窘境。如何解决呐?
其实和EPOLL的实现(ovflist)一样,就是找个地方先把这些命令放着,然后重写完之后,在把它们搞进去。
AOF写入:在后面过期删除后会向 AOF文件中追加一条DEL命令。
AOF重写:重写时过滤。
复制
见:Redis 复制
当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:
- 主服务器在删除-一个过期键之后,会显式地向所有从服务器发送-一个DEL命令,告知从服务器删除这个过期键。
- 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键。
- 从服务器只有在接到主服务器发来的DEL命令之后,才会删除过期键
这里我想到的就是:如果请求到从服务器,明明过期的数据,你还返回不是有问题的吗?
6. 数据库通知略
哎, 目前主要是没有太多的时间,如果有的话,好想自己实现一个。