Redis数据库——内存预分配

Redis数据库——内存预分配

大家好,这里是编程Cookbook。本文详细介绍Redis的内存预分配策略,在使用各数据结构类型时,内存是如何变化的,以及触发底层数据结构变化的条件。

Redis 的内存预分配策略是一种优化手段,用来 减少频繁的内存分配和释放操作对性能的影响。通过预先分配足够的内存,Redis 可以提高操作效率(在很多编程语言,如Go的切片,中都有实现),尤其是在高并发场景下。


什么是内存预分配?

  • 定义
    Redis 在某些数据结构(如字符串、列表、哈希等)存储数据时,不是每次都按照精确的内存需求分配,而是会额外预留一部分内存空间
  • 目的
    • 减少频繁的内存分配系统调用。
    • 提高数据结构扩展时的性能。
    • 降低内存碎片化的风险。

各数据结构的实现

Redis 的多个数据结构会使用内存预分配策略,包括:

String(字符串)

  • 背景:Redis 的字符串类型基于 SDS 实现,支持动态扩容。
  • 触发条件
    • 当字符串长度超过当前分配的内存容量。
  • 扩容策略
    1. 短字符串(小于 1MB)
      • 当 SDS 扩容时,新内存大小 = 当前长度的两倍
      • 例如:原字符串长度为 10 字节,扩容后长度为 20 字节。
    2. 长字符串(大于等于 1MB)
      • 每次扩容固定增加 1MB
      • 例如:原字符串长度为 2MB,扩容后长度为 3MB。
  • 好处
    • 对于短字符串,快速满足增长需求。
    • 对于长字符串,减少频繁的大量扩容。
  • 释放策略
    • 当字符串缩短时,预分配的空间不会立刻释放,但可以复用。

Hash(哈希)

  • 背景:Redis 的哈希类型使用 ziplisthashtable 存储。
  • 触发条件
    1. ziplist:键值对数量或单个键值长度超过限制。
    2. hashtable:负载因子(填充率)超过阈值。
  • 扩容策略
    1. ziplist
      • ziplist 是一个固定大小的数据结构。它并不像 hashtable 那样支持动态扩容,而是使用固定的内存块来存储数据。它依赖于紧凑存储,如果遇到空间不足,它会通过切换为 hashtable 来处理大量数据的需求。
      • 切换条件:Redis 在以下情况下,自动将 Hash 的底层存储从ziplist 切换为 hashtable
        1. 字段数量超出阈值
          • 默认 hash-max-ziplist-entries=512,即当字段数量超过 512 时,切换为哈希表。
        2. 单个字段或值的大小超出阈值
          • 默认 hash-max-ziplist-value=64,即当字段或值的大小超过 64 字节时,切换为哈希表。
    2. hashtable
    • 负载因子(填充率)超过阈值(默认是1.0),会按照下面进行扩容
      • 两倍扩展大小
      • 使用渐进式 rehash,分批次迁移数据,降低性能抖动。
    • 当哈希表中的负载因子降到 小于 0.1 时(表空间使用率过低),Redis 会触发缩容
      • 将哈希表容量调整为当前元素数量的两倍(创建一个新的更小的哈希表。将旧表中的数据逐步迁移到新表),以保证负载因子接近 0.5。
      • 使用渐进式 rehash,分批次迁移数据,降低性能抖动。
  • 好处
    • 避免小型哈希频繁扩容问题。
    • 哈希表扩展为两倍,保证插入操作性能。

List(列表)

  • 背景:Redis 的列表类型在底层使用 quicklist(快速列表)存储,每个节点是一个 ziplist(压缩列表)。

    1. quicklist 是一个双向链表,每个节点存储一个 ziplist
    2. ziplist 是一个连续内存区域,存储多个列表元素。
    3. 分层机制
      • quicklist 提供灵活的节点管理能力。
      • ziplist 提供紧凑的内存存储,节省空间。
  • 触发条件

    1. ziplist

      • 当新增元素导致 ziplist 的容量不足,超出其当前内存空间限制时触发扩容。
      • 如果 ziplist 达到 配置的最大容量限制(由 list-max-ziplist-size 控制),则 quicklist 会拆分出一个新的节点。
    2. quicklist

      • ziplist 达到容量限制,或者 quicklist 的设计策略要求分离数据时,触发 quicklist 新建节点操作。
  • 扩容策略

    1. ziplist

      • 扩容是 成比例 的,通常为 当前容量的两倍,以减少未来的扩容频率。
      • ziplist 的内存空间是连续的,因此扩容需要分配一段新的连续内存,并将数据迁移到新区域。
    2. quicklist

      • 新建节点 时,会根据需求适当预留更多容量,用于存储未来插入的数据。
      • 这种设计避免了频繁创建新节点或调整链表结构的开销。
  • 好处

    • 快速插入和追加,减少扩容次数。
    • 降低链表操作带来的内存分配开销。

