Redis的字典渐进式扩容与ConcurrentHashMap的扩容策略比较

本文对比Redis字典与ConcurrentHashMap的扩容策略。Redis采用单线程渐进式rehash,允许并发读写,适合对写和删除操作要求高的场景;ConcurrentHashMap采用多线程协同式rehash,扩容速度快,适用于内存资源紧张的情况。

本文介绍Redis的字典(是种Map)与ConcurrentHashMap的扩容策略,并比较它们的优缺点。

(不讨论它们的实现细节)

dict是Redis的hash数据结构,所有类型的元素都可以依据key值计算hashkey,然后将元素插入到dict的某个hash链上(采用拉链法解决hash冲突)。其中,dict的中的hashtable(dictht)的扩容是dict很重要的部分。Redis的“管家”函数serverCron会依据一定的算法(dict中的used与size的比值)判定是否开始进行hashtable的扩容。dict中的ht[1]是作为扩容的临时数据,扩容之后,hashtalbe的长度将变长,那么hashtalbe的sizemask(哈希表大小掩码,用于计算索引值,总是到等于size-1)与原来的sizemask就不同了,那么计算出的hashkey也将不同。所以就需要Rehash对ht[0]中的元素重新计算hashkey。

        在Rehash阶段,首先将原来的ht[0]中的元素重新rehash到ht[1]中,故而要耗费大量的力气从新计算原来的ht[0]表中元素的在ht[1]表总的hashkey,并将元素转移到ht[1]的表中。由于这样Rehash会耗费大量的系统资源,如果一次性完成一个dict的Rehash工作,那么将会对系统中的其他任务造成延迟? 

首先Redis的字典采用的是一种‘’单线程渐进式rehash‘’,这里的单线程是指只有一个线程在扩容,

而在扩容的同时其他的线程可以并发的进行读写。

Redis系统后台会定时给予扩容的那个线程足够的运行时间,这样不会导致它饿死。

大致过程是这样的:

ht[0],是存放数据的table,作为非扩容时容器。

ht[1],只有正在进行扩容时才会使用,它也是存放数据的table,长度为ht[0]的两倍。

扩容时,单线程A负责把数据从ht[0] copy到ht[1] 中。如果这时有其他线程

进行读操作:会先去ht[0]中找,找不到再去ht[1]中找

进行写操作:直接写在ht[1]中

进行删除操作:与读类似。

当然这过程中会设计到一系列的锁来保证同步性,不过这并不是本文的重点。

而ConcurrentHashMap采用的扩容策略为: “多线程协同式rehash“。

这里的多线程指的是,有多个线程并发的把数据从旧的容器搬运到新的容器中。

扩容时大致过程如下:

线程A在扩容把数据从oldTable搬到到newTable,这时其他线程

进行get操作:这个线程知道数据存放在oldTable或是newTable中,直接取即可。

进行写操作:如果要写的桶位,已经被线程A搬运到了newTable。

那么这个线程知道正在扩容,它也一起帮着扩容,扩容完成后才进行put操作

进行删除操作:与写一致。


两者对比:

1.扩容所花费的时间对比:

一个单线程渐进扩容,一个多线程协同扩容。在平均的情况下,是ConcurrentHashMap快。这也意味着,扩容时所需要

花费的空间能够更快的进行释放。

2.读操作,两者的性能相差不多。

3.写操作,Redis的字典返回更快些,因为它不像ConcurrentHashMap那样去帮着扩容(当要写的桶位已经搬到了newTable时),等扩容完才能进行操作。

4.删除操作,与写一样。

所以是选择单线程渐进式扩容还是选择多线程协同式扩容,这个就具体问题具体分析了。

1.如果内存资源吃紧,希望能够进行快速的扩容方便释放扩容时需要的辅助空间,且那么选择后者。

2.如果对于写和删除操作要求迅速,那么可以选择前者。

个人觉得ConcurrentHashMap的扩容策略更让人惊艳,效果也不错。

