学完MySQL InnoDB之后,又开始学习和研究Redis。
首先介绍下书:《Redis设计与实现》第二版 黄健宏著,机械工业出版社,388页,基于redis3.0版本。版本有点低,这个影响不大,基本面变化不大,而变化的部分网上查资料可以弥补。
一、概述
Redis服务器是一个键值对(key-value pair)类型数据库服务器,属于NoSQL。Redis源码使用ANSI C语言编写而成。
它最大的特点数据全部放缓存,主要用于读写操作频繁的,如秒杀系统。我联想的是我们监控系统中的秒级回放,特别是分布式系统中,或许可以考虑。
二、数据结构
包括动态字符串、链表、字典、跳跃表、整数集合、压缩列表共六种。
redis虽然是C语言实现的,但是它的数据结构和平时用的不一样,有些改进和封装,更适用于数据库;这应该也是它的创新之处。
动态字符串SDS和Java StringBuffer类十分相似:带缓冲区的字符串,值可以被改变。
印象最深还是跳跃表,书上介绍说实现比平衡树简单,但是效率却可以和它媲美;从结构设计看,非常优秀。
三、对象类型
包括字符串对象、列表对象、哈希对象、集合对象、有序集合对象共五种。对象是对基础数据结构的再封装,从而供Redis直接使用,即Redis并不直接操作上述基本的数据结构。而每种对象都用到了至少一种上面的数据结构。
这个五大对象类型,才Redis数据库中,对应五个常量,即对象类型常量。而上述六种的数据结构也有对应的常量,叫编码常量。但是编码常量共有八种,其他两种是整型字符串和短字符串。当字符串长度小于等于39字节时,采用embstr编码,即短字符串编码。
那么对象类型常量和编码常量是一个固定的对应关系,除了字符串对象类型常量对应三种编码常量外,其他都是对应两种编码常量。
注:一个对象类型常量对应的 两个或者说三个编码常量在一定条件下系统会自动切换的,条件包括字符串长度变化、元素个数变化、追加不同类型等等;Redis称为编码转换。
Redis的键和值都是对象,所以Redis基于对象操作的,而对象的内存回收通过引用计数技术。
四、数据库
1. 数据库
Redis服务器的所有数据库都保存在redisServer.db数组中,而数据库的数量则由redisServer.dbnum属性保存,默认会创建16个数据库。
数据库由字典构成,我们将这个字典称为键空间(key space);键总是一个字符串对象,而值可以是任意一种对象类型,如上描述的五种对象类型之一。
数据库主要由dict和exprires两个字典构成,其中dict字典负责保存键值对,而 exprires字典负责键的过期时间。
2 .键的生命周期
Redis设置键的过期时间管理键的生存时间,设置过的键同时保存在exprires字典中,对键的生存时间的设置有如下命令。
expire------设置过期时间,第二个参数是TTL(单位秒)。
pexpire------设置过期时间(单位豪秒)。
TTL------查看剩下多少时间(单位秒),返回负数则key失效,key不存在了。
PTTL------功能同TTL,单位对应毫秒。
expireat------设置过期时间,第二个参数是时间戳(单位秒)。
pexpireat------功能同上(单位豪秒)。
persist ------取消过期时间,将键值对从exprires字典中移除。
3.过期键的删除
删除策略有三种方式:
(1)定时删除:创建定时器,只要存在键过期则马上删除。
(2)惰性删除:获取键的时候才删除。
(3)定期删除:每隔一段时间检测,删除过期键。但不是所有过期键,随机抽取的,由系统算法决定。
Redis实际采用第二种和第三种。
SAVE命令 和 BGSAVE命令所产生的新的RDB文件不会包含过期键。
BGREWRITEOF 命令所产生的AOF重写文件不会包含过期键。
主从复制时,从节点不会主动删除过期键,而是等待主节点发送DEL命令,以保证数据的一致性。
4.其他常见命令
set------设置 key 对应的值为 string 类型的 value。
setnx------设置 key 对应的值为 string 类型的 value。如果 key 已经存在,返回 0,nx 是 not exist 的意思。
setex------设置 key 对应的值为 string 类型的 value,并指定此键值对应的有效期。
del------删除某个key,第一次返回1 删除了 第二次返回0。
mset------一次设置多个 key 的值,成功返回 ok 表示所有的值都设置了,失败返回 0 表示没有任何值被设置。
getset------设置 key 的值,并返回 key 的旧值。
mget------一次获取多个 key 的值,如果对应 key 不存在,则对应返回 nil。
append------给指定 key 的字符串值追加 value,返回新字符串值的长度。
strlen------取指定 key 的 value 值的长度。
randomkey------随机返回一个key
rename------重命名
type ------返回数据类型
select 0 ------选择数据库(0-15库)
move age 1------把age 移动到1库
FLUSHDB------清空整个数据库
思考:
Redis主要是内存资源使用多,但据网上资料,支持较短长度字符串时上百万和千万级不成问题。
五、独立功能
1.事件
分为时间事件和文件事件两类。
时间事件指服务器的一些定时操作。
文件事件是服务器与客户端或者其他服务器之间的通信。文件事件处理器基于Reactor模型实现的网络通信,反应器模型;IO多路复用采用相对的还有proactor模型,主动器模型。
2.客户端
Redis服务器是典型的一对多程序,可以同时与多个客户端进行通信,同时保存每个客户端相关信息。
客户端结构体最重要的两部分是一个输入缓冲区和一个输出缓冲区,用于保存接收和发送的内容。输入缓冲区,大小不能超过1GB;输出缓冲区分成两部分,固定的和可变的,固定的大小16KB。
客户端分为普通客户端,伪客户端。其中伪客户端包括LUA脚本伪客户端和AOF文件伪客户端。普通客户端的fd属性值大于-1,伪客户端该值为-1。
3.服务器
服务器常见功能是处理客户端输入的命令外,还有serverCron函数,它是一个时间事件,每隔100毫秒运行一次。
serverCron函数的工作主要包括更新服务器状态信息,处理服务器接收的SIGTERM信号,管理客户端资源和数据库状态,检查并执行持久化操作等。
SIGTERM信号主要作用是服务器关闭前不会因其他事件如持久化阻塞而能及时退出。
补充:LRU缓存淘汰算法
在redis缓存机制中,提到了LRU,least recently used最近最少使用,这个在MySQL也有。但是两者描述的不是很多。
感觉这个算法很好,在网上查资料,很多互联网的技术也使用了该算法。其实对热点数据,例如我们的常用的摄像机的搜索、回放和即时回放可以考虑。
最新版本Redis还有LFU(least frequently used最不经常使用)缓存技术。
4.事务
相对MySQL非常简单,ACID都没有MySQL那么强大;也不支持回滚。可能定位不一样,redis本身是单线程。
5.发布与订阅
对操作和消息而言,客户端和服务器之间使用。
6.查找排序
通过SORT命令对数据的查找和排序。
7.LUA脚本
Redis内嵌的脚本语言,方便实现批量操作等。常见的有EVAL命令和EVALSHA命令,EVALSHA命令可以直接执行由EVAL命令生成的SHA1校验和。
补充:Lua是一种内嵌的脚本语言,轻量小巧,标准C语言编写,开源。目前常(嵌入)用在游戏开发中。
8.二进制位数组
Redis支持二进制位数组的操作,这种类型可以大大节约内存空间,而且查找和统计都非常方便。
补充:实际应用中,有很多的大型系统中将二进制位数组用于查询和统计用户的登录状态。
9.慢查询日志
保存在结构体slowlogEntry为元素的slowlog链表中。可以设置超时时间单位毫秒,和保存条数。
10.监视器
客户端可通过monitor命令将自己变成监视器,监视服务器处理的每个命令。
六、持久化
redis数据都是放内存,持久化即将数据写入磁盘,以下两种都是以日志的形式存放。
1.RDB持久化
RDB是Redis DataBase缩写,存的最终的真实的数据,二进制文件,用redis自带命令可以解析和读取。
功能核心函数rdbSave(生成RDB文件)和rdbLoad(从文件加载内存)两个函数。
SAVE命令 和 BGSAVE命令用于持久化操作,后者表示子进程在后台运行。BGSAVE模式下,服务器根据配置文件或者save(小写)命令设置得参数自动间隔执行一次BGSAVE。save参数第一个是间隔时间,第二个是修改次数;满足其中之一就会执行。
2.AOF持久化
AOF是Append Only File缩写,存的是增删改的操作命令,ASCII字符格式,可读。
数据修改时,先写入AOF缓冲区,再WRITE写入AOF文件(此时还在文件系统缓存中),最后由系统调用 fsync 或 fdatasync 函数将系统缓存数据保存到磁盘即同步。何时同步,有三个选项:always马上,everysec隔约一秒,no交给操作系统;默认值everysec。
BGREWRITEOF 命令实现AOF重写的功能,即对最终数据读取再添加的命令写入文件,减少中间操作和最终文件的大小。同样使用子进程执行。运行的同时新写的数据放在AOF重写缓冲区,即服务器父进程将写命令同时写入AOF缓冲区和AOF重写缓冲区;待重写工作完成后,再将AOF重写缓冲区内容写入新的AOF文件,继续后面结尾工作。
注意:当服务器开启了AOF功能时,服务器优先使用AOF文件来还原数据;而AOF默认是关闭的。
而且,BGSAVE 和 BGREWRITEOF 两个命令不能同时运行。如果BGREWRITEOF 在运行,BGSAVE 会被拒绝;而BGSAVE 运行时,BGREWRITEOF 会被延迟到BGSAVE 完成之后。
七、集群
1.主从复制
(1)主从服务器架构中,从服务器发送SLAVEOF命令给主服务器启动复制。从服务器收到OK返回消息后,再次发送SYNC命令表示请求同步数据。主服务器先发送RDB文件给从服务器,再将缓冲区所有写命令也发送给从服务器,从而实现主从数据一致。但是如果同步后,在中间过程从服务器断开,再连接上,将重复上面的过程,即完全增量的同步。
(2)Redis从2.8版本开始用PSYNC代替SYNC,来实现部分重同步。
(3)部分重同步通过复制偏移量、复制积压缓冲区、服务器运行ID三个部分来实现。
2.Sentinel
Sentinel哨兵模式是Redis的高可用解决方案。
(1)由一个或者多个Sentinel实例组成Sentinel系统,监视多个主服务器以及服务器下的所有从服务器。当一套主从服务器集群中,主节点出现下线或者故障时,将从从服务器中选举一个作为该集群的主服务器,如果以前的主服务器再次上线时,自动变成从服务器,将从新的主服务器复制数据保持同步。
(2)上述过程中从服务器是如何选出的?领头Sentinel先剔除离线的,较长时间没有回复INFO命令的,再根据优先级,复制偏移量来选择的;即选择状态良好的、数据完整的从服务器。
(3)Sentinel是一个系统,自身每个节点都是特定模式下的redis服务器,和普通Redis服务器使用的命令不同。
(4)Sentinel通过向主服务器发送INFO命令来获取主服务器和从服务器的相关信息。再每十秒一次向所有记录的主从服务器发送INFO命令,当集群在主节点故障转移过程中,将改为一秒一次。
(5)Sentinel以每秒一次向所有实例(所有主从服务器和其他Sentinel)发送PING命令,再根据回复是否有效,如果一定时长都收到无效回复,Sentinel认为该实例主观下线。随后该Sentinel会向其他Sentinel进行询问,是否同意该实例是否进入主观下线,如果同意的数量达到足够时,将判断该实例为客观下线。如果该实例是主服务器,即将开始故障转移操作。
(6)故障转移操作必须是领头Sentinel来下发,当存在多个Sentinel时需选举其中一个作为领头。
每个发现主服务器进入客观下线的sentinel都可以要求其他sentinel选自己为领头sentinel,选举是先到先得。同时每个sentinel每次选举都会自增配置纪元,每个纪元中只会选择一个领头sentinel。如果所有超过一半的sentinel选举某sentinel领头sentinel。如果在给定时间内,未选出,将重来直到选出为止。
补:选举方法是参考的raft协议(一致性分布式协议),被现在很多分布式系统采用。
(7)存在多个Sentinel时,任意Sentinel都需每两秒一次发现hello频道消息给其他Sentinel以宣告自己的存在。
3.Redis Cluster
Redis3.0上加入了cluster模式,实现的redis的分布式存储。
(1)每个主节点还可以再加入自己的从节点,主节点兼有Sentinel特性,从而也可以实现复制和故障转移的功能。如果主节点A和它的从节点A1都宕机了,那么该集群就无法再提供服务了。
(2)集群即主主之间通过分片(sharding)来分配和共享数据。集群的整个数据库被分为16384个槽(slot),所以每个主节点可以处理0-16383个槽。
(3)槽被分配(槽指派)的信息存在一个数组下,数组下标为16384/8=2048,即二进制位数组,每个位代表被分配的状态。二进制位数组前面已有讲述,使用在这里非常优秀。
(4)每个键都属于这16384个哈希槽的其中一个,公式 CRC16(key) % 16384来计算键key属于哪个槽。
(5)集群的各个节点通过Gossip协议来交换各自信息,该协议由MEET、PING、PONG三种消息实现。
补充:Gossip来源于流行病学的研究(ps,当前正是疫情之下,让人瑟瑟发抖)
- 一个节点状态发生变化,并向临近节点发送更新信息
- 对于节点状态变化的信息随机发送给b个节点
- 随着时间推移,信息能够传达到所有的节点
Gossip简单、高效,同时具有很好的可扩展性和鲁棒性,非常适合大规模、动态、资源受限的网络环境。
(6)节点的FAIL是通过集群中超过半数的节点检测失效时才生效。
集群的演进:主从复制实现了读写分离,哨兵模式实现了故障转移,集群模式实现了分布式存储。
个人收获总结:读书除了可以系统的了解一个key-value数据库的实现外,还可以看到版本的演进例如集群。还可以见识和学习巩固很多策略和算法,例如跳跃表、二进制位数组、LRU、raft、LUA脚本、Gossip协议等等。开卷有益。如果再深一步,看源码我想收获会更大吧。