第九章 数据库
本章主要介绍了Redis的数据库存储结构,还介绍了包括增删改查的实现流程、key过期处理、数据库通知等。
数据结构与CURD
redis支持多个数据库同时提供服务,默认会创建16个数据库。数据库内的数据采用的是KV方式存储,比较像一个hashmap
,因此在每个数据库内,key是唯一的,以下该map称为 数据字典;数据库内还有一个hashmap
来存放所有key的过期时间,key与数据字典中的key相同,value是绝对的过期时间,以下称为 过期时间字典;还有一些统计相关的字段,比如统计数据库的缓存命中率(某个key是否在存在)等。
在redis中对某个key进行CURD的操作时,需要先查询过期时间字典得到该key的过期时间。
- 如果数据已过期,则将删除该过期数据的kv对,然后对于写操作(比如setnx)会继续执行写入流程并且返回执行结果,对于读操作,则直接返回空结果。
- 如果数据未过期,则正常进行读写流程,并返回对应的执行结果。
- 读写流程中,在完成数据的读取或写入后,还会进行以下操作
- 更新操作对象的lru字段
- 更新数据库的相关统计字段(
keyspace_hits
以及keyspace_misses
) - 对于写入流程,如果有
WATCH
监听该key,则将该key标记为dirty
,并且每次写入,dirty
值加1,并且触发数据持久化以及复制操作 - 如果开启了数据库通知功能,还将产生对应事件,发送相关通知。
key过期处理
redis支持在写入数据时设置该数据的过期时间,也支持单独通过expire
等相关api设置某个key的过期时间。对于过期key,redis有以下几种删除策略
- 定时删除
- 在设置过期时间时,在服务器上启动一个定时器,到了过期时间则立即删除
- 优点是节省内存,能够最大程度的避免存储空间浪费
- 缺点是浪费CPU,在有过期时间key数量较多的情况下,大量的CPU被用来处理key的过期轮询
- 惰性删除
- 只有在某个key在访问的时候,才去校验过期时间,并且对过期的key执行删除操作
- 优点是节省CPU,不需要花费额外的CPU时间做过期key的轮询
- 缺点是浪费内存,对于长时间没有访问的过期key无法删除,浪费存储空间
- 定期删除
- 每隔一段时间对数据库中的key做过期时间校验,并且删除过期的key,并且通过限制执行时间,来避免长时间占用CPU
- 优点是比较均衡,在结合了以上两种删除策略的优缺点后,权衡出的一个对CPU和内存都比较友好的策略
- 缺点是在策略设置不恰当时,会退化为其中一种删除策略
对于分布式环境下,主从节点间对于过期key的删除处理如下
- 所有删除动作由主节点触发,在主节点删除过期key之后,向从节点发送
DEL
命令,删除该过期key - 从节点不主动删除过期key,在对该过期key执行读取操作时,从节点不会主动删除该过期key,依然会返回该过期key的读取结果。
通过这种方式,redis保证了主从节点间的数据一致性,但是对于业务程序来说,可能会遇到 幻读 现象。
数据库通知
redis支持两种通知订阅模式:对key订阅(key_space_notification
)或对事件订阅(key_event_notification
)。具体支持的订阅可以在redis中文官网查询。
第十~十一章 RBD持久化 与 AOF持久化
这两章里有部分内容过于深入与细节,对于使用上没有什么帮助,在造轮子上帮助也不大。再加上这两章其实是对redis持久化功能不同实现的介绍,因此将两章合并,也有助于理解redis的持久化功能以及不同实现的差异。
功能点 | RDB | AOF |
---|---|---|
存储内容 | 完整数据,包含一些分隔标识符 | 执行命令的流水,包含命令key以及数据 |
数据格式 | 非常紧凑,完全由数据组成 | 包含了执行命令,同一个key可能包含多条执行命令 |
过期删除策略 | 保存策略 过期的key本身不会保存 载入策略 master节点:载入时会判断载入的key是否过期,过期则不载入 slave节点:载入时不做判断,全量载入,主从同步时删除 |
保存策略 过期删除时,会追加一条DEL命令,删除该key 载入策略 无影响,对过期key会执行插入命令与删除命令(过期时保存的DEL命令) AOF重写 重写时不记录已过期的key,与RDB的保存策略类似 |
保存时机 | 命令触发或条件触发,条件格式为在M的时间内有N次写入命令,支持多个条件以 或的方式生效 | 提交写命令后触发追加命令流水,同时有定时的 AOF重写 要注意,追加流水不等于写入AOF文件,这是因为写入文件会有写入缓冲区 为了保证持久化的可靠性,redis支持三种写入文件配置 always模式:每条命令都追加到硬盘上 everysec模式:每隔1s将缓冲区内容刷入文件 no模式:不主动将缓冲区输入文件,等待操作系统处理 |
载入策略 | 保存了全量数据,载入效率高,有可能丢数据,因此载入优先级低 | 保存了执行命令,载入效率低,载入优先级高 |
AOF重写
AOF文件保存的是命令流水,随着服务器运行时间的增长,命令流水也会不断增加,导致AOF文件越来越大。在这种情况下,redis采用了一种叫做 AOF重写 的策略来压缩AOF文件的占用空间。其实质过程是将redis中存储的所有数据,用写入命令回放一遍,得到一个新的AOF文件。在重写过程中,服务器接收到的所有写命令会临时存放在一个重写缓冲区中,在新的AOF文件生成完毕后,将该缓冲区的命令追加的新AOF文件的末尾。
特别的AOF
对于PUBSUB
命令与SCRIPT LOAD
命令,redis会通过REDIS_FORCE_AOF
来刷入AOF缓冲区,原因是PUB_SUB
命令影响的是所有订阅者的状态或数据,SCRIPT LOAD
影响的是服务器状态。因此虽然这两个命令本身不改变redis数据库中的数据值,但是也会追加到AOF文件中。
第十二~第十四章 事件/客户端/服务器
在介绍了redis的数据结构与持久化之后,紧接着的三章开始介绍redis的C/S架构实现,比如一个请求如何从client端发出,server端如何接受并处理。这三章有助于理解redis是如何设计并实现的,在需要造轮子的时候会有帮助。
事件
redis内部分为两种事件: 文件事件(file event) 以及 时间事件(time event) 其中的文件事件简单理解可以认为是命令触发事件,时间事件就是服务器做的一些定时任务(比如在AOF持久化中的everysec模式就会产生一个时间事件)。那么redis是如何使用单线程来处理这两种事件的呢?
redis采用的是按时间事件作为时间分片,在一个时间片内尽可能多的处理文件事件,时间片结束之后,处理时间事件并根据下一个到来的时间事件分配时间片。听起来比较拗口,可以用一张图来理解
TODO:补图
redis在处理各事件时,也会考虑将执行时间过长的事件拆成几部分执行。比如如果一个文件事件传输的数据过大,那么会在传输一部分(实际上数据在缓冲区里)之后中断,从而执行时间事件,在时间事件执行完成后,又会重新进行传输。
客户端
本章主要介绍了redis的单机客户端实现,大部分功能点或者概念都比较容易想到,这里不展开细说。要注意两个特别的客户端 LUA Client 以及 AOF Client。
- LUA Client在服务器启动时创建,直到服务器销毁时销毁,用处是执行LUA脚本。
- AOF Client在载入AOF文件时创建,在执行完所有AOF文件命令时销毁,用处是从AOF恢复数据。
服务器
本章主要介绍了redis的单机服务器实现,实现部分较多,不作详细介绍了。
总结
Redis设计与实现 一书的第二章 单机数据库的实现 到此结束。本章主要介绍了redis的数据结构与CURD、数据持久化方案、事件模型、客户端与服务器架构几个方面的内容。平时工作中最常接触的,应该是数据持久化方案这一部分,包括面试中也比较常见。对于其他部分,个人觉得有价值的一个是数据结构与CURD部分,一个是事件模型部分。数据结构部分讲述了redis的底层数据结构,以及CURD在redis上的实现,有助于理解redis的高性能是怎么实现的。事件模型部分对于单线程处理时间事件与文件事件的设计值得学习,设计上也比较巧妙。