Redis基础之一:数据结构和高性能IO模型
众所周知,Redis是非常出色的跨平台、非关系型、键值数据库。基于Redis的各种特性,我们可以构建出一套非常成熟的商业级数据存储方案,不夸张的说,Redis是现如今实际工作和面试中都必不可缺的一项技能,学好Redis已成为后端开发者(包括部分前端)的必要。
当我们在学习Redis的过程中,不仅会了解到高可用系统中所有需要思考的点位,还能领略到一个强大系统的设计之美。
那我们今天带着两个问题,开始进入到Redis的基础知识学习:
Redis有哪些数据结构?
Redis为什么快?
一、Redis的数据结构
我们都知道,Redis的数据类型有String(字符串)、List(列表)、Hash(哈希)、Set(集合)和 Sorted Set(有序集合)。他们属于键值中“值”的数据类型,在学习它们时我们首先要了解其底层实现和特点,才能在实际业务中因地制宜。
1.全局哈希表
首先,我们要了解什么是哈希表,哈希表其实就是一个数组,数组中的每一个元素都称之为哈希桶,哈希桶内保存的元素并不是值本身,而是指向具体值的指针。
在图中可以看到,哈希桶中的entry元素保存了*key和*value指针,分别指向了实际的键和值。而这个表保存了所有的键值对以方便快速的查询,所以也被称之为全局哈希表。哈希表结构的优势是,通过对键的哈希计算,可以迅速定位哈希桶的位置,从而用O(1)的时间复杂度完成查询。不管Redis中有多少数据量,这个过程只依赖于一次哈希计算,所以这也是Redis查询速度快的原因之一。
当我们了解了哈希表的优点之后,也需要考虑一下潜在的风险,那就是哈希冲突和rehash的阻塞,这个后续我会单独讲解。
2.基于哈希表的String、Hash、Set
对String或者Hash、Set集合中的单个元素进行增删改查操作,因为哈希表的使用,只需要O(1)的复杂度就可以快速操作键值对,效率是很高的。
但是对于集合的范围操作,要遍历集合中的元素,也有需要返回集合中所有数据的场景,这种情况下复杂度一般是O(N),我们应该避免此类操作。
3.基于跳表的Sorted Set
跳表是在链表的基础上,增加了多级索引,通过索引位置的几次跳转,实现定位数据。当数据量很大时,复杂度可以达到O(logN)。
4.基于双向链表和压缩列表的List
压缩列表类似于一个数组,数组中的每一个元素都保存着一个元素的数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数,压缩列表在表尾还有一个 zlend,表示列表结束。
在压缩列表中,如果要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。
所以List在高频随机读写的场景,由于底层数据结构的原因,它的性能并没有哈希表那么好,需要谨慎使用。List的优点是,由于压缩列表的表头三个字段,使POP/PUSH的效率很高,可以考虑将其应用在队列场景。
5.小结
了解到几种类型的底层数据结构原理之后就可以不用死记硬背了,哪怕遇到不熟悉的场景和操作,也使你有能力推理出不同操作的复杂度从而完成选型。原理就是打开未知的钥匙,在向上攀爬的同时,一定要牢固根基。
二、Redis的IO模型
在介绍Redis的IO模型之前,我们回到最开始的两个问题,Redis的数据类型都有哪些,Redis为什么这么快?
数据类型刚刚已经介绍过,Redis正得益于这种高性能的底层数据结构,再加上数据存储在内存中,使它的操作变得非常快速。但是仅这两点原因,并不能让Redis完成真正高性能的蜕变,还存在一个关键的问题,它的网络IO如何并发处理大量的客户端请求?接下来我们就来聊聊,Redis是如何以单线程之躯实现高吞吐量的。
1.Redis的通信方式
Redis选择了Socket网络通信的方式,提供键值存储服务,可以大大的提高Redis的应用范围。
2.Redis为什么是单线程
严格来说,Redis 的单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。下面将要讨论的单线程机制,可以将其理解为Redis的主线程。
当然,我们首先要知道为什么不选择多线程?多线程模式编程可以提高系统的吞吐量和扩展性,但如果系统没有较好的设计,那多线程的实际结果可能不会理想,这主要受限于两点原因:
a.在没有额外机制保证的情况下,不能保证共享资源的准确性.
b.添加额外机制可能带来额外的开销
这就是多线程编程模式面临的共享资源的并发访问控制问题。而选择单线程的模式后,只要解决掉网络IO过程中产生的阻塞,就可以完成对多线程性能上的超越。接下来就聊一聊Redis核心机制——Linux中的IO多路复用。
3.Linux的IO多路复用
在普通的IO操作中,Socket处理请求存在潜在的阻塞点,即当Redis监听到一个客户端有连接请求,但是一直未能成功建立连接,会阻塞在accept或者recv阶段,让其他客户端无法和Redis建立连接,这就导致Redis的线程被阻塞了。
Socket的非阻塞模式虽然可以让Redis在与客户端无法连接时,不再等待返回处理其他操作。但是我们还需要有机制去继续监听之前未完成的连接,或者监听已连接成功但还未有数据返回的请求,此时就需要Linux的IO多路复机制用来解决此场景。
Linux的IO多路复用机制,是指一个线程处理多个IO流,Linux内核可同时监听多个Socket操作(监听套接字和已连接套接字),一旦确认请求成立,就会交给Redis线程去处理,这就实现了一个Redis线程处理多个IO流的效果。
4.小结
通过了解了Redis的访问框架以及IO模型,Redis为何拥有每秒十万级别的高吞吐率,答案已呼之欲出:Linux的IO多路复用机制成为关键。只要遇到问题时,多考虑其底层实现的原理,哪怕只是浅尝辄止,也能有所进益。
三、总结
Redis为什么这么快?
相信此时的你心里已经有所答案,总结下:
1.操作在内存上完成
2.高效的数据结构
3.IO多路复用机制
亲爱的各位,这篇文章到这里就结束了。丸弟并不是大神,在写的时候也借鉴了很多资料,我尽量把知识转化为我自己的思路和语言,这不仅可以加深我本人对Redis的理解,若还能给其他同学提供到一点点参考,那我觉得都是意义非凡的。我希望可以把这个系列延续下去,和各位一起完成对Redis的进阶。
如果有哪些知识点整理的有错漏,希望能得到您的指正。