【Redis】 数据库
【Redis】 数据库
Metadata
title: 【Redis】 数据库
date: 2023-07-09 08:37
tags:
- 行动阶段/完成
- 主题场景/数据存储
- 笔记空间/KnowladgeSpace/ProgramSpace/BasicsSpace
- 细化主题/数据存储/Redis
categories:
- 数据存储
keywords:
- 数据存储/Redis
description: 【Redis】 数据库
概述
从 Redis 服务端的实现角度介绍,包括 db 存储,切换,键的存储及过期相关处理。
- Redis服务器的所有数据库都保存在redisServer.db数组中,而数据库的数量则由redisServer.dbnum属性保存。
- 客户端通过修改目标数据库指针,让它指向redisServer.db数组中的不同元素来切换不同的数据库。
- 数据库主要由dict和expires两个字典构成,其中dict字典负责保存键值对,而expires字典则负责保存键的过期时间。
- 因为数据库由字典构成,所以对数据库的操作都是建立在字典操作之上的。
- 数据库的键总是一个字符串对象,而值则可以是任意一种Redis对象类型,包括字符串对象、哈希表对象、集合对象、列表对象和有序集合对象,分别对应字符串键、哈希表键、集合键、列表键和有序集合键。
- expires字典的键指向数据库中的某个键,而值则记录了数据库键的过期时间,过期时间是一个以毫秒为单位的UNIX时间戳。
- Redis使用惰性删除和定期删除两种策略来删除过期的键:惰性删除策略只在碰到过期键时才进行删除操作,定期删除策略则每隔一段时间主动查找并删除过期键。
- 执行SAVE命令或者BGSAVE命令所产生的新RDB文件不会包含已经过期的键。
- 执行BGREWRITEAOF命令所产生的重写AOF文件不会包含已经过期的键。
- 当一个过期键被删除之后,服务器会追加一条DEL命令到现有AOF文件的末尾,显式地删除过期键。
- 当主服务器删除一个过期键之后,它会向所有从服务器发送一条DEL命令,显式地删除过期键。
- 从服务器即使发现过期键也不会自作主张地删除它,而是等待主节点发来DEL命令,这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。
- 当Redis命令对数据库进行修改之后,服务器会根据配置向客户端发送数据库通知。
服务器中的数据库
Redis 把所有库信息都保存在redis.h/redisServer
结构的 db 数组中,数组类型是redis.h/redisDB
,dbnum 决定着应该创建多少数据库中的 db,clients 维护着所有连接 Redis 的客户端:
struct redisServer{
//服务器的数据库数量
int dbnum;
//一个数组,保存着服务器中的所有数据库
redisDb *db;
//客户端状态链表
list *clients;
//...
}
服务器数据库实例如图所示:
当切换库时,其实就是 redisClient.db 对 redisServer.db 数组的目标数据库指针的移动。下面展示了从 0 号库切为 1 号库的过程。通过指针的切换,实现对库的共享:
数据库的键空间
redis 将所有 key 进行统一管理,按照所属的库划分,放在 redisDb 的字典中(按照上面画的数据结构,redis 每一个库都对应一个 redisDb)。redisDb 结构的 dict 字典保存了该数据库中的所有键值对,也称为键空间。键空间的数据结构如下:
typedef struct redisDb{
//数据库键空间,保存着数据库中的所有键值对
dict *dict;
...
}
键空间的键就是数据库的键,每个键都是一个字符串对象,键空间的值就是字符串对象,列表对象,哈希表对象,集合对象和有序集合对象
当执行一些插入指令时,就是对 dict 中 key 的新增;同理,删除键后,dict 中的键值对对象都会被删除。
读写键空间时的维护操作
对键的读写时,服务器会做相应的善后操作,比如更新缓存的命中率,更新 LRU(最后一次使用)时间,对已过期的键先进行删除操作,修改时对客户端 watch 的键进行 dirty 标记,更新 dirty 键计数器的值,当开启通知功能后,键修改时需要按配置发送相应通知。
键过期时间相关操作
通过EXPIRE
或PEXPIRE
,客户端可以以秒或毫秒为精度设置过期时间(Time To Live,TTL)。通过EXPIREAT
或PEXPIREAT
,客户端可以设置时间戳作为过期时间。
使用TTL
或PTTL
也可查看某个键的剩余生存时间,还有多久过期:
redis> SET key value
OK
redis> EXPIRE key 500
(integer) 1
redis> TTL key
(integer) 498
Redis 是如何保存过期时间的,又是如何删除过期键的将在下面论述。
设置过期时间
Redis 提供了 4 个命令设置过期时间:
EXPIRE<key> <ttl>
:将 key 的生存时间设为 ttl 秒。PEXPIRE<key> <ttl>
:将 key 的生存时间设为 ttl 毫秒。EXPIREAT<key> <timestamp>
:将 key 的过期时间设置为 timestamp 秒数时间戳。PEXPIREAT<key> <timestamp>
:将 key 的过期时间设置为 timestamp 毫秒数时间戳。
其实几个命令底层都是经过换算后,用 PEXPIREAT 实现的。
实现转换关系图:
存储过期时间
redisDb 中有一个 expires 的字典数据结构保存所有键的过期时间,也称为过期字典。过期字典的值是一个 long long 类型的整数,保存了键所指向的数据库键的过期时间(毫秒精度的 Unix 时间戳)。
typedef struct redisDb{
//过期字典,保存着键的过期时间
dict *expires;
...
} redisDb;
图中键空间和过期的键其实复用了一个键对象,这里方便展示就拆开来,假设我们给键 alphabet 和 book 都设置了过期时间:
移除过期时间
PERSIST 命令可以移除一个键的过期时间,在过期字段中查找给定键,并解除键和值在过期字典中的关联。
redis> SET key value
OK
redis> EXPIRE key 500
(integer) 1
redis> TTL key
(integer) 498
redis> PERSIST message
(integer) 1
redis> TTL key
(integer) -1
计算并返回剩余生存时间
TTL 以秒为单位返回剩余时间,PTTL 以毫秒返回键的剩余时间。二者的计算都是通过计算键的过期时间与当前时间之差来实现的。
过期键删除策略
如果一个键过期了,那么在什么时候被删除?列举几个常见淘汰策略:
- 定时删除:设置键的过期时间时,创建定时器,过期时,以定时器立刻执行键的删除。
- 惰性删除:不着急删除过期键,每次获取时都会进行过期校验。
- 定期删除:隔一段时间,程序就对数据库检查,删除过期键。
定时删除
定时删除策略对内存友好,但对 CPU 不友好。过期键比较多时,删除会占用资源,特别是和删除当前任务无关的过期键,影响性能。Redis 定时器需要创建时间事件,时间事件底层由无需链表实现,查找复杂度为 O(N),如果需要高效处理必然要创建大量的定时器,并不现实。
惰性删除
惰性删除对 CPU 友好,但对内存不友好。不需要把时间浪费在非相关键的删除上。当键非常多时,会导致内存泄漏,因为只有用到时才会判断,删除。
定期删除
定期删除是一种折衷的方式,隔一段时间执行一次,并限制删除操作执行的时长和频率减少对 CPU 的占用;定期删除还能减少庞大的过期键对内存的占用。如何确定时长和频率是难点,过长或过少,会退变为定时删除和惰性删除。
Redis 的过期键删除策略
Redis 使用了惰性删除和定期删除两种策略配合,服务器可以合理地在使用 CPU 时间和避免内存浪费之间权衡。
- 惰性删除策略的实现
该策略由db.c/expireIfNeeded
函数实现,如同指令过滤器,在执行读写键指令时都会调用该函数检查,如果过期则删除。
- 定期删除策略的实现
该策略由redis.c/activeExpireCycle
函数实现,当服务器周期性调用redis.c/serverCron
函数时,activeExpireCycle
函数就会被调用,规定时间内,多次遍历服务器的各个数据库,从 expires 字典中随机检查一部分键的过期时间,并删除过期键。activeExpireCycle
函数的主要工作可以拆分为:
- 每次运行,都从一定数量的数据库中取出一定数量的随机键检查并删除过期键。
- 全局遍历记录检查进度,有记忆功能,全局变量存储的是几号库。
- 当所有数据库都被检查一遍后,重置全局变量,进行新一轮检查。
数据库通知
Redis 发布订阅功能可以让客户端获取数据库中键的变化及命令的执行情况。关注某个键执行了什么命令的通知称为键空间通知。关注某个命令被什么键执行的通知称为事件通知。
主要就是围绕通知功能,简单看下发送通知及其实现。
发送通知
该功能由notify.c/notifyKeyspaceEvent
函数实现:
通过几个入参:要发送的通知类型,时间名称,产生时间的键,产生时间的数据库号。来构造事件通知内容和接收频道名,Redis 许多指令的执行函数都会调用这个函数,传递该命令引发的事件相关信息。
发送通知的实现
- 通过服务器配置的值判断,如果给定通知类型不是服务器允许的就直接返回。
- 如果是服务器允许发送的,检测是否允许发送键空间通知,允许则构建发送事件并通知。
- 检测是否允许发送键事件通知,如果允许则构建并发送通知。