说到redis我们都知道,redis查询很快,常常被用来做缓存。在查询key值时,能以微秒级别的速度查询到数据。
redis查询为什么这么快?
- 内存数据库,我们知道内存的访问速度都是很快的,而redis就是内存数据库,所有的操作都在内存中完成的。
- redis的数据结构有一定的优势,redis操作键值对就是对数据结构进行增删改查。
redis的数据类型有哪些,我们都知道有5种:String、List、Hash、Set、Sorted Set。但是我们仔细研究一下他们底层的数据结构就会发现,他们一共有6种:简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组。
他们的时间复杂度分别是:
名称 | 时间复杂度 |
哈希表 | O(1) |
跳表 | O(logN) |
双向链表 | O(N) |
压缩列表 | O(N) |
整数数组 | O(N) |
我们可以看出String的底层实现也是一种数据结构,而List、Hash、Set、Sorted Set都有两种底层实现,他们的特点都是一个键对应了一个集合的数据。
在redis中为了实现键和值的快速访问,使用一个哈希表来保存所有的键值对。
在上图中可以看到哈希桶中存放的并不是值本身,而是指针来指向具体值,这个哈希表保存了所有的键值对,也称为全局哈希表。这个哈希表的优势是我们查找键值对的时间复杂度是O(1)。但是也存在一个风险就是当数据也来越多时,由于哈希冲突和rehash操作会带来阻塞问题。
哈希冲突是指,两个不同的key通过哈希运算,正好同时落在了衣一个哈希桶里。redis的解决方法就是使用链式哈希,这也是比较容易理解的,在同一个哈希桶的多个元素使用一个链表来保存,他们之间依次使用指针连接。这也比较常见的思路,在很多开源代码中都使用了这种思想,比如JDK1.7中HashMap等的实现都使用链式哈希来解决哈希冲突。
redis之所以对哈希表做rehash操作,就是因为但一个哈希桶中的链表越来越长时,导致该链表上元素查找耗时长,效率降低。rehash操作就可以将逐渐增多的entry元素在更多的桶之间分散保存,减少单个桶的元素数量,减少单个桶的冲突。为了rehash的操作更高效,redis默认使用两个全局哈希表:一开始插入数据时默认使用哈希表1,此时哈希表2没有被分配空间,随着数据增多,redis执行rehash:
- 给哈希表2分配更大的空间,可以是哈希表1的2倍;
- 把哈希表1的数据重新映射并拷贝到哈希表2
- 释放哈希表1的空间
此时,我们就可以将哈希表1切换到哈希表2;用增大的哈希表2保存更多的数据,原来的哈希表1留作下一次rehash备用。
但是这又会造成另一个问题,就是大面积的拷贝数据,必然会造成Redis的线程阻塞,无法处理其他请求,造成访问数据变慢。
为了避免这个问题,Redis使用渐进式rehash。
简单而言,就是在拷贝数据时,Redis依然正常处理客户端的请求,每处理一个请求时,从哈希表1的第一个索引位置开始,将索引上所有的entry拷贝到哈希表2;等待处理下一个请求时,顺带拷贝哈希表1中的下一个索引位置的entry。可以把一次性大量的拷贝开销分配到多次请求过程找那个,避免长时间耗时操作。