小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
Redis 是当下最流行的内存键值数据库,与只能存储 String 的 memcache 相比,虽然它支持丰富的数据结构,但是从全局来看,它保存的依然是若干个 key-value 组合。
Redis 在全局使用一个哈希表来管理所有的键值对,这样的好处是可以用 O(1) 的时间复杂度完成键值对的查找,结合 Redis 所有操作都在内存上完成这一特性,使得 Redis 对数据的操作非常高效。
全局哈希表如何组织数据
我们可以把 Redis 管理所有数据的哈希表称为全局哈希表。一个哈希表中存储数据的其实就是一个数组,数组中的每个元素是一个哈希桶(bucket)。每个键值对通过哈希运算,存放在对应的桶中。理想的状态是,桶中存放一个键值对,或者没有存放键值对。这样,当需要查找一个键值对的时候,只要经过一次哈希运算,就能找到对应的位置,读取到键值对。
随着数据的增加,查找数据会变慢吗?
当 key 不同的两个键值对的哈希运算的到同一个值的时候,它们就需要被保存在同一个桶当中,这种情况被称为「哈希冲突」。如果一个桶中保存了多个键值对,它们会形成一个链表,依次用指针连接。当需要查找键值对的时候,如果定位到的桶包含多个键值对,那么,就需要在链表上依次查找,这种情况下,查找的速度会大大降低。
哈希表这种数据结构处理这种问题的通用方式,是对数组进行扩容,然后对其中所有的元素进行 rehash,也就是重新计算每个元素的哈希,然后调整它们在数组中的位置,使它们分布得更分散,以减少单个桶中元素的数量。
在 Redis 中,默认情况下有两个全局哈希表,我们称为「表A」和「表B」,默认情况下,Redis 在表A中存储数据,随着数据量的不断增加,Redis 开始进行 rehash。
首先,给表B分配内存空间,因为要对数据进行扩容。这个空间需要比表A的内存空间大。然后把表A中的数据重新进行哈希运算并拷贝到表B中对应的位置,最后把表A的空间释放。这样就完成了整个扩容的过程。
渐进式的 rehash 如何完成
当需要进行 rehash 的时候,意味着 Redis 中已经储存了大量的数据,而对大量的数据进行 rehash 会造成 Redis 线程的阻塞。作为一个以数据操作效率著称的数据库,线程阻塞对 Redis 效率的影响是很大的。因此,Redis 采用了渐进式 rehash 的方式。
简而言之,渐进式 rehash 就是,所有数据的 rehash 并不是一次完成的,而是在从表A到表B拷贝数据的过程中,Redis 仍然在处理客户端的数据操作请求。每处理一个请求,Redis 就从表A中读取一个桶的的所有键值对拷贝到表B中,直到表A中的所有数据都被处理完。这样就把单次执行的巨大开销,分摊到了每次请求的过程中处理,保证了 rehash 的过程中,依然可以处理数据操作。
最后还有一个问题:如果 Redis 处于空闲状态,没有收到任何数据操作请求,是不是 rehash 操作也就被暂停了?(实际上这个时候更适合去做 rehash 的工作)
其实并不会。Redis 在触发 rehash 之后,即使没有收到新的数据操作请求,也会按一定频率在不影响其他任务的情况下执行一次 rehash 操作。