数据结构与对象
SDS(Simple Dynamic String)
struct sdshdr {
int len;//已使用字节数
int free;//未使用字节数
char buf[];//字节数组
}
特点:
1、空间预分配策略,减少内存重分配次数(内存分配涉及系统调用)。
2、获取长度O(1),通过属性记录字符串长度。
3、杜绝缓冲区一次,通过API对SDS进行修改时,会先检查空间是否满足条件。
4、惰性空间释放。
5、二进制安全,所有SDS API都会以二进制方式处理字节数组,不用像c字符串以\0结尾,能够存储图片、音频、视频、压缩文件等二进制数据。
链表
带头结点的双向链表。
字典
采用哈希表实现,通过联地址法解决键冲突。
Rehash
让哈希表负载因子维持在一个合理的范围之内。
1、渐进式Rehash,rehash的动作不是一次性、集中式完成的,而是分多次、渐进式处理。每次对字典执行添加、删除、查找或者更新操作,根据哈希函数计算出对应的一个哈希索引,将哈希索引对应的所有元素(链表)一起进行rehash,这样将整个rehash过程带来的性能损耗均摊到一系列操作上,平滑处理。当然,rehash过程中,数据分散在两张hash表中,对字典的操作会同时处理两个hash表。
跳跃表
跳跃表支持平均O(logN)、最坏O(N)的节点查找,还可以通过顺序性操作来批量处理节点。
思考:跳跃表借鉴数据库聚集索引的思想,通过多层索引,加快数据检索效率,用空间换时间。
整数集合
typedef struct intset {
uint32_t encoding;//编码方式
unit32_t length; //集合包含的元素数量
int8_t content[]; //保存元素的数组
}
通过编码方式指导数组内容的解析,提供灵活的整数集合API。用尽可能小的内存,使用统一标准保存所有整数。
压缩列表
压缩列表是Redis为了节约内存而开发。存储结构的设计思路参考Sql Server元组的存储结构,可以参看《Sql Server存储引擎》
参考:https://blog.csdn.net/sunxianghuang/article/details/51828516
对象
Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含
- 字符串对象
- 列表对象
- 哈希对象
- 集合对象
- 有序集合对象
Redis中的每个对象由一个RedisObject结构表示:
typedef struct redisobject {
unsigned type:4;//类型
unsigned encoding:4;//编码类型
void *ptr;//指向底层实现数据结构的指针
}
Redis可以根据不同的使用场景来为一个对象设置不同的编码,即底层实现类型,从而优化对象在某一个场景下的效率。
字符串对象
1、保存的是整数值,并且这个整数可以用long类型表示,编码为int,即使用整数存储数据。
2、保存的是字符串,并且长度> 39字节,那么使用SDS存储数据,编码为raw。
3、保存的是字符串,并且长度<=39字节,编码为embstr,将redisobject和sdshdr存储在一块连续的内存空间,一方面减少内存分配次数,另一方面充分利用内存的缓存机制。
列表对象
1、元素数量<512 且 所有元素长度小于64字节,使用压缩列表ziplist
2、否则,使用双向链表linkedlist
哈希对象
1、键值对数量<512 且 所有键和值长度小于64字节,使用ziplist
2、否则,使用hashtable
集合对象
1、所有元素都是整数 且 元素数量<= 512 ,使用intset
2、否则,使用hashtable
有序集合
1、元素个数<128 且 所有元素长度<64,使用ziplist
2、否则,使用skiplist
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表。
typedef struct zset {
zskiplist *zsl;
dict * dict;
}
为什么同时使用跳跃表和字典实现?
1、字典实现O(1)查找复杂度,适合点查询和点处理。
2、字典提供有序访问,适合批量查询和处理。
思想:通过空间换时间。
内存回收
通过引用计数技术实现内存的回收机制。
对象共享
整数值对象,通过指针指向共同的存储对象,节省空间。类似Java JVM中string对象的共享,包名、静态字符串等会共享一个对象。JVM在编译阶段处理对象共享,而Redis在程序执行阶段处理对象共享。
Redis为什么不共享字符串对象?
针对字符串对象,决定是否可以共享对象前,需要通过检测字符串相等来匹配共享对象,但是字符串匹配时间复杂度为O(N),CPU开销大。
单机数据库的实现
数据库
通过dice字典保存所有键值对。
过期建删除策略
1、定时删除,为每个设置过期时间的建,创建一个定时器timer,内存最友好,CPU时间最不友好。
2、惰性删除,只有访问键时,才去检查是否过期,内存最不友好,CPU最友好。
3、定期删除,每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
RDB 持久化
将Redis在内存中的数据库状态保存到磁盘里面,避免数据意外丢失。
AOF持久化
通过保存Redis所执行的写命令来记录数据库状态。AOF并不是简单的写命令日志文件,而是将数据库当前的状态用最小化的写命令表述。相比RDB,AOF更具有动态性,具有增量备份的特性。
AOF不同的持持久化行为
1、always,永远执行文件同步。
2、everysec,每秒执行一次文件同步。
3、no,由操作系统执行文件同步。
文件的写入和同步
为了提高文件的写入效率,当用户调用write函数将数据写入到文件,操作系统通常会将写入数据保存在一个内存缓冲区里面,等到缓冲区的空间被填满、或者超过指定的时间限制之后,才真正地将缓冲区中的数据写入到磁盘中。
这种做法虽然提高了效率,但是也为写入数据带来了安全问题,如果计算机发生停机,保存在内存缓冲区里面的写入数据将会丢失。
事件
Redis服务器是一个时间驱动程序,时间分为:文件事件 和 时间事件。
文件事件
文件事件处理器使用I/O多路复用程序来监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
虽然文件事件处理器以单线程方式运行,但是使用I/O多路复用程序来监听多个套接字,文件事件处理器即实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程方式运行的模块进行对接,这样保持了Redis内部单线程设计的简单性。
时间事件
定时事件和周期性事件。
多机数据库的实现
复制
同步
开始同步时,主服务器生成当前数据库状态的RDB文件,然后发送给从服务器,从服务器加载该RDB文件将自己的数据库状态更新至同步开始时的状态。同步过程中执行的所有写命令,主服务器会记录在缓冲区。在RDB文件发送完毕后,缓冲区中的所有写命令将会发送给从服务器,从服务器执行同步过程中产生的写命令,最终保持与主服务器状态一致。
命令传播
在同步操作执行完毕之后,主从服务器数据库状态达到一致,但是这种一致并不是一成不变的,每当主服务器执行客户端发送的写命令时,主服务器必须将该写命令传播给所有从服务器并执行,才能保证主从服务器的实时同步。
断线后重复值
从服务器断线后重新连上主服务器,如果采用全量复制的策略,将会导致效率底下。实际上我们只需要采用增量复制策略,重新执行断线后所有的写命令即可。那么如何识别断线后的写命令呢?可以使用序号标记写命令,写命令的序号随着时间递增,当前服务器接收到最大的命令序号为serial_last,这样只需要让主服务器重新发送序号>serial_last的写命令即可。但是,主服务器不可能保存所有写命令及其序号(存储成本高),只能使用复制积压缓冲区保存最近特定数量的写命令及其序号,假设复制积压缓冲区最小的命令序号为serial_min,只有serial_min<=serial_last时,才能执行增量复制。
思考:这里复制积压缓冲区的思想类似于计算机网络里TCP滑动窗口。
哨兵(Sentinel)
Sentinel是Redis的高可用性解决方案,由一个或多个Sentinel实例组成的Sentinel系统,可以监视任意多个主服务器,以及这些主服务器从属的从服务器。当检测到主服务器进入下线状态时,自动选择某个从服务器升级为主服务器。
集群
Redis集群是Redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能。
slot指派
Redis集群通过分片的方式保存数据库中的键值对:集群中整个数据库被分为16384个slot,数据库中的每个键映射到其中一个slot,集群中的每个节点可以处理0个或者多个slot(通过二进制位数组记录)。通过哈希函数CRC16(key)&16383实现key到slot的映射。
集群中各个节点会通过消息传播自己负责的slot集合,以及该节点已知的其他节点负责的slot集合。这样所有节点都能知道全局各个节点处理的slot信息。
当接收到特定key的命令,首先计算映射的slot,如果当前节点负责该slot的处理,那么当前节点将会处理该命令,否则,查找到负责处理该slot的节点,并通知客户端向目前节点重新发送命令请求。
思考:为什么不使用一致性哈希?
参考:https://blog.csdn.net/clz1314521/article/details/80604555
独立功能的实现
发布和订阅
Redis将所有频道的订阅关系都保存在服务器状态的pubsub_channels字典里面,这个字典的键是某个被订阅的频道,而键的值是一个链表,链表里记录了所有订阅这个频道的客户端。
所有模式的订阅关系都保存在服务器状态的pubsub_patterns里面,pubsub_patterns是一个链表,每个节点记录被订阅的模式,以及所有订阅该模式的客户端。为什么这里使用链表?因为模式匹配需要遍历所有模式,而频道匹配只需要通过hash映射查找。
事务
事务提供了一种将多个命令请求打包,然后一次性、顺序执行多个命令的机制。
Redis事务通过乐观锁实现并发,Watch命令可以在Exec命令执行之前,监视任意数量的数据库键,如果Exec命令执行时,存在被监视的键已修改,那么服务器将会拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
Redis是单线程模型,因此如果被监视的键无修改,事务执行一定能够保证ACID特性。
思考:Redis事务与关系数据库事务的区别?
内容源自《Redis设计与实现》