Redis接收到一个键值对操作后能以微秒级别的时间找到数据,并完成操作。那他为什么这么快呢?
一、存储位置
首先就是因为redis是一个内存型数据库,其数据是存在内存中的,所以读取速度比存在磁盘中的数据不是快了一星半点。
不过这是用他的安全性换了的,因为一旦redis所在的服务器宕机或者断电,其内存中的数据就会丢失,这就是redis为什么需要RDB和AOF的原因了。
二、 键值对的数据结构
另一大原因就是他的数据结构了。全局哈希表和为了解决冲突的rehash
1)全局哈希表
为了实现快速访问,redis采用了一个全局哈希表来存储所有的键值对。如上图所示,
哈希表即一个数组,里面存储的是“哈希桶(entry)”,每当存储一个键值对时,会根据key进行hash计算,得到他在全局哈希表的位置,然后生成一个entry存入其中。entry中包含*key和*value, 其中*key为存入键值对的key,然而*value则是指向值的指针。
因此,当你想要从redis中定位到一个值,只需要根据key找到“哈希桶”的位置,然后根据*value指针找到对应的值,无论redis有10条数据还是10000条数据,都只需要一次即可找到想要的数据,时间复杂度为O(1)。
2)哈希冲突和rehash
然而真的有这么简单吗?当然不是,你会发现随着数据的增多,redis的查询速度会越来越慢,这是为什么呢?主要是因为 “哈希冲突”:
Redis中的全局哈希表的大小不可能是无限大的,当数据量很大时,两个 key 的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中, 这就是哈希冲突。 Redis 解决哈希冲突的方式,就是链式哈希。链式哈希也很容易理解,就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。如下图所示:
entry1、entry2 和 entry3 都需要保存在哈希桶 3 中,导致了哈希冲突。此时,entry1 元素会通过一个*next指针指向 entry2,同样,entry2 也会通过*next指针指向 entry3。这样一来,即使哈希桶 3 中的元素有 100 个,我们也可以通过 entry 元素中的指针,把它们连起来。这就形成了一个链表,也叫作哈希冲突链。
当哈希冲突链上越来越长时,想获取冲突链上数据的时间也就越来越长,这就是前面提到的当数据量越来越大时,获取数据就越来越慢的原因。
这样致命的问题,Redis当然不会允许他的出现,接下来就要说解决哈希冲突链的方法“rehash”,rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。那具体怎么做呢?
其实,为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;释放哈希表 1 的空间。到此,我们就可以从哈希表 1 切换到哈希表 2,用增大的哈希表 2 保存更多数据,而原来的哈希表 1 留作下一次 rehash 扩容备用。
这个过程看似简单,但是第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求。此时,Redis 就无法快速访问数据了。为了避免这个问题,Redis 采用了渐进式 rehash。如下图所示
简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。
三、总结
本文中主要从redis找数据的方面讲了redis为什么他这么快,一是:因为数据存储在内存中,所以读取速度快,二是:采用了全局哈希表存储键值对,并采用了rehash的方式解决导致查询慢的哈希冲突问题。讲到这里我们已经知道Redis可以快速找到数据,那么找到数据了,还需要操作数据,那么Redis是怎么快速对数据进行操作的呢?请听下回分解。