Redis线程模型
redis(Remote Dictionary Server)本质上是一个key-value型的数据库,整个数据库加载在内存当中运行定期通过异步操作把数据库数据flush到硬盘上,因此是纯内存操作。
Redis是基于Reactor模式开发的网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构分为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列是单线程的,所以redis才叫做单线程模型。
消息处理流程
尽管多个文件事件可能会并发的出现,但I/O多路复用程序总是会将所有产生事件的套接字都推到一个队列里面,然后通过这个队列,以有序、同步、每次一个套接字的方向向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕之后,I/O多路复用才会继续向文件事件配送器传送下一个套接字。
Redis使用的是单线程模型,那为什么快呢?
Redis为什么快?
Redis快体现在什么地方?它接收到键值对操作后,能以微妙级别的速度找到数据,并快速完成操作。数据库这么多,为什么Redis有这么突出的表现呢?一个重要原因是因为Redis快,那为什么Redis如此之快呢,主要有两个原因
- Redis是内存数据库,所有的操作都是在内存上完成,内存操作本来就快。
- 另一方面归功于它的数据结构。键值对是按一定的数据结构来组织的,操作键值对最终就是对数据结构进行增删改查操作,所以高效的数据结构是Redis快速处理处理数据的基础。
Redis值的数据结构常用的有5种:String、List、Hash、Set、Sorted Set。这里说的数据结构是它们底层是如何实现的。简单来说,底层数据结构一共有6种,分别是:简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组。
String类型的底层实现只有一种数据结构,也就是简单动态字符串。而List、Hash、Sorted Set、Set这四种数据类型,都有两种底层实现,通常把这四种类型称为集合类型,他们的特点是一个键对应了一个集合的数据。
这些数据结构都是值的底层实现,键和值本身之间用什么结构组织的呢?为什么集合类型有那么多底层结构,他们都是怎么组织数据的,都很快吗?什么是简单动态字符串,和常用的字符串是一回事吗?
全局Hash表
为了实现从键到值的快速访问,Redis使用了一个哈希表来保存所有键值对。一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。所以我们常说,一个哈希表是由多个哈希桶组成的,每个哈希桶中键值对数据。看到这里可能会问,如果值是集合类型的话,作为数组元素的哈希桶怎么保存呢?其实哈希桶中的元素并不保存值本身,而是指向具体的指针。这也就是说,不管值是String,还是集合类型,哈希桶中的元素都是指向它们的指针。
哈希桶中的entry元素保存了key和value指针,分别指向了实际的键和值,这样一来,即使是一个集合,也可以通过value指针被查到。
因为这个哈希表保存了全部键值对,所以被称为全局哈希表。哈希表的最大好处是可以用O(1)的时间复杂度快速找到键值对。这个查找过程依赖于哈希计算,和数据量的多少没有直接关系。也就是说,不管是10万个键还是100万个键,我们只需要一次计算就能找到相应的键。
如果只了解哈希表的O(1)复杂度快速查找特性,那么当往Redis中写入大量数据后,就可能发现操作突然变慢了,这其实是因为忽略了一个潜在的风险,那就是哈希冲突和rehash可能带来的操作阻塞。
Hash表为何变慢了
当向哈希表中写入更多数据时,哈希冲突是不可避免的问题。这里的哈希冲突指两个key的哈希值和哈希桶计算对应关系时,正好落在一个哈希桶中。毕竟哈希桶的数量通常要少于key的数量,也就是说,难免会有一些key的哈希值对应到同一个哈希桶中。
解决哈希冲突的方法有两种,一是开放寻址法,二是拉链法。Redis使用的是第二个。
entry1、ertry2、entry3都需要保存在哈希桶3中,导致哈希冲突,此时entry1元素会通过一个next指针指向entry2,同样entry2指向entry3,即使哈希桶3中有100个元素,也能把它们连接起来。
但是,哈希冲突链上的元素只能通过指针逐一查找在操作。如果哈希表写入的数据越来越多,哈希冲突可能越来越多,这就会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率低。所以Redis会对哈希表做rehash操作。rehash就是增加现有哈希桶数量,让逐渐增多的entry元素能在更多的桶之间分散保存,减少单个桶中元素的数量,从而减少单个桶中的冲突。
为了使rehash操作高效,Redis使用了两个全局哈希表:哈希表1和哈希表2,一开始刚插入数据时默认使用哈希表1,此时的哈希表2并没有分配空间。随着数据增多,Redis开始执行rehash,rehash的步骤:
- 给哈希表2分配更大的空间,如哈希表1的两倍
- 把哈希表1中的数据重新映射拷贝到哈希表2中
- 释放哈希表1的空间
到此,可以从哈希表1切换到哈希表2,用更大的哈希表2保存更多的数据,而原来的哈希表1留作下一次rehash扩容。这个过程看似简单,但第二步涉及大量数据拷贝,如果一次性把哈希表1中的数据都迁移完,会造成Redis线程阻塞,无法服务其他请求。此时Redis就无法快速访问数据了。为了避免这个问题,Redis采用另外渐进式rehash。
这样就把一次大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。
到这里应该能理解Redis的键和值是怎么通过哈希表组织的了。对于String类型来说,找到哈希桶就能直接操作了,所以,哈希表的O(1)操作复杂度就是它的复杂度。但是对于集合类型,即使找到了哈希桶,还要在集合中再进一步操作。
和String类型不同,一个集合类型的值,第一步是通过全局哈希表找到对应的哈希桶位置,第二步是在集合中增删改查。集合的操作效率与哪些因素有关呢?
- 与集合底层数据结构有关。如,使用哈希表实现集合,要比链表实现的集合访问效率高;
- 和操作本身特点有关,比如读写一个元素比读写所有元素效率高。
到这里你明白了为什么Redis快了吗?什么时候进行Rehash?其他的底层数据结构不做过多介绍,欢迎留言提问讨论。
往期精彩内容推荐