Redis设计思路学习与总结
宋增宽,腾讯⼯程师,16年毕业加⼊腾讯,从事海量服务后台设计与研发⼯作,现在负责QQ后台等项⽬,喜欢研究技术,并思考技术演变,专注于⾼并发业务架构的设计与性能优化。
下半年利⽤空余时间研究和分析了部分Redis源码,本⽂从⽹络模型、数据结构和内存管理、持久化和多机协作四个⾓度对redis的设计思路进⾏了分析,若有不正确之处,希望各路⼤神指出。
Redis是业界普遍应⽤的缓存组件,研究⼀个组件框架,最直观的办法就是从应⽤⽅的⾓度出发,将每个步骤的考虑⼀番,从这些步骤⼊⼿去研究往往能够最快的体会到⼀个组件框架的设计哲学。以Redis为例,每当发起⼀条请求时,redis是如何管理管理⽹络请求,收到请求后⼜是通过什么样的数据结构进⾏组织并操作内存,这些数据⼜是如何dump到磁盘实现持久化,再到多机环境下如何同步和保证⼀致性……本⽂就是从⽹络模型、数据结构设计与内存管理、持久化⽅法和多机四个⾓度简要描述了redis的设计和⾃⼰的⼀点体会。
⼀.⽹络模型
Redis是典型的基于Reactor的事件驱动模型,单进程单线程,⾼效的框架总是类似的。⽹络模型与spp的异步模型⼏乎⼀致。
Redis流程上整体分为接受请求处理器、响应处理器和应答处理器三个同步模块,每⼀个请求都是要经历这三个部分。
Redis集成了libevent/epoll/kqueue/select等多种事件管理机制,可以根据操作系统版本⾃由选择合适的管理机制,其中libevent是最优选择的机制。
Redis的⽹络模型有着所有事件驱动模型的优点,⾼效低耗。但是⾯对耗时较长的操作的时候,同样⽆法处理请求,只能等到事件处理完毕才能响应,之前在业务中也遇到过这样的场景,删除redis中全量的key-value,整个操作时间较长,操作期间所有的请求都⽆法响应。所以了解清楚⽹络模型有助于在业务中扬长避短,减少长耗时的请求,尽可能多⼀些简单的短耗时请求发挥异步模型的最⼤的威⼒,事实上在Redis的设计中也多次体现这⼀点。
⼆.数据结构和内存管理
1.字符串
1.1 结构
Redis的字符串是对C语⾔原始字符串的⼆次封装,结构如下:
struct sdshdr {
long len;
long free;
char buf[];
};
可以看出,每当定义⼀个字符串时,除了保存字符的空间,Redis还分配了额外的空间⽤于管理属性字段。
1.2 内存管理⽅式
动态内存管理⽅式,动态⽅式最⼤的好处就是能够较为充分的利⽤内存空间,减少内存碎⽚化,与此同时带来的劣势就是容易引起频繁的内存抖动,通常采⽤“空间预分配”和“惰性空间释放”两种优化策略来减少内存抖动,redis也不例外。
每次修改字符串内容时,⾸先检查内存空间是否符合要求,否则就扩⼤2倍或者按M增长;减少字符串内容时,内存并不会⽴刻回收,⽽是按需回收。
关于内存管理的优化,最基本的出发点就是浪费⼀点空间还是牺牲⼀些时间的权衡,像STL、tcmalloc、protobuf3的arena机制等采⽤的核⼼思路都是“预分配迟回收”,Redis也是⼀样的。
1.3 ⼆进制安全
判断字符串结束与否的标识是len字段,⽽不是C语⾔的'\0',因此是⼆进制安全的。
放⼼的将pb序列化后的⼆进制字符串存⼊redis。
简⽽⾔之,通过redis的简单封装,redis的字符串的操作更加⽅便,性能更友好,并且屏蔽了C语⾔字符串的⼀些需要⽤户关⼼的问题。
2.字典(哈希)
字典的底层⼀定是hash,涉及到hash⼀定会涉及到hash算法、冲突的解决⽅法和hash表扩容和缩容。
2.1 hash算法
Redis使⽤的就是常⽤的Murmurhash2,Murmurhash算法能够给出在任意输⼊序列下的散列分布性,并且计算速度很快。之前做共享内存
的Local-Cache的需求时也正是利⽤了Murmurhash的优势,解决了原有结构的hash函数散列分布性差的问题。
2.2 hash冲突解决⽅法
链地址法解决hash冲突,通⽤解决⽅案没什么特殊的。多说⼀句,如果选⽤链地址解决冲突,那么势必要有⼀个散列性⾮常好的hash函数,否则hash的性能将会⼤⼤折扣。Redis选⽤了Murmurhash,所以可以放⼼⼤胆的采⽤链地址⽅案。
2.3 hash扩容和缩容
维持hash表在⼀个合理的负载范围之内,简称为rehash过程。
rehash的过程也是⼀个权衡的过程,在做评估之前⾸先明确⼀点,不管中间采⽤什么样的rehash策略,rehash在宏观上看⼀定是:分配⼀个新的内存块,⽼数据搬到新的内存块上,释放旧内存块。
⽼数据何时搬?怎么搬?就变成了⼀个需要权衡的问题。
第⼀部分的⽹络模型上明确的指出Redis的事件驱动模型特点,不适合玩长耗时操作。如果⼀个hashtable⾮常⼤,需要进⾏扩容就⼀次性把⽼数据copy过去,那就会⾮常耗时,违背事件驱动的特点。所以Redis依旧采⽤了⼀种惰性的⽅案:
新空间分配完毕后,启动rehashidx标识符表明rehash过程的开始;之后所有增删改查涉及的操作时都会将数据迁移到新空间,直到⽼空间数据⼤⼩为0表明数据已经全部在新空间,将rehashidx禁⽤,表明rehash结束。
将⼀次性的集中问题分⽽治之,在Redis的设计哲学中体现的淋漓尽致,主要是为了避免⼤耗时操作,影响Redis响应客户请求。
3.整数集合
变长整数存储,整数分为16/32/64三个变长尺度,根据存⼊的数据所属的类型,进⾏规划。
每次插⼊新元素都有可能导致尺度升级(例如由16位涨到32位),因此插⼊整数的时间复杂度为O(n)。这⾥也是⼀个权衡,内存空间和时间的⼀个折中,尽可能节省内存。
4.跳跃表
Redis的skilplist和普通的skiplist没什么不同,都是冗余数据实现的从粗到细的多层次链表,Redis中应⽤跳表的地⽅不多,常见的就是有序集合。
Redis的跳表和普通skiplist没有什么特殊之处。
5.链表
Redis的链表是双向⾮循环链表,拥有表头和表尾指针,对于⾸尾的操作时间复杂度是O(1),查时间复杂度O(n),插⼊时间复杂度O(1)。Redis的链表和普通链表没有什么特殊之处。
三.AOF和RDB持久化
AOF持久化⽇志,RDB持久化实体数据,AOF优先级⼤于RDB。
1.AOF持久化
机制:通过定时事件将aof缓冲区内的数据定时写到磁盘上。
2.AOF重写
为了减少AOF⼤⼩,Redis提供了AOF重写功能,这个重写功能做的⼯作就是创建⼀个新AOF⽂件代替⽼的AOF,并且这个新的AOF⽂件没有⼀条冗余指令。(例如对list先插⼊A/B/C,后删除B/C,再插⼊D共6条指令,最终状态为A/D,只需1条指令就可以)
实现原理就是读现有数据库的状态,根据状态反推指令,跟之前的AOF⽆关。同样,为了避免长时间耗时,重写⼯作放在⼦进程进⾏。3.RDB持久化
SAVE和BGSAVE两个命令都是⽤于⽣成RDB⽂件,区别在于BGSAVE会fork出⼀个⼦进程单独进⾏,不影响Redis处理正常请求。
定时和定次数后进⾏持久化操作。
简⽽⾔之,RDB的过程其实是⽐较简单的,满⾜条件后直接去写RDB⽂件就结束了。
四.多机和集
1.主从服务器
避免单点是所有服务的通⽤问题,Redis也不例外。解决单点就要有备机,有备机就要解决固有的数据同步问题。
1.1 sync——原始版主从同步
Redis最初的同步做法是sync指令,通过sync每次都会全量数据,显然每次都全量复制的设计⽐较消耗资源。改进思路也是常规逻辑,第⼀次全量,剩下的增量,这就是现在的psync指令的活。
reactor线程模型 java1.2 psync
部分重同步实现的技术⼿段是“偏移序号+积压缓冲区”,具体做法如下:
(1)主从分别维护⼀个seq,主每次完成⼀个请求便seq+1,从每同步完后更新⾃⼰seq;
(2)从每次打算同步时都是携带着⾃⼰的seq到主,主将⾃⾝的seq与从做差结果与积压缓冲区⼤⼩⽐较,如果⼩于积压缓冲区⼤⼩,直接从积压缓冲区取相应的操作进⾏部分重同步;
(3)否则说明积压缓冲区不能够cover掉主从不⼀致的数据,进⾏全量同步。
本质做法⽤空间换时间,显然在这⾥牺牲部分空间换回⾼效的部分重同步,收益⽐很⼤。
2.Sentinel
本质:多主从服务器的Redis系统,多台主从上加了管理监控,以保证系统⾼可⽤性。
3.集
Redis的官⽅版集尚未在⼯业界普及起来,下⾯主要介绍⼀下集的管理体系和运转体系。
2.1 slot-集单位
集的数据区由slot组成,每个节点负责的slot是在集启动时分配的。
2.2 客户请求
客户请求时如果相应数据hash后不属于请求节点所管理的slots,会给客户返回MOVED错误,并给出正确的slots。
从这个层⾯看,redis的集还不够友好,集内部的状态必须由客户感知。
2.3 容灾
主从服务器,从⽤于备份主,⼀旦主故障,从代替主。
通过Redis的研究,深刻体会到的⼀点就是:所有设计的过程都是权衡和割舍的过程。同样放到⽇常的⼯作和开发中也是如此,⼀句代码写的好不好,⼀个模块设计的是否科学,就从速度和内存的⾓度去衡量看是否需要优化,并去评估每⼀种优化会收益到什么,同时会损失什么,收益远⼤于损失的就是好的优化,这样往往对于开发和提升更有针对性,更能提⾼效率。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。