八、数据库
8.1服务器中的数据库
所有数据库保存在服务器状态结构的db数组中
struct redisServer{
//一个数组,保存着服务器中的所有数据库
redisDb *db;
//服务器的数据库数量
int dbnum;
}
8.2切换数据库–SELECT
每个redis客户端有自己的目标数据库,默认目标数据库0
通过修改redisClient指针,切换目标数据库
typedef struct redisClient{
//记录客户端当前正在使用的数据库
redisDB *db;
}
8.3数据库键空间
redisDb结构的dict字典保存了数据库中的所有键值对----键空间(key space)
typedef struct redisDb{
//键空间,保存着数据库中的所有键值对
dict *dict;
}
操作是通过对键空间字典进行操作来实现的 。
- 添加新键
- 删除键
- 更新键
- 对键取值
- 其他键空间操作
- 读写键空间时的维护操作
8.4设置键的生存事件或过期时间-EXPIRE/PEXPIRE
1.设置过期时间
EXPIRE<key><ttl>秒
PEXPIRE<key><ttl>毫秒
EXPIREAT<key><timestamp>秒时间戳
PEXPIREAT<key><timestamp>毫秒级时间戳
最终执行效果都和执行PEXPIREAT命令一样。
最终三个命令都对转换为PEXPIREAT命令执行
2.保存过期时间
expire字典保存了数据库中所有键的过期时间–过期字典
- 过期字典的键是指针,指向键空间中的某个键对象
- 过期字典的值是一个long,long整型的整数,保存了键所指向的数据库键的过期时间
typedef struct redisDb{
dict *expires;//过期字典,保存着键的过期时间
}
3.移除过期时间
PERSIST
4.计算并返回剩余生存时间
过期时间-当前时间
- TTL
- PTTL
5.过期键的判定
- is_expired(key)----true/false
- ttl/pttl—与0的关系
8.5过期键删除策略
1.定时删除----定时器
缺点:对cpu时间不友好
2.惰性删除–取出键时检查是否过期
缺点:对内存不够友好
3.定期删除–每隔一段时间进行一次删除过期键
8.6redis的过期键删除策略
配合使用惰性删除和定期删除
1.惰4性删除策略的实现–expireIfNeeded(过滤器)
2.定期删除策略的实现–周期性操作activeExpireCycle
8.7AOF、RDB和复制功能对过期键的处理
1.生成RDB文件
SAVE/BGSAVE:已过期键不会被保存
2.载入RDB文件
- 以主服务器模式运行,载入时,对文件中保存的键进行检查,未过期的载入,过期忽略
- 从服务器:保存所有键(主从服务器同步时,从服务器清空,不会有影响)
3.AOF文件写入
过期键不删除无影响。
删除:追加append一条del命令显示记录删除
4.AOF 重写
已过期不保存到重写后的AOF文件中
5.复制
服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制–保证主从一致性
从服务器接收到主服务器的del命令后,才会删除。
8.8数据库通知
让客户端通过订阅给定的频道或者模式,来获知数据库中键的变化,以及数据库中命令的执行情况。—notify-keyspace-events 选项决定服务器所发生通知的类型
- 关注键执行了什么命令–键空间通知
- 关注某个命令被什么键执行–键事件通知
空间+事件:AKE
空间:AK
事件:AE
和字符串键有关的键空间K$
和列表键有关的键事件El
1.发送通知
void notifyKeyspaceEvent(int type,char *event,robj *key,int dbid);
- type是当前想要发送通知的类型
- event:事件名称
- keys:产生事件的键
- dbid:产生时间的数据库号码
2.发送通知实现
notifyKeyspaceEvent
1.判断type是否是服务器允许发送的通知类型
2.检测服务器是否允许发送键空间通知,允许:构建并发送事件通知
3.函数检测服务器是否允许发送键事件通知,允许构建并发送
第八章重点
九、RDB持久化
redis内存数据库,服务器进程退出,不保存到磁盘中,会丢失。RDB文件保存在硬盘里,经压缩的二进制文件。
手动执行、根据服务器配置选项定期执行
9.1RDB文件的创建(rdbSave函数)和载入
命令:SAVE、BGSAVE
- SAVE阻塞redis服务器进程,直到rdb文件创建完。
- BGSAVE派生出一个子进程,由子进程负责创建RDB文件。
创建RDB文件–rdbSave函数
载入RDB文件–服务器启动时自动执行,检测到存在,自动载入。
AOF文件更新频率比RDB文件更新的频率高,只有在AOF持久化功能处于关闭状态,服务器才会使用RDB文件还原数据库状态
载入RDB文件–rdbLoad函数
1.SAVE命令执行时服务器状态–阻塞
2.BGSAVE命令执行时服务器状态–3个命令与平时不同
- SAVE:拒绝,防止产生竞争
- BGSAVE与BGREWRITEAOF不能同时执行(子进程执行)
- BGSAVE执行,BGREWRITEAOF被延迟
- BGREWRITEAOF执行,BGSAVE被拒绝
3.RDB文件载入时服务器状态–阻塞
9.2自动间隔性保存
设置save项,让服务器每隔一段时间执行BGSAVE命令(设多个,任意满足一个即可执行)
1.设置保存条件–redisServer的saveparams属性
save 900 1
saveparams属性–数组,每个元素是一个saveparam结构,每个结构保存一个save选项设置的保存条件
struct saveparam{
time_t seconds;//秒数
int changes;//修改数
}
2.dirty计数器和lastsave属性
- dirty:记录距离上次成功执行save命令或bgsave后,服务器对数据库状态进行了多少次修改
- lastsave:UNIX时间戳,记录服务器上次成功执行save命令或bgsave命令时间
struct redisServer{
//...
//记录保存条件的数组
struct saveparam *saveparams;
//修改计数器
long long dirty;
//上一次执行保存的时间
time_t lastsave;
}
3.检查保存条件是否满足
周期性操作函数serverCron:每隔100ms会执行一次,用于对正在运行的服务器进行维护,其中一项检查save选项所设置的保存条件是否已经满足,满足执行bgsave
检查saveParams所有保存条件:seconds,changes…
9.3RDB文件结构
全大写:常量,全小写:变量/数据
-
REDIS:5个字节,REDIS五个字符,载入文件时,快速检查所载入的文件是否RDB文件
-
db_version:4个字节,字符串表示的整数,记录了RDB文件的版本号eg0006 第六版
-
databases:包含0个或任意多数据库以及数据库中键值对数据
数据库状态为空,0字节/非空,也不空
-
EOF:1字节,标志RDB文件正文内容结束
-
check_sum:8字节无符号整数,校验和,前四个部分计算得出。
1.databases
每个非空数据库都可以保存为selectdb,db_number、key_value_pairs
- selectdb:1字节,接下来要读的数据库号码
- db_number:数据库号码1/2/5字节–select命令
- key_value_pairs:保存数据库所有键值对数据(包含过期键)
2.key_value_pairs
保存数据库所有键值对数据(包含过期键)
- 不过期:type(底层编码),key,value
- 过期:expiretime,ms,type,key,value
expiretime:告知程序接下来要读过期时间
ms:8字节,带符号整数,过期时间
3.value编码
value保存值对象,每个值对象的类型都有type记录,根据类型不同个,value部分结构长度不同
(1)字符串对象
type-redis_rdb_type_string—int或raw(压缩/不压缩)
int
raw–字符串长度–压缩(>=20字节)/不压缩
不压缩
压缩–LZF算法
(2)列表对象
type-redis_rdb_type_list–linkedlist
长度为3,第一个列表项长度为5,内容为字符串”hello“。。。
(3)集合对象
type-redis_rdb_type_set–HT
(4)哈希表对象
type-redis_rdb_type_hash–HT
键值对数量:2
第一个键值对,键长度为1的字符串a,值长度为5的字符串apple
(5)有序集合对象
type-redis_rdb_type_zset–skiplist
element开头的部分代表有序集合元素,每个元素分为成员(字符串对象)和分值(浮点数)
(6)INTSET编码集合
type-redis_rdb_type_set_intset–整数集合对象
先转为字符串对象,在保存到RDB文件里
(7)ZIPLIST编码的列表,哈希表或有序集合
type-redis_rdb_type_list_ziplist–压缩列表
type-redis_rdb_type_hash_ziplist–压缩列表
type-redis_rdb_type_zset_ziplist–压缩列表
压缩列表转换为字符串对象–保存到rdb文件
9.4分析RDB文件–od
该命令可以用给定格式转存并打印输入文件
1.不包含任何键值对的RDB文件
od -c dump.rdb
2.包含字符串键的RDB文件
3.包含带有过期时间的字符串键的RDB文件
4.包含一个集合键的RDB文件
5.关于分析RDB文件的说明
文件检查工具 redis-check-dump
校验和 ----->od -cx dump.rdb 看到清除
第九章重点
十、AOF持久化append only file
- RDB:保存数据库中键值对来记录数据库状态不同
- AOF:通过保存redis服务器所执行的写命令来记录数据库状态–请求协议格式保存
服务器启动,载入AOF
10.1AOF持久化实现
步骤:
命令追加(append)文件写入 文件同步(sync)
1.命令追加append
服务器执行完一个写命令,以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区末尾
struct redisServer{
//aof缓冲区
sds aof_buf;
}
2.AOF文件的写入与同步sync
redis服务器进行就是一个事件循环loop,:
- 文件事件负责接收客户端命令请求,以及向客户端发送命令回复
- 时间事件负责执行像serverCron函数这样需要定时运行的函数
结束一个事件循环前调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里。
默认everysec
写入和同步
- 为了提高写入效率,write函数写入 将写入数据暂存到内存缓冲区里,等填满或超过指定时限写入磁盘。
- 安全问题:会丢失
- 解决:fsync 和 fdatasync 两个同步函数,强制让os立即将缓冲区中数据写入硬盘
效率和安全性:appendfsync决定
- always:所有内容写入,效率最慢,最安全
- everysec:效率快,只丢失一秒命令数据
- no:全写入AOF 什么时候同步 由os控制,写入速度最快
10.2AOF文件的载入与数据还原
- 创建不带网络连接的伪客户端,执行aof写命令
- 从AOF文件分析并读取一条写命令
- 使用伪客户端执行被读出的写命令
- 循环2,3 直到处理完毕
10.3AOF 重写BGREWEITEAOF
AOF文件越大,进行数据还原所需时间越多。
文件膨胀----文件重写,创建新文件代替现有文件
1.AOF文件重写的实现-通过读取服务器当前数据库状态实现
eg:list
直接从数据库中读取键list的值,然后用一条命令记录
避免客户端溢出–处理列表,哈希表,集合,有序集合这四种可能带有多种元素的键,会先检查键所包含的数量,超过常量值(目前64),使用多条命令写
2.AOF后台重写
AOF重写函数aof_rewrite 大量的写入操作,会长时间阻塞
aof重写程序子进程执行,带有数据副本,不是线程,安全
问题:aof重写,新命令对现有数据库状态进行修改,与重写后不一致—数据不一致
解决:aof重写缓冲区,执行完写命令,同时将写命令发送给aof缓冲区和重写缓冲区
完成aof重写,发送信号,父进程调用处理信号
执行:将aof重写缓冲区内容写入新aof文件,新文件改名,原子地覆盖现有aof文件,完成新旧两文件替换
第十章重点
十一、事件
redis服务器是一个事件驱动程序,两类事件
- 文件事件:redis服务器通过套接字与客户端进行连接,文件事件就是服务器对套接字操作的抽象。服务器与客户端通信产生相应文件事件,服务器通过监听这些事件完成一系列网络通信操作。
- 时间时间:redis一些操作需要在给定时间点执行,时间事件就是服务器对这类定时操作的抽象
11.1文件事件
redis基于reactor模式开发了自己的网络事件处理器–文件事件处理器
- I/O复用–同时监听多个套接字,根据套接字目前执行的任务来为套接字关联不同的事件处理器
- 被监听的套接字准备好执行连接应答accept,读取read,写入write,关闭close等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
1.文件事件处理器构成
套接字,I/O多路复用程序,文件事务分派器,事件处理器
套接字:文件事件是对套接字的抽象,每个套接字准备好执行连接应答,写入,读取,关闭等操作时产生一个文件事件。-多个文件事件可能会并发出现。
I/O多路复用程序:负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字。将所有产生事件的套接字放到一个队列里面。
文件事件分派器:接收I/O多路复用程序传来的套接字,并根据套接字产生的事件的类型,调用相应的事件处理器、
处理器是一个个函数,定义了某个事件发生时,服务器应该执行的动作。
2.I/O多路复用程序的实现
I/O多路复用程序的所有功能通过包装常见的select epoll evport 和kqueue这些I/O多路复用函数库实现。I/O多路复用程序的底层实现可以互换
3.事件类型
I/O多路复用程序可以监听多个套接字的readable和writable事件,这两类事件和套接字操作之间对应关系如下
- 当套接字变得可读(客户端对套接字执行write或close)或有新的可应答套接字时,套接字产生readable事件
- 当套接字变得可写(客户端对套接字执行read)套接字产生writable事件
程序允许服务器同时监听套接字的两个事件,如果一个套接字同时产生这两种事件,优先处理readable事件。—当一个套接字又可读又可写,服务器先读套接字,后写套接字。
4.API
aecreatefileevent:接收一个套接字描述符,一个事件类型,一个事件处理器作为参数,将给定套接字的给定事件加入到I/O多路复用程序的监听范围之内,并对事件和事件处理器进行关联
aedeletefileevent:接收一个套接字描述符和一个监听事件类型作为参数,取消对套接字给定事件的监听,并取消事件和事件处理器的关联
aegetfileevents接收一个套接字描述符,返回该套接字正在被监听的事件类型
aewait:aeapipoll;
aeprocessevents;aegetapiname
5.文件事件的处理器
redis编写了多个处理器,用于实现不同的网络通信需求
应答处理器,命令请求处理器,
命令回复处理器,复制处理器
(1)连接应答处理器acceptTcpHandler
用于对连接服务器监听套接字的客户端进行应答
redis服务器初始化,程序将这个连接应答处理器和服务器监听套接字readable事件关联起来
客户端用connect连接服务器监听套接字时,套接字产生readable事件,引发连接应答处理器执行并执行相应的套接字应答操作
(2)命令请求处理器readQueryFromClient
负责从套接字中读入客户端发送的命令请求内容
客户端通过连接应答处理器成功连接到服务器后,服务器会将客户端套接字的readable事件和命令请求处理器关联起来
客户端向服务器发送命令请求的时候,套接字会产生readable事件,引发命令请求处理器执行,执行相应套接字读入操作。
一直连接,一直关联。
(3)命令回复处理器sendReplyToClient
负责将服务器执行命令后得到的命令回复通过套接字返回给客户端
服务器有命令回复需要传送给客户端时,服务器会将客户端套接字waitable事件和命令回复处理器关联。
客户端准备好接收服务器传回的命令回复时,产生waitable事件,引发命令回复处理器执行,并执行相应的套接字写入操作。
回复命令发送完毕,服务器解除命令回复处理器与客户端套接字的waitable事件之间的关联
(4)一次完整的客户端与服务器连接事件示例
redis服务器正在运行,
服务器监听套接字的readable事件处于监听–连接应答处理器。
客户端发起连接–监听套接字产生readable事件-触发连接应答处理器执行。
处理器对客户端连接请求进行应答,创建客户端套接字,客户端状态,并将客户端套接字的readable事件与命令请求处理器关联,使客户端可以向主服务器发送命令请求
客户端发送命令请求–客户端套接字产生readable事件,引发命令请求处理器执行,处理器读取客户端的命令内容,传给相关程序执行
执行命令产生相应命令回复,服务器将客户端套接字的waitable事件与命令回复处理器关联,
客户端尝试读取命令回复时,客户端套接字将产生waitable事件,触发命令回复处理器执行,命令回复处理器将命令回复全部写入到套接字后,服务器接触客户端套接字的waitable事件与命令回复处理器的关联
11.2时间事件
redis时间事件两类:定时事件,周期性事件
- 定时事件:
- 周期性事件:
时间事件三个属性
- id:全局唯一ID(标识号),从小到大递增
- when:毫秒UNIX时间戳,时间事件的到达时间
- timeProc:时间事件处理器,函数,时间事件到达时,服务器就会调用相应处理器来处理事件
事件处理器返回ae_nomore–定时事件,事件达到一次删除,之后不再到达
事件处理器返回非ae_nomore–周期性事件–when属性更新,让这个事件在一段时间后再次到达
目前只使用周期性事件
1.实现
时间事件放在一个无序(不按when的顺序,id有序,新来的在表头)链表中–时间事件执行器运行–遍历整个链表,查找时间事件,调用相应事件处理器。
2.API
3.时间事件应用实例:serverCron函数
持续运行的redis服务器需定期对自身资源和状态进行检查和调整,确保服务器可以长期、稳定运行,这些定期操作由serverCron函数负责执行,工作包括
11.3事件的调度与执行
调度:aeProcessEvents
第十一章重点
十二、客户端
redis服务器典型一对多服务器程序。
服务器为每个客户端建立相应的redisClient结构,保存了客户端当前的状态信息,执行相关功能需用到的数据结构等。。。
redis服务器状态结构的clients属性是一个链表,这个链表保存了所有与服务器连接的客户端状态结构
list *clients;
12.1客户端属性
客户端属性分类
- 通用属性:
- 特定功能相关的属性:eg操作数据库用到的db属性和dictid属性,执行事务的mstate属性,执行WATCH命令的watched_keys属性
1.套接字描述符fd
typedef struct redisClient{
int fd;
}
- 伪客户端:fd = -1,伪客户端处理的命令来源于aof 或lua,不需套接字连接,
- 普通客户端:fd > -1,使用套接字通信
2.名字
一般没名字,Client setname可设置
typedef struct redisClient{
robj *name;
}
3.标志—客户端角色,目前状态
typedef struct redisClient{
int flags;
}
可以是单个标志 flags=
或 多个标志的二进制 flags= | |…
一部分标志记录了客户端的角色
redis_master 客户端代表一个主服务器 redis_slave 客户端代表一个从服务器
redis_pre_psync
redis_lua_client
另一部分记录客户端目前所处状态
例
4.输入缓冲区
保存客户端发送的命令请求,大小动态缩小扩大,但不超过1GB
typedef struct redisClient{
sds querybuf;
}
5.命令与命令参数
服务器对命令请求内容进行分析,将得出的 命令参数以及命令参数个数分别保存到客户端argv属性和argc属性
typedef struct redisClient{
//属性数组,
//字符串对象argv[0]要执行的命令
//之后的其他项是传给命令的参数
robj **argv;
//记录argv数组长度
int argc;
}
6.命令的实现函数
根据argv[0]的值在命令表中查找命令所对应的命令实现函数
typedef struct redisClient{
struct redisCommand *cmd;
}
7.输出缓冲区
执行命令所得的命令回复被保存在客户端缓冲区,每个客户端两个输出缓冲区可用,一个大小固定,一个可变,先使用固定缓冲区
typedef struct redisClient{
char buf[REDIS_REPLY_CHUNK_BYTES];
int bufpos;//数组目前已使用字节数量
}
typedef struct redisClient{
list *reply;
}
8.身份验证
用于记录客户端是否通过了身份验证,仅在服务器启用身份验证功能时使用。
typedef struct redisClient{
int authenticated;
}
值为0 未通过身份验证。1:已通过
9.时间
typedef struct redisClient{
time_t ctime;//创建时间
time_t lastinteraction;
time_t obuf_soft_limit_reached_time;//最红一次进行互动时间
}
12.2客户端创建与关闭
1.创建普通客户端
服务器将新客户端状态添加到clients链表末尾
2.关闭普通客户端
关闭原因:
- 客户端进程退出或者被杀死
- 客户端发送带有不符合协议格式的命令请求
- 客户端成为client kill 命令目标
- 设置timeout配置选项
- 发送命令大小超过输入缓冲区大小限制
- 回复超过输出缓冲区大小
限制客户端输出缓冲区大小
- 硬性限制:超过硬性限制,立即关闭
- 软性限制:超过软性限制,记录到达软性限制起始时间,一直超出,关闭,
3.Lua脚本的伪客户端
在服务器运行的整个生命周期中会一直存在,只有服务器被关闭,才会被关闭
typedef struct redisClient{
redisClient *lua_client;
}
4.AOF文件的伪客户端
载入AOF文件时,创建。载入完成,关闭。
第十二章重点
十三、服务器
负责与多个客户端建立网络连接
13.1命令请求的执行过程
- 客户端发送命令请求
- 服务器接收并处理,操作,产生命令回复ok
- 服务器将命令回复ok发送给客户端
- 客户端接收服务器返回的命令回复ok,并将这个回复打印给用户观看
1.发送命令请求
2.读取命令请求
- 读取套接字中协议格式的命令请求,保存到输入缓冲区
- 对请求进行分析,提取命令参数,命令参数个数,保存到argv 和argc中
- 调用命令执行器,执行客户端指定的命令
3.命令执行器(1)查找命令实现
根据argv[0] 在命令表中查找参数所指定的命令,并将找到的命令保存到客户端状态的cmd属性里
命令表是一个字典,字典的键是命令的名字,值为RedisCommand结构
RedisCommand结构记录了一个redis命令的实现信息
4.命令执行器(2)执行预备操作
检查客户端状态cmd–>cmd指向的redisCommand结构的arity属性(为-3 输入命令参数个数大于等于3个)—> 检查是否通过身份验证—>检查maxmemory功能—>BGSAVE出错不能再写命令–>正在subscribe或psubscribe 拒绝—>lua超时阻塞 只执行nosave kill
5.命令执行器(3)调用命令的实现函数
client->cmd->proc(client)
相当于setCommand(client),
产生回复“+OK\r\n”,保存到客户端状态buf属性中
6.命令执行器(4)执行后续工作
如果开启了慢查询日志,慢查询日志模块会检查是否需要为刚刚执行完的命令请求添加新的慢查询日志
AOF持久化功能
有正在复制的。。。
7.将命令回复发送给客户端
命令回复保存到客户端的输出缓冲区里面,并为套接字关联命令回复处理器,当套接字变成可写状态,服务器就会执行命令回复处理器,将保存在客户端输出缓冲区中的命令回复发送给客户端
发送完毕,清空输出缓冲区,为处理下一个做准备
8.客户端接收并打印命令回复
接受命令,转换为可读格式–>打印
13.2serverCron函数(默认每100秒执行一次)
负责管理服务器资源,并保持服务器自身的良好运转
1.更新服务器时间缓存100s更新一次
struct redisServer{
//保存秒级精度的系统当前unix时间戳
time_unixtime;
//毫秒级
long long mstime;
}
精确度不高,对精确度要求不高的任务上,
2.更新LRU时钟10s/次
服务器时间缓存的一种
struct redisServer{
unsigned lruclock:22;
}
计算数据库键的空转时间
3.更新服务器每秒执行命令次数100s
以抽样计算的方式,估算并记录服务器在最近一秒钟处理的命令请求数量
struct redisServer{
long long ops_sec_last_sample_time;
...
}
4.更新服务器内存峰值记录
struct redisServer{
size_t stat_peak_memory;//已使用内存峰值
}
5.处理SIGTERM信号–信号处理器
对服务器状态进行检查,根据属性决定是否关闭服务器int shutdown_asap;
6.管理客户端资源
serverCron 函数每次执行都会调用clientCron函数会对一定数量客户端进行检查:连接超时的,输入缓冲区超过一定限度的
7.管理数据库资源
调用databasesCron函数 对一部分数据库检查,删除过期键,对字典收缩
8.执行被延迟的BGREWRITEAOF
在执行BGSAVE时,会将BGREWRITEAOF命令延迟----int aof_rewrite_scheduled为1 有延迟的
9.检查持久化操作的运行状态
两个pid记录了BGSAVE和BGREWRITEAOF进程
10.将AOF缓冲区中的内容写入AOF文件
11.关闭异步客户端–关闭超限制的客户端
12.增加cronloops计数器的值–serverCron函数执行次数
13.3初始化服务器
1.初始化服务器状态结构
创建一个struct redisServer