### Redis中的扩容缩容机制 Redis 中的扩容和缩容机制主要依赖于其字典(Dictionary)的实现。字典Redis 的核心数据结构之一,广泛应用于数据库键值对存储、哈希表等场景。为了支持高效的数据管理,Redis字典采用了两个哈希表(`ht[0]` 和 `ht[1]`)的设计,并通过 **渐进式 rehash**(Incremental Rehash)机制来实现扩容和缩容操作。 在正常情况下,字典仅使用 `ht[0]` 哈希表来存储数据。当需要扩容或缩容时,系统会创建一个新的哈希表 `ht[1]`,并逐步将数据从 `ht[0]` 迁移到 `ht[1]`。这种迁移过程是渐进式的,而不是一次性完成的,以避免对性能造成显著影响[^1]。 #### 扩容机制 Redis 字典扩容基于负载因子(Load Factor)的判断。当 `ht[0]` 的负载因子超过一定阈值时,Redis 会触发扩容操作。具体来说,扩容时会创建一个更大的哈希表 `ht[1]`,并开始将数据逐步迁移到新的哈希表中。每次对字典进行操作(如 SET、GET、DEL 等)时,Redis 会检查是否正在进行 rehash,如果存在未完成的迁移任务,则会迁移一部分数据[^2]。 #### 缩容机制 缩容操作的触发条件扩容类似,但负载因子的阈值更低。当 `ht[0]` 的负载因子低于某个阈值时,Redis 会触发缩容操作,以减少内存占用。扩容类似,缩容也是通过渐进式 rehash 完成的,逐步将数据从 `ht[0]` 迁移到 `ht[1]`,最终用 `ht[1]` 替换 `ht[0]`,完成缩容[^3]。 ### 渐进式 Rehash 的技术原理 渐进式 rehash 是 Redis 为避免一次性 rehash 导致性能抖动而设计的一种机制。由于 Redis 是单线程模型,一次性 rehash 可能会导致主线程长时间阻塞,从而影响服务的响应性能。因此,Redis 采用了渐进式 rehash,将数据迁移分散到多次操作中完成。 #### 核心原理 渐进式 rehash 的核心在于 **逐步迁移**。在 rehash 过程中,字典同时维护两个哈希表(`ht[0]` 和 `ht[1]`),其中 `ht[0]` 是旧表,`ht[1]` 是新表。每次对字典的操作(如插入、查询、删除等)都会触发一部分数据的迁移。迁移的粒度通常是一个桶(Bucket)或一个元素[^1]。 #### 数据迁移过程 在 rehash 过程中,Redis 会维护一个索引,记录当前迁移的进度。每次操作时,Redis 会检查是否正在进行 rehash,并根据索引迁移一部分数据。迁移完成后,索引会向前推进,直到所有数据迁移完毕。这种机制确保了 rehash 过程不会对性能造成显著影响[^2]。 #### 优势 渐进式 rehash 的主要优势在于 **性能平滑迁移**。相比传统的同步 rehash,Redis渐进式 rehash 将迁移任务分散到多次操作中,避免了主线程的长时间阻塞。此外,这种机制还支持动态调整哈希表大小,从而优化内存使用和性能[^3]。 ### 示例代码 以下是一个简单的示例,展示了 Redis字典扩容的基本逻辑: ```c // 触发扩容的条件 if (dictIsRehashing(d) == 0) { if (d->ht[0].used == 0 || (d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) { // 创建新的哈希表 ht[1] if (dictExpand(d, d->ht[0].used*2) == DICT_ERR) return NULL; } } // 每次操作时检查是否需要迁移数据 int dictAddRaw(dict *d, void *key, dictEntry **existing) { // 检查是否正在进行 rehash if (d->rehashidx != -1) { _dictRehashStep(d); } // 插入数据的逻辑 ... } ``` ### 渐进式 Rehash 的应用场景 渐进式 rehash 机制广泛应用于 Redis字典操作中,特别是在大规模数据集的情况下。通过分散 rehash 任务,Redis 能够保持高性能和低延迟,同时支持动态调整哈希表大小。这种机制特别适用于高并发场景,例如缓存服务、实时数据处理等[^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值