十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Redis 渐进式 Rehash 详解:如何避免哈希表扩容时的阻塞

Redis 渐进式 Rehash 详解:如何避免哈希表扩容时的阻塞 一、Redis 哈希表基础与 Rehash 必要性1.1 Redis 哈希表数据结构解析Redis 使用字典哈希表作为核心数据结构实现快速的数据存取。Redis 中的哈希表由 dict 结构表示包含两个哈希表ht[0] 和 ht[1]以及一些元信息如 rehashidx、size、size_mask、used 等。typedef struct dict { dictType *type; // 类型特定函数 void *privdata; // 私有数据 dictht ht[2]; // 两个哈希表 long rehashidx; // rehash 索引 int iterators; // 迭代器数量 } dict;哈希表的每个节点是一个 dictEntry 结构typedef struct dictEntry { void *key; // 键 union { void *val; // 值 uint64_t u64; int64_t s64; double d; } v; struct dictEntry *next; // 链表指针 } dictEntry;哈希表解决冲突使用链地址法当哈希冲突较多时多个键值对会形成链表。1.2 哈希表扩容触发条件分析Redis 的哈希表扩容条件主要考虑两个因素负载因子和服务器状态。负载因子load_factor used/size其中 used 是哈希表中元素个数size 是哈希表大小。触发扩容的条件服务器没有执行 BGSAVE 或 BGREWRITEAOF 命令且哈希表的负载因子大于 1服务器正在执行 BGSAVE 或 BGREWRITEAOF 命令且哈希表的负载因子大于 5负载因子阈值不同的原因是在执行 BGSAVE 或 BGREWRITEAOF 时Redis 会创建子进程消耗大量内存此时为了防止内存爆炸式增长提高扩容阈值。1.3 Rehash 对 Redis 性能的影响当哈希表需要扩容时Rehash 操作会将旧哈希表中的所有键值对重新计算哈希值并迁移到新的哈希表中。这个操作如果一次性完成会消耗大量 CPU 资源导致 Redis 阻塞影响其他请求的处理。一次性 Rehash 的缺点阻塞时间与哈希表大小成正比在大数据量情况下可能导致明显的服务延迟无法满足高并发场景的服务质量要求Redis 通过渐进式 Rehash 机制解决这个问题将 Rehash 操作分散到多次操作中避免长时间阻塞。二、Redis 渐进式 Rehash 机制详解2.1 渐进式 Rehash 与一次性 Rehash 的对比一次性 Rehash一次性完成所有键值对的迁移操作简单但会造成长时间阻塞适用于数据量小且对延迟不敏感的场景渐进式 Rehash将 Rehash 操作分散到多次命令执行过程中每次只迁移一部分键值对在 Rehash 期间新数据会同时写入两个哈希表适用于高并发、低延迟的场景对比表格| 特性 | 一次性 Rehash | 渐进式 Rehash ||------|--------------|---------------|| 执行时间 | 短时间集中执行 | 长时间分散执行 || 对客户端影响 | 阻塞明显 | 几乎无感知 || CPU 使用 | 短时间高 CPU | CPU 使用均匀 || 内存使用 | 需要双倍内存 | 需要双倍内存 || 适用场景 | 数据量小 | 大数据量、高并发 |2.2 渐进式 Rehash 的核心实现Redis 的渐进式 Rehash 核心是通过dictRehash函数实现的每次只迁移少量节点通常是 1 个这样可以在不影响正常服务的情况下逐步完成 Rehash。int dictRehash(dict *d, int n) { int empty_visits n * 10; /* 最大访问次数限制 */ if (d-iterators 0) return 0; while (n-- d-ht[0].used ! 0) { dictEntry *de, *nextde; // 跳过空桶 void *table d-ht[0].table; while (d-rehashidx d-ht[0].size table[d-rehashidx] NULL) d-rehashidx; // 检查是否完成 Rehash if (d-rehashidx d-ht[0].size) { d-ht[0] d-ht[1]; _dictReset(d-ht[1]); d-rehashidx -1; return 1; } // 迁移桶中的所有节点 de table[d-rehashidx]; while (de ! NULL) { nextde de-next; uint64_t h; // 计算在新哈希表中的位置 h dictHashKey(d, de-key) d-ht[1].size_mask; de-next d-ht[1].table[h]; d-ht[1].table[h] de; d-ht[0].used--; d-ht[1].used; de nextde; } // 将旧桶置为空 table[d-rehashidx] NULL; d-rehashidx; if (d-iterators 0) break; } return 0; }2.3 rehashidx 的作用与维护Redis 使用rehashidx字段记录渐进式 Rehash 的进度初始值为 -1表示未开始 Rehash开始 Rehash 时设置为 0表示从 ht[0] 的第 0 个桶开始迁移每迁移一个桶后rehashidx 加 1当 rehashidx 等于 ht[0].size 时表示完成所有桶的迁移在渐进式 Rehash 期间Redis 会处理数据修改操作的特殊逻辑对于删除操作同时在两个哈希表中执行删除对于添加操作新键值对直接添加到 ht[1] 中对于查找操作先在 ht[0] 中查找若未找到则到 ht[1] 中查找三、渐进式 Rehash 的执行流程3.1 Rehash 的启动条件Redis 在执行以下操作时会检查是否需要执行 Rehash添加操作dictAdd、dictReplace查找操作dictFind删除操作dictDelete随机获取键值对dictGetRandomKey检查条件包括没有在执行 Rehashrehashidx -1负载因子达到阈值如前所述没有正在执行的迭代器避免迭代期间修改数据结构3.2 渐进式 Rehash 的具体步骤渐进式 Rehash 的执行过程可以用以下流程图表示是否是否是否开始 Rehash设置 rehashidx 0数据操作检查是否在Rehash中?处理数据操作本次操作中执行部分Rehash是否需要开始Rehash?正常处理操作Rehash完成?释放ht[0]具体步骤说明启动 Rehash创建新的哈希表 ht[1]大小为 ht[0] 的两倍设置 rehashidx 0表示开始迁移数据操作处理对于每个数据操作增删改查先在 ht[0] 中处理如果已开始 Rehash则额外执行少量 Rehash 工作执行部分 Rehash每次数据操作后执行少量工作迁移部分数据默认每次迁移一个桶中的所有键值对更新 rehashidx 指向下一个桶完成 Rehash当 rehashidx 达到 ht[0].size 时表示所有桶已迁移释放 ht[0]将 ht[1] 重命名为 ht[0]重置 rehashidx -1结束 Rehash3.3 Rehash 完成的标志Rehash 完成的标志是rehashidx -1ht[0].used 0ht[0] 和 ht[1] 指针互换在 Redis 中Rehash 完成后原先的 ht[1] 变为新的 ht[0]原先的 ht[0] 被释放所有新添加的键值对都直接进入新的哈希表系统恢复正常状态四、Redis 中渐进式 Rehash 的实践应用4.1 增删查操作中的 Rehash 处理增加操作中的 Rehash 处理当执行增加操作如 HSET时如果未开始 Rehash先检查是否需要触发 Rehash如果已经开始 Rehash新键值对直接添加到 ht[1] 中适当执行少量 Rehash 工作int dictAdd(dict *d, void *key, void *val) { int index; dictEntry *entry; // 如果需要Rehash先执行部分工作 if (dictIsRehashing(d)) _dictRehashStep(d); // 计算位置 if (dictIsRehashing(d)) index _dictKeyIndex(d, key, entry); else index _dictKeyIndex(d, key, entry); // 处理键已存在的情况 if (entry) return DICT_ERR; // 创建新节点 entry dictCreateEntry(d, key); if (!entry) return DICT_ERR; // 添加到哈希表 if (dictIsRehashing(d)) d-ht[1].table[index] entry; else d-ht[0].table[index] entry; // 更新计数 d-ht[dictIsRehashing(d) ? 1 : 0].used; return DICT_OK; }删除操作中的 Rehash 处理当执行删除操作如 HDEL时如果已开始 Rehash需要在两个哈希表中都查找并删除如果删除的是 ht[0] 中的元素需适当执行 Rehash 工作查找操作中的 Rehash 处理当执行查找操作如 HGET时如果已开始 Rehash先在 ht[0] 中查找如果未找到再在 ht[1] 中查找适当执行少量 Rehash 工作4.2 定时任务中的 Rehash 控制Redis 还通过定时任务加速渐进式 Rehash 的执行服务器周期性任务Redis 服务器周期性执行redis.c中的serverCron函数默认每 100 毫秒执行一次在执行过程中会调用dictRehash函数执行批量 Rehash 工作批量 Rehash 机制在周期性任务中如果正在进行 Rehash会执行更多工作每次执行最多迁移 100 个桶Redis 源码中定义的值这样可以在不影响正常服务的情况下加速 Rehash 进度activerehashing 配置通过activerehashing配置参数控制是否启用加速机制默认启用在 Redis 源码中为server.activerehashing 1在内存受限的环境中可以禁用以减少 CPU 使用4.3 避免长时间 Rehash 的策略虽然渐进式 Rehash 已经大大减少了阻塞时间但在极端情况下仍可能出现问题控制单次操作耗时在高并发场景下单个命令执行时间应控制在毫秒级避免在单个命令中处理大量数据避免一次性添加大量数据批量添加数据时考虑分批执行使用 Pipeline 减少网络往返时间监控 Rehash 进度通过 INFO 命令监控哈希表状态观察redis-cli --stat输出中的内存变化情况合理配置 Redis 参数根据数据量预分配足够的内存设置合理的hash-max-ziplist-entries和hash-max-ziplist-value参数在数据结构设计上使用更合适的数据类型如有序集合代替哈希表在非高峰期执行大量数据操作避免在业务高峰期执行大量数据添加或修改考虑使用 Redis 的 Redis RDB 或 AOF 重写功能来减少内存碎片五、优化建议与最佳实践5.1 合理设置 Redis 配置参数hash-max-ziplist-entries控制哈希表使用压缩列表ziplist的最大元素数量默认值为 512根据实际数据特点调整避免频繁转换为字典结构hash-max-ziplist-value控制哈希表使用压缩列表的最大值大小默认值为 64 字节大于此值的字段将使用独立存储activerehashing控制是否启用 Rehash 加速机制默认为 yes在内存敏感环境中可设置为 no以减少 CPU 使用maxmemory设置 Redis 最大内存限制防止内存无限制增长配合 maxmemory-policy 使用避免内存耗尽hash-max-ziplist-entries 和 hash-max-ziplist-value对于哈希表类型合理设置这两个参数可以减少内存使用值越小内存使用越省但可能影响性能5.2 监控 Rehash 状态的方法INFO 命令使用 INFO memory 查看内存使用情况观察 used_memory 和 used_memory_peak 等指标注意 mem_fragmentation_ratio 内存碎片率STATS 命令使用 redis-cli --latency 监控延迟情况观察 redis-cli --stats 中的统计信息关注操作延迟突然上升的情况慢查询日志配置 slowlog 记录执行时间长的命令分析慢查询日志发现潜在问题设置合理的 slowlog-log-slower-than 阈值内存分析使用 MEMORY USAGE 命令分析大键使用 redis-rdb -c memory 分析 RDB 文件识别内存使用异常的键自动化监控建立监控系统收集关键指标设置告警阈值及时发现异常使用 Prometheus、Grafana 等工具进行可视化监控5.3 应对大规模 Key 时的优化方案数据结构选择对大规模数据考虑使用 Hash 结构代替 String对有序数据使用 Sorted Set 代替 List对小量数据使用 Ziplist 压缩存储分片策略使用 Redis Cluster 进行水平分片按业务特点设计合理的分片键避免热点数据导致单个分片压力大读写分离使用 Redis Sentinel 或 Cluster 实现高可用配置读写分离减轻主节点压力使用复制延迟监控避免数据不一致批量操作使用 Pipeline 减少网络往返时间使用 Lua 脚本保证原子性同时减少网络开销避免单个处理大量数据的长时间操作内存管理合理使用过期策略如 volatile-lru对不再使用的数据及时删除使用 Scan 命令替代 Keys 命令
返回列表