作者简介: 不肯过江东丶,一个来自二线城市的程序员,致力于用“猥琐”办法解决繁琐问题,让复杂的问题变得通俗易懂。
支持作者: 点赞、关注、留言~
作为程序员的我们,工作中肯定避免不了和 Redis 打交道。除了日常工作以外,我们在面试的时候也常常会被问到一些关于 Redis 的问题,其中出场率最高的就是:请你说说 Redis 为什么这么快?我们都知道 Redis 很快,它QPS可达10万(每秒请求数),但是很多小伙伴知道 Redis 快仅仅因为它是基于内存实现的,对于其它原因倒不是很清楚,也就不能回答出面试官想听到的东西。那么今天咱们就一起总结一下 Redis 为什么会这么快。
Redis 是基于内存存储实现的一个数据库,而其他传统数据库(如 MySQL)则是依赖磁盘实现的(我们暂且称之为磁盘数据库)。Redis 与传统磁盘数据库相比来说,Redis 将数据存放在内存,需要查询或存储数据时,直接对内存进行操作,省掉了磁盘 I/O 的步骤,也就减少了对应的消耗、加快了速度。
Redis 中一共有5种数据类型,分别是 String 数据类型、List 数据类型、Hash 数据类型(散列类型)、set 数据类型(无序集合)、SortedSet 数据类型(zset、有序集合)。不同的数据类型底层使用了一种或者多种数据结构来支撑,目的就是为了追求更快的速度。下面就对这些数据结构进行一个简单的讲解
P.S. 主要是本人经验有限,太深入的东西也讲不出来,各位小伙伴见笑了
我们知道 Redis 的底层是用C语言来编写的,Redis 的 String 底层数据结构实现并没有直接使用C语言中的数据结构,它为了实现方便的扩展,考虑到安全和性能,自己定义了一个结构用来存储字符串,这个数据结构就是:简单动态字符串(Simple Dynamic String 简称SDS),并将 SDS 用作 Redis 的默认字符串。
struct sdshdr {
//用于记录buf数组中使用的字节的数目,和SDS存储的字符串的长度相等
int len;
//用于记录buf数组中没有使用的字节的数目
int free;
//字节数组,用于储存字符串
char buf[]; //buf的大小等于len+free+1,其中多余的1个字节是用来存储’\0’的
};
上面的代码就是 Redis 中保存字符串对象的数据结构,相比于C语言来说,它也就多了 len 和 free 这两个变量,但就是两个变量却成了神来一笔
intset 是集合键的底层实现之一,如果一个集合满足只保存整数元素和元素数量不多这两个条件,那么 Redis 就会采用 intset 来保存这个数据集。intset 整数集合支持三种编码类型,分别是int16_t、int32_t、int64_t。通过 intset 整数集合。其数据结构如下
typedef struct intset {
uint32_t encoding; // 编码模式
uint32_t length; // 长度
int8_t contents[]; // 数据部分
} intset;
其中,contents 字段用于保存整数,数组中的元素要求不重复且按照从小到大的顺序排列;encoding 字段表示该整数集合的编码模式,在读取和写入的时候,均按照指定的 encoding 编码模式读取和写入。Redis 提供三种模式的宏定义如下:
// 数据以int16_t类型存放,每个占2个字节,能存放-32768~32767范围内的整数
#define INTSET_ENC_INT16 (sizeof(int16_t))
// 数据以int32_t类型存放,每个占4个字节,能存放-2^32-1~2^32范围内的整数
#define INTSET_ENC_INT32 (sizeof(int32_t))
// 数据以int64_t类型存放,每个占8个字节,能存放-2^64-1~2^64范围内的整数
#define INTSET_ENC_INT64 (sizeof(int64_t))
可以看出,虽然 contents 字段是通过 int8_t 类型来声明是,但是存储数据时并不以这个类型来存放数据。
intset 整数集合中最值得一提的就是升级操作。当 intset 中添加的整数超过当前编码类型的时候,它就会自动升级到能容纳该整数类型的编码模式。比如我们需要创建一个集合来存储1、2、3、4这四个元素,在创建该集合的时候,采用 int16_t 的类型来存储,如果此时需要在集合中存储一个更大的元素X,且元素X超出了当前集合能存放的最大范围,这个时候 Redis 就会自动对该整数集合进行升级操作,将 encoding 字段改成 int32_6 类型,并对contents 字段内的数据进行重排列。
压缩列表是 List 、hash、 sorted Set 三种数据类型底层实现之一。当只有少量数据,并且每个列表项要么为小整数值,要么是长度比较短的字符串时, Redis 就会使用压缩列表来做列表键的底层实现。
当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis 就会使用链表作为列表键的底层实现。
SortedSet 数据类型的排序功能就是通过 skipList 跳跃表数据结构来实现的。
skipList 跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳表在链表的基础上,增加了多层级索引,通过索引位置的几个跳转,实现数据的快速定位。
Redis 的键值对存储就是用字典实现的,Hash 的底层实现之一也是字典。因为本人对字典部分还不是很了解,这里就不多说了,有兴趣的小伙伴可以自行百度查阅。如果哪位小伙伴对字典部分比较了解,欢迎在评论区留言哦~
Redis 采用单线程模型并不是说 Redis 就只有一个线程,而是 Redis 对数据的所有操作都是由一个线程按顺序依次执行的。使用单线程模型可以有效的避免线程之间的竞争问题(如添加锁、释放锁、死锁等)、减少线程创建导致的性能消耗,同时也让代码变得更清晰,处理逻辑也更简单。Redis 官方也针对单线程模型给出了一个解释
因为 Redis 是基于内存的操作,CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。
我们解释一下“I/O多路复用模型”这句话,I/O 就很好理解了,它代表了网络 I/O;多路代表着多个套接字(socket)连接;复用的含义是共用一个线程或进程。简单来说就是,Redis 使用 I/O 多路复用模型同时监听多个套接字,并将这些事件推送到一个队列里,然后逐个被执行,最终将执行结果返回给客户端。采用I/O多路复用模型保证了 Redis 线程不会阻塞在某一个特定的监听或已连接套接字上,也就是说,不会阻塞在某一个特定的客户端请求处理上。正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性和吞吐量。
Gossip 通信协议是针对于 Redis 集群来讲的,Gossip 通信协议是 P2P 方式的通信协议,它拥有瘟疫一般的传播速度。当 Redis 集群中的一个节点 A 广播自身信息,其他节点收到了A节点广播出来的信息,那么这些节点再继续在集群中传播这个A节点的信息,一段时间后整个集群中所有的节点就都有了A节点的信息。是不是有点像村里聊八卦的大妈们,无论大妈们知道了什么小道消息,用不了两天,全村人就都知道了~
本人经验有限,有些地方可能讲的没有特别到位,如果您在阅读的时候想到了什么问题,欢迎在评论区留言,我们后续再一一探讨
希望各位小伙伴动动自己可爱的小手,来一波点赞+关注 (✿◡‿◡) 让更多小伙伴看到这篇文章~ 蟹蟹呦(●’◡’●)
如果文章中有错误,欢迎大家留言指正;若您有更好、更独到的理解,欢迎您在留言区留下您的宝贵想法。
你在被打击时,记起你的珍贵,抵抗恶意;
你在迷茫时,坚信你的珍贵,抛开蜚语;
爱你所爱 行你所行 听从你心 无问东西