Set(集合)

  • 背景:Redis 的集合类型使用 intset(整数集合)或 hashtable 存储。

  • 触发条件

    1. intset扩容触发条件:

      1. 插入元素类型超出当前范围
        • intset 使用紧凑的存储方式,只支持 int16int32int64 类型整数。如从 int16 扩展为 int32
      2. 插入非整数值或集合大小超过阈值(默认 512 个)
        • 默认情况下,当集合中的元素数量或类型复杂度超出 intset 的范围时,Redis 会将集合从 intset 转换为 hashtable
    2. hashtable扩容触发条件:负载因子(填充率)超过阈值。

  • 扩容策略

    1. intset:当插入的整数类型需要更大的存储空间时,intset 会升级存储类型,例如从 int16 升级到 int32,或者从 int32 升级到 int64。
    2. hashtable:按两倍容量扩展。(同上面的hash中的扩容策略
  • 好处

    • 减少扩容性能损耗。
    • 在集合规模增长时动态调整存储结构。

Sorted Set(有序集合)

  • 背景:Redis 的有序集合使用 ziplistzskiplist + dict/hashtable(跳跃表 + 字典/哈希表)存储。

  • 触发条件

    1. ziplist
    • ziplist 的扩容触发条件

      • 新增元素导致内存空间不足:当有序集合使用 ziplist 存储且现有内存空间不足以容纳新元素时,会触发动态扩容。
    • ziplist 转换为 zskiplist + hashtable 的触发条件

      • 集合元素数量超出阈值:默认情况下,如果有序集合中的元素数量超过 128 个(由配置项 zset-max-ziplist-entries 控制),Redis 会将底层存储从 ziplist 切换为 zskiplist + hashtable
      • 单个元素长度超出阈值:默认情况下,如果有序集合中的任意元素长度超过 64 字节(由配置项 zset-max-ziplist-value 控制),也会触发结构转换。
    1. zskiplist + hashtable
    • 增加元素需要优化查询效率:跳跃表通过增加索引层数来优化查询性能。当插入大量元素或元素分布范围广泛时,可能触发增加索引层数以维持平均 O(log N) 的查询效率。
      • 跳跃表(zskiplist):按分值排序,支持高效范围查询(如 ZRANGE)。
      • 哈希表(hashtable):存储 member → score 的映射,支持 O(1) 复杂度查询单个元素分值(如 ZSCORE)。
  • 扩容策略

    1. ziplist 的扩容策略

      • ziplist 使用动态扩容方式管理内存空间,具体规则如下:
        • 比例扩容
          • 当需要存储更多元素时,ziplist 会根据当前大小按一定比例扩展内存(通常为 1.5 倍至 2 倍)。
        • 追加扩容
          • 如果当前 ziplist 存储的内容接近容量上限,Redis 会直接为新增数据分配所需空间,并适当预留一部分。
    2. ziplist 转换为 zskiplist + hashtable 的扩容策略

      • 转换过程
        1. 创建一个新的 zskiplist 数据结构。
        2. 将 ziplist 中的数据逐个迁移到 zskiplist。
        3. 在转换完成后,ziplist 被释放,后续操作都在 zskiplist 上执行。
    3. zskiplist + hashtable 的扩容策略

      • zskiplist 的核心是通过 动态调整索引层数 来维持性能,扩容策略包括:

        1. 增加索引层数
          • 跳跃表的索引层数是基于随机概率的,每次插入一个新节点时,有一定概率为该节点增加额外的索引层。
          • 索引层数越高,跳跃表的查询效率越高,但内存占用也会增加。
        2. 分布优化
          • 在跳跃表中,索引层的分布会随着元素数量的增加进行调整,确保查询效率。
      • hashtable 的核心是通过 存储 member → score 的映射,支持 O(1) 复杂度查询单个元素分值(如 ZSCORE)。

  • 好处

    • 优化小集合存储空间。
    • 提升大规模有序集合的插入性能。

内存预分配的优缺点

优点

  1. 减少系统调用
    • 批量分配内存,降低内存分配和释放的频率。
  2. 提高性能
    • 数据扩展时无需频繁重新分配内存。
  3. 避免碎片
    • 内存分配更加连续,减少碎片化。

缺点

  1. 可能导致内存浪费
    • 预分配的内存如果未使用,可能造成一定的浪费。
  2. 释放延迟
    • 预分配的内存不会及时回收,仅在更大范围的操作中释放。

总结

Redis 的内存预分配策略结合动态扩容机制,有效提高了性能并降低内存分配开销。预分配策略的设计在多种数据结构中得到了应用,同时通过合理的参数调整,可以进一步优化 Redis 的内存利用效率。