Redis数据库——内存预分配

大家好,这里是编程Cookbook。本文详细介绍Redis的内存预分配策略,在使用各数据结构类型时,内存是如何变化的,以及触发底层数据结构变化的条件。
Redis 的内存预分配策略是一种优化手段,用来 减少频繁的内存分配和释放操作对性能的影响
。通过预先分配足够的内存,Redis 可以提高操作效率(在很多编程语言,如Go的切片,中都有实现),尤其是在高并发场景下。
什么是内存预分配?
- 定义:
Redis 在某些数据结构(如字符串、列表、哈希等)存储数据时,不是每次都按照精确的内存需求分配,而是会额外预留一部分内存空间。 - 目的:
- 减少频繁的内存分配系统调用。
- 提高数据结构扩展时的性能。
- 降低内存碎片化的风险。
各数据结构的实现
Redis 的多个数据结构会使用内存预分配策略,包括:
String(字符串)
- 背景:Redis 的字符串类型基于 SDS 实现,支持动态扩容。
- 触发条件:
- 当字符串长度超过当前分配的内存容量。
- 扩容策略:
- 短字符串(小于 1MB):
- 当 SDS 扩容时,新内存大小 = 当前长度的两倍。
- 例如:原字符串长度为 10 字节,扩容后长度为 20 字节。
- 长字符串(大于等于 1MB):
- 每次扩容固定增加 1MB。
- 例如:原字符串长度为 2MB,扩容后长度为 3MB。
- 短字符串(小于 1MB):
- 好处:
- 对于短字符串,快速满足增长需求。
- 对于长字符串,减少频繁的大量扩容。
- 释放策略:
- 当字符串缩短时,预分配的空间不会立刻释放,但可以复用。
Hash(哈希)
- 背景:Redis 的哈希类型使用 ziplist 或 hashtable 存储。
- 触发条件:
- ziplist:键值对数量或单个键值长度超过限制。
- hashtable:负载因子(填充率)超过阈值。
- 扩容策略:
- ziplist:
- ziplist 是一个固定大小的数据结构。它并不像 hashtable 那样支持动态扩容,而是使用固定的内存块来存储数据。它依赖于紧凑存储,如果遇到空间不足,它会通过切换为 hashtable 来处理大量数据的需求。
- 切换条件:Redis 在以下情况下,自动将 Hash 的底层存储从ziplist 切换为 hashtable:
- 字段数量超出阈值:
- 默认
hash-max-ziplist-entries=512
,即当字段数量超过 512 时,切换为哈希表。
- 默认
- 单个字段或值的大小超出阈值:
- 默认
hash-max-ziplist-value=64
,即当字段或值的大小超过 64 字节时,切换为哈希表。
- 默认
- 字段数量超出阈值:
- hashtable:
- 负载因子(填充率)超过阈值(默认是1.0),会按照下面进行扩容:
- 按两倍扩展大小。
- 使用渐进式 rehash,分批次迁移数据,降低性能抖动。
- 当哈希表中的负载因子降到 小于 0.1 时(表空间使用率过低),Redis 会触发缩容:
- 将哈希表容量调整为当前元素数量的两倍(创建一个新的更小的哈希表。将旧表中的数据逐步迁移到新表),以保证负载因子接近 0.5。
- 使用渐进式 rehash,分批次迁移数据,降低性能抖动。
- ziplist:
- 好处:
- 避免小型哈希频繁扩容问题。
- 哈希表扩展为两倍,保证插入操作性能。
List(列表)
-
背景:Redis 的列表类型在底层使用 quicklist(快速列表)存储,每个节点是一个 ziplist(压缩列表)。
- quicklist 是一个双向链表,每个节点存储一个 ziplist。
- ziplist 是一个连续内存区域,存储多个列表元素。
- 分层机制:
- quicklist 提供灵活的节点管理能力。
- ziplist 提供紧凑的内存存储,节省空间。
-
触发条件:
-
ziplist:
- 当新增元素导致 ziplist 的容量不足,超出其当前内存空间限制时触发扩容。
- 如果 ziplist 达到 配置的最大容量限制(由
list-max-ziplist-size
控制),则 quicklist 会拆分出一个新的节点。
-
quicklist:
- 当 ziplist 达到容量限制,或者 quicklist 的设计策略要求分离数据时,触发 quicklist 新建节点操作。
-
-
扩容策略:
-
ziplist:
- 扩容是 成比例 的,通常为 当前容量的两倍,以减少未来的扩容频率。
- ziplist 的内存空间是连续的,因此扩容需要分配一段新的连续内存,并将数据迁移到新区域。
-
quicklist:
- 在 新建节点 时,会根据需求适当预留更多容量,用于存储未来插入的数据。
- 这种设计避免了频繁创建新节点或调整链表结构的开销。
-
-
好处:
- 快速插入和追加,减少扩容次数。
- 降低链表操作带来的内存分配开销。
Set(集合)
-
背景:Redis 的集合类型使用 intset(整数集合)或 hashtable 存储。
-
触发条件:
-
intset扩容触发条件:
- 插入元素类型超出当前范围:
- intset 使用紧凑的存储方式,只支持
int16
、int32
或int64
类型整数。如从int16
扩展为int32
。
- intset 使用紧凑的存储方式,只支持
- 插入非整数值或集合大小超过阈值(默认 512 个):
- 默认情况下,当集合中的元素数量或类型复杂度超出 intset 的范围时,Redis 会将集合从 intset 转换为 hashtable。
- 插入元素类型超出当前范围:
-
hashtable扩容触发条件:负载因子(填充率)超过阈值。
-
-
扩容策略:
- intset:当插入的整数类型需要更大的存储空间时,intset 会升级存储类型,例如从 int16 升级到 int32,或者从 int32 升级到 int64。
- hashtable:按两倍容量扩展。(同上面的hash中的扩容策略)
-
好处:
- 减少扩容性能损耗。
- 在集合规模增长时动态调整存储结构。
Sorted Set(有序集合)
-
背景:Redis 的有序集合使用 ziplist 或 zskiplist + dict/hashtable(跳跃表 + 字典/哈希表)存储。
-
触发条件:
- ziplist:
-
ziplist 的扩容触发条件
- 新增元素导致内存空间不足:当有序集合使用 ziplist 存储且现有内存空间不足以容纳新元素时,会触发动态扩容。
-
ziplist 转换为 zskiplist + hashtable 的触发条件
- 集合元素数量超出阈值:默认情况下,如果有序集合中的元素数量超过 128 个(由配置项
zset-max-ziplist-entries
控制),Redis 会将底层存储从 ziplist 切换为 zskiplist + hashtable。 - 单个元素长度超出阈值:默认情况下,如果有序集合中的任意元素长度超过 64 字节(由配置项
zset-max-ziplist-value
控制),也会触发结构转换。
- 集合元素数量超出阈值:默认情况下,如果有序集合中的元素数量超过 128 个(由配置项
- zskiplist + hashtable:
- 增加元素需要优化查询效率:跳跃表通过增加索引层数来优化查询性能。当插入大量元素或元素分布范围广泛时,可能触发增加索引层数以维持平均 O(log N) 的查询效率。
- 跳跃表(zskiplist):按分值排序,支持高效范围查询(如
ZRANGE
)。 - 哈希表(hashtable):存储
member → score
的映射,支持 O(1) 复杂度查询单个元素分值(如ZSCORE
)。
- 跳跃表(zskiplist):按分值排序,支持高效范围查询(如
-
扩容策略:
-
ziplist 的扩容策略
- ziplist 使用动态扩容方式管理内存空间,具体规则如下:
- 比例扩容:
- 当需要存储更多元素时,ziplist 会根据当前大小按一定比例扩展内存(通常为 1.5 倍至 2 倍)。
- 追加扩容:
- 如果当前 ziplist 存储的内容接近容量上限,Redis 会直接为新增数据分配所需空间,并适当预留一部分。
- 比例扩容:
- ziplist 使用动态扩容方式管理内存空间,具体规则如下:
-
ziplist 转换为 zskiplist + hashtable 的扩容策略
- 转换过程:
- 创建一个新的 zskiplist 数据结构。
- 将 ziplist 中的数据逐个迁移到 zskiplist。
- 在转换完成后,ziplist 被释放,后续操作都在 zskiplist 上执行。
- 转换过程:
-
zskiplist + hashtable 的扩容策略
-
zskiplist 的核心是通过 动态调整索引层数 来维持性能,扩容策略包括:
- 增加索引层数:
- 跳跃表的索引层数是基于随机概率的,每次插入一个新节点时,有一定概率为该节点增加额外的索引层。
- 索引层数越高,跳跃表的查询效率越高,但内存占用也会增加。
- 分布优化:
- 在跳跃表中,索引层的分布会随着元素数量的增加进行调整,确保查询效率。
- 增加索引层数:
-
hashtable 的核心是通过 存储 member → score 的映射,支持 O(1) 复杂度查询单个元素分值(如 ZSCORE)。
-
-
-
好处:
- 优化小集合存储空间。
- 提升大规模有序集合的插入性能。
内存预分配的优缺点
优点
- 减少系统调用:
- 批量分配内存,降低内存分配和释放的频率。
- 提高性能:
- 数据扩展时无需频繁重新分配内存。
- 避免碎片:
- 内存分配更加连续,减少碎片化。
缺点
- 可能导致内存浪费:
- 预分配的内存如果未使用,可能造成一定的浪费。
- 释放延迟:
- 预分配的内存不会及时回收,仅在更大范围的操作中释放。
总结
Redis 的内存预分配策略结合动态扩容机制,有效提高了性能并降低内存分配开销。预分配策略的设计在多种数据结构中得到了应用,同时通过合理的参数调整,可以进一步优化 Redis 的内存利用效率。