Redis设计与实现
简述Redis设计与实现
Redis是一个高性能的key-value的非关系型数据库,Redis是运行在内存中的一种数据库,但是它也可以持久化到磁盘中,Redis的实现有着更为复杂的数据结构并且提供对他们的原子性操作。
-
Redis的优势
-
Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,以便redis服务器重启的时候再次加载使用。
-
Redis不仅仅支持简单的key-value类型数据的存储,同时还提供了其它的数据结构存储例如:list、set、zset、hash等。
-
Redis支持三种模式部署分别是:单机部署、主从部署和集群部署;为数据备份提供了支持。
-
Redis性能极高,它的读速度是110000次/s,写的速度是80000次/s,
-
Redis的所有操作都是原子性的,要么成功,要么失败,单个/多个操作都是原子性的,Redis也支持事务,但是Redis的事务是基于Multi和Exec指令的。
-
Redis还支持发布定于进行消息队列特性等。
-
-
Redis的应用场景
- 缓存、排行榜、计数器、共享Session、分布式锁、消息队列、位操作、购物车、全局Id、限流、抽奖、点赞、签到、商品标签、关注等。
-
Redis的实现
Redis的实现在内部使用了大量的特性,例如:内存数据库、高效的数据结构、网络I/O多路复用、虚拟内存机制等。
-
数据结构
Redis使用了大量的数据结构,它的数据结构分别有以下几种:简单动态字符串、链表、字典、跳跃表、整数集合、压缩列表等
- 简单动态字符串
Redis的字符串没有使用C语言的字符串,而是自己构建了一种简单的动态字符串(SDS),C字符串并不记录自身的长度信息,程序必须遍历整个字符串,对遇到的每个字进行计数,直到遇到代表字符串结尾的空字符为 止,这个操作的复杂度为O(N)。而Redis的SDS获取长度的时间复杂度是O(1),除此之外C语言的字符串还会存在缓冲区溢出,而SDS的空间分配策略解决了缓冲区溢出问题,当SDS API需要对SDS进行修改时,API会先检查SDS空间是否满足所需要求,不满足的会进行自动扩容,不需要手动修改SDS空间大小,SDS也采用了空间与分配原则SDS每次进行扩容是(当前长度*2)+1,它也是二进制安全,兼容部分C字符串函数。
- 链表
链表这种数据结构在Redis中应用非常广泛,链表提供了高效的节点重排能力,和顺序的节点访问方式,因为Redis使用的C语言没有内置链表数这种数据结构,所以Redis自己实现了链表。Redis链表的实现实用的是双向链表,在节点中分别存储了value、next、prev;总的来说redis链表的特性如下:
- 双向链表:从某个节点获取前/后节点时间复杂度都是O(1);
- 无环链表:表头节点的prev指针和表尾节点的next指针都指向了null;
- 带有链表长度的计数器:Redis的链表有一个属性来对链表节点进行计数,所以获取链表节点的数量的复杂度为O(1)。
- 字典
字典在Redis中应用也非常广泛,Redis的数据库就是使用字典来作为底层实现的,对Redis的增、删、改、查操作也是构建在对字典的操作之上。因为C语言没有内置字典这种数据结构,所以Redis也是自己构建了字典的实现。Redis的字典使用哈希表作为底层的实现,一个哈希表里面可以有多个哈希表节点,每个哈希表节点保存了字典中的一个键值对。针对哈希冲突Redis使用的是拉链法《拉链法其实说白了就是在一个hash值上形成了一个单链表数组每次新的添加到最前面》解决冲突,,使用MurmurHash2算法来计算键的哈希值。
- Rehash
哈希表保存的键值对会逐渐地增多或者减少,Redis为了让哈希表的 负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或 者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作来完成。
- 跳表(skiplist)
Redis查询快是因为它使用了跳表这种有序的数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳表支持平均O(logN)、最坏O(N)的复杂度节点的查找,,每个跳跃表节点的层高是随机生成一个介于1-32之间的值作为level数组的大小,这个大小就是层的高度。Redis的跳表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳 跃表信息(比如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点。在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是 唯一的。跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小 进行排序。跳表的原理
- 整数集合
整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且 这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。它可以保存类型为 int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。整数集合仅支持升级操作,不支持降级操作。升级操作为整数集合带来了操作上的灵活性,并且尽可能的节约了内存。
- 压缩列表(ziplist)
压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列 表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的 顺序型(sequential)数据结构。一个压缩列表可以包含任意多个节点(entry),每个节 点可以保存一个字节数组或者一个整数值。压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值。添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作, 但这种操作出现的几率并不高。
- 对象
在前面我们陆续介绍了Redis用到的所有主要数据结构,比如简单动 态字符串(SDS)、双端链表、字典、压缩列表、整数集合等等。Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创 建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种我们前面所介绍的数据结构。Redis的对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象的时候,这个对象所占用的内存就会被自动释放;另外,Redis还通过引 用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存。Redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长在服务器启用了maxmemory功能的情况下,空转时长较大的那些键可能会优先被服务器删除。Redis每种对象都至少使用了两种不同的编码。
-
列表对象(list)的编码可以是ziplist或者linkedlist。当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:列表对象保存的所有字符串元素的长度都小于64字节;列表对象保存的元素数量小于512个;不能满足这两个条件的列表对象需要使用 linkedlist编码。
-
Hash对象哈希对象的编码可以是ziplist或者hashtable。当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;哈希对象保存的键值对数量小于512个;不能满足这两个条件的哈希对象需要使用hashtable编码。
-
集合对象(set) 的编码可以是intset或者hashtable。当集合对象可以同时满足以下两个条件时,对象使用intset编码:集合对象保存的所有元素都是整数值;集合对象保存的元素数量不超过512个。不能满足这两个条件的集合对象需要使用hashtable编码。
-
有序集合对象(zset) 有序集合的编码可以是ziplist或者skiplist。ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨 在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素 则保存元素的分值(score)。
-
- 简单动态字符串
-
内存回收
因为C语言并不具备自动内存回收功能,所以Redis在自己的对象系统中构建了一个引 用计数(reference counting)技术实现的内存回收机制,通过这一机制,程序可以通过跟 踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。
-
对象共享
Redis除了实现引用计数内存回收机制之外,对象的引用计数属性还带有对象共享的作用。假设数据库中保存了整数值100的键不只有键A和键B两个,而是有一百个,那 么服务器只需要用一个字符串对象的内存就可以保存原本需要使用一百个字符串对象的内存才能保存的数据。
-
对象的空转时长
redisObject结构包含的一个属性为lru属性,该属性记录了对象最后一次被命令程序访问的时间,对象会记录自己的最后一次被访问的时间,这个时间可以用于计算对象的空转时间。
-
-
Redis键的过期策略
众所周知,Redis是可以设置过期时间,那么Redis有几种键过期策略呢?我们来说一下;Redis有三种过期策略分别是:定时过期、惰性过期、和定期过期。
- 定时过期
Redis的定时过期其实就是给每一个键添加一个定时器,这样的做法无疑是会占用大量的CPU。
- 惰性过期
Redis的惰性过期其实就是在你每次访问一个键的时候去检查一下是否过期,如果过期了就删除,这个策略会对CPU占用比较小,但是对内存不太友好,因为如果有很多一次性的key不在访问,则会占用大量内存。
- 定期过期
Redis的定期过期其实就是一个定时任务,每隔一段时间去扫描一定数量的过期时间字典并清除过期的key。
- 定时过期
-
Redis的持久化、多机Redis
-
Redis持久化
Redis持久化分为两种,分别是RDB持久化和AOF持久化,Redis默认开启的是RDB持久化,如果开启了AOF持久化那么AOF的持久化优先级高于RDB持久化。
- RDB持久化
因为Redis是内存数据库,一旦服务器重启你的保存的数据就会丢失,但是并不代表Redis没有持久化机制,为了解决这个问题Redis提供了RDB持久化功能,这个功能可以定时将某个时间点的数据保存到磁盘中,RDB除了保存数据外,还有一些过期键操作,也就是说RDB持久化只会保存没有过期的键,RDB持久化的实现原理是两个命令分别是SAVE和BGSAVE命令,SAVE这个命令会阻塞线程因为它是同步的;BASAVE是异步他其实就是派生出一个子进程由子进程负责创建RDB文件进行持久化。 Redis服务重启之后RDB文件载入到内存会阻塞服务器状态,一直等待RDB文件载入完成,才会放弃阻塞。
- AOF持久化
Redis的AOF持久化是通过保存Redis服务器所执行的写命令来进行数据库状态,AOF文件中的所有命令都以Redis命令请求协议的格式保存。命令请求会先保存到AOF缓冲区里面,之后再定期写入并同步到AOF文件。
- RDB持久化
-
Redis多机部署
- 主从复制
Redis的主从复制分为两种,第一种是RDB文件传输全量复制,也就是说你的slave第一次加入集群,那么它会将Master的RDB文件传输过来,进行全量复制,第二种是增量复制,增量复制采用的是命令传输模式。如果slave断开之后再次重新连接Master,在Master同步备份有偏移量,如果没有超出这个偏移量会继续增量同步,如果超出偏移量则需要人为进行一次全量同步。,主从复制不会阻塞Master的I/O读写,全量复制是在Master上fork一个子进程执行bgsaveRDB文件。
- Sentinel(哨兵模式)
Redis的哨兵模式是高可用的一种解决方案,哨兵用来监控Redis服务器的运行状态,如果一个集群中的Master下线之后Sentinel会重新选举服务器,Sentinel会选择出一个数据偏移量最大的作为主服务器,如果有多个相同偏移量的则按照运行ID进行排序选择一台ID最小的作为主服务器。
- 集群
Redis集群是Redis提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。它是Redis分布式数据库一种的方案,集群提供了高可用性,一部分节点失效不影响处理请求,数据自动切分到多个节点中,集群部署至少需要3个Master和3个Slave。
- 主从复制
-
-
总结
本篇主要是简述了Redis的整体的一个设计和他的部分功能的实现和使用的数据结构,以及主从复制和哨兵选择Master节点按照什么选择的等等。本系列有两篇,下一篇将着重Redis的经典面试题。
▼-----------------------------------------▼-----------------------------------------▼