尊嘟假嘟~常见 Redis 大key,热key解决方案

一:什么是Redis 大Key

在Redis中,"big key" 指的是一个占用较多内存空间的键。当一个键的值占用的内存超过了Redis的配置阈值时,它就被认为是一个 "big key"。大的键可以导致一些性能问题,因为它们需要更多的内存和网络带宽来存储和传输。

"big key" 可能会对Redis的性能产生负面影响,因为它们可能导致内存碎片,增加数据传输的成本,降低缓存的效率等。一些常见导致大键问题的场景包括:

  1. 字符串过大: 当字符串值的大小远远超过了Redis的配置限制时,它可能成为一个大键。
  2. Hash、Set、List等数据结构的元素过多: 当一个Hash、Set或List中的元素数量非常庞大时,也可能导致它成为一个大键。

为了避免 "big key" 问题,可以采取以下一些措施:

  1. 合理设置内存限制: 在Redis的配置中,可以设置最大使用的内存限制,通过合理设置该值,可以防止大键问题对系统造成过大的影响。
  2. 合理使用数据结构: 选择合适的数据结构来存储数据,避免在单个键中存储过多的元素。
  3. 定期清理不需要的数据: 对于不再需要的大键,可以定期清理或采取适当的缓存策略,以确保内存得到有效利用。
  4. 使用分布式缓存: 在某些情况下,将大量数据分布到多个Redis节点上,以减轻单个节点的负担。

了解和监控大键问题对于维护Redis性能是非常重要的。可以使用Redis的监控工具,如Redis的INFO命令或第三方监控工具,来跟踪内存使用情况和识别潜在的大键。

redis中有常见的几种数据结构,每种结构对大key的定义不同,比如:

  • value是String类型时,size超过10KB
  • value是ZSET、Hash、List、Set等集合类型时,它的成员数量超过1w个

上述的定义并不绝对,主要是根据value的成员数量和字节数来确定,业务可以根据自己的场景也确定标准。

二:针对于redis不同的数据结构怎么解决大key问题

2.1: String

处理大键(big key)问题对于字符串(String)数据结构可以采取以下一些方案:

  1. 数据分割: 如果字符串的值非常大,可以考虑将其分割为多个小的字符串,并使用多个键来存储这些小字符串片段。

    ruby 复制代码
    # 原始大字符串
    SET user:1:description "Very long description..."
    # 分割后的小字符串片段
    SET user:1:description:part1 "Very long"
    SET user:1:description:part2 " description..."
  2. 使用压缩算法: 对于文本型的数据,可以考虑使用Redis支持的压缩功能,如zstd压缩算法,以减小存储空间。

    ruby 复制代码
    # 使用压缩算法
    SET user:1:description "Compressed description..." ZSTD

    注意:需要在客户端应用中进行解压缩,因为Redis本身并不会自动解压缩存储的值。

  3. 使用其他数据结构: 如果字符串的值具有结构化信息,可以考虑使用其他数据结构,如Hash或List。这样可以更好地组织数据并避免一个键变得过大。

    ruby 复制代码
    # 使用Hash
    HSET user:1:info description "Very long description..."
  4. 数据清理: 定期清理不再需要的字符串数据,以确保只保留必要的信息。

    ruby 复制代码
    # 定期清理字符串数据
    DEL user:1:description:old

选择适当的方法取决于应用的具体需求和访问模式。需要根据数据的特性来选择合适的数据结构和拆分策略,以避免大键问题。

2.2: List

处理大键(big key)的问题对于列表(List)结构可以采取以下一些策略:

  1. 分割列表: 如果列表中包含的元素过多,可以考虑将列表分割成多个小的列表。每个小列表只包含部分元素,这样可以降低单个列表的长度。

    ruby 复制代码
    # 原始大列表
    LPUSH user:1:activity_log item1
    LPUSH user:1:activity_log item2
    LPUSH user:1:activity_log item3
    # 分割后的小列表
    LPUSH user:1:activity_log:part1 item1
    LPUSH user:1:activity_log:part1 item2
    LPUSH user:1:activity_log:part2 item3
  2. 定期修剪: 定期检查列表,删除掉不再需要的元素,以保持列表的合理大小。可以使用LTRIM命令来截取列表,保留指定范围内的元素。

    ruby 复制代码
    # 定期修剪列表
    LTRIM user:1:activity_log 0 99

    上述命令保留了列表中的前100个元素,删除了其余的元素。

  3. 使用其他数据结构: 如果列表中的元素有一定的关联性或结构化信息,考虑使用其他数据结构,如有序集合(Sorted Set)或哈希(Hash)。这样可以更灵活地管理数据,避免一个大列表的问题。

    ruby 复制代码
    # 使用有序集合
    ZADD user:1:activity_log timestamp1 item1
    ZADD user:1:activity_log timestamp2 item2
  4. 使用Stream数据类型: 如果数据是事件流的形式,可以考虑使用Redis Streams,它是一种支持持久化、多消费者、多生产者的数据结构,能够有效地处理事件流。

    bash 复制代码
    # 使用Stream
    XADD user:1:activity_log * type item1
    XADD user:1:activity_log * type item2

选择适当的方法取决于应用的具体需求。在设计时,需要根据数据的特性和访问模式选择合适的数据结构和拆分策略,以避免大键问题。

2.3: Set

对于集合(Set)数据结构,处理大键(big key)问题可以采取以下一些方案:

  1. 分割集合: 如果集合中元素很多,可以考虑将其分割成多个小的集合。每个小集合只包含部分元素,这样可以降低单个集合的大小。

    ruby 复制代码
    # 原始大集合
    SADD user:1:interests interest1
    SADD user:1:interests interest2
    SADD user:1:interests interest3
    # 分割后的小集合
    SADD user:1:interests:part1 interest1
    SADD user:1:interests:part2 interest2
    SADD user:1:interests:part2 interest3
  2. 使用有序集合(Sorted Set): 如果集合中的元素有一定的顺序关系,可以考虑使用有序集合来存储数据。这样可以更方便地处理一部分数据,而不是将所有元素存储在一个集合中。

    ruby 复制代码
    # 使用有序集合
    ZADD user:1:interests 1 interest1
    ZADD user:1:interests 2 interest2
    ZADD user:1:interests 3 interest3
  3. 使用HyperLogLog: 如果集合的目的是为了计算基数(元素的不重复数量),可以考虑使用HyperLogLog数据结构。它可以在极大规模的数据集合上估计基数,而且占用的内存相对较小。

    ruby 复制代码
    # 使用HyperLogLog
    PFADD user:1:interests interest1 interest2 interest3
  4. 定期清理不需要的元素: 定期检查集合,删除掉不再需要的元素,以保持集合的合理大小。可以使用SREM命令来移除指定的元素。

    ruby 复制代码
    # 定期清理集合元素
    SREM user:1:interests unwanted_interest

选择合适的方案取决于应用的具体需求。在设计时,需要根据数据的特性和访问模式选择合适的数据结构和拆分策略,以避免大键问题。

2.4: Hash

针对Hash数据结构的大键问题,以下是一些具体的方案:

  1. 字段分组: 如果一个哈希中的字段数量庞大,可以考虑将字段进行适当的分组,然后使用多个哈希键来存储这些分组。例如,将字段按照某种规则划分为多个小组,然后每个小组使用一个独立的哈希键存储。
sql 复制代码
# 原始大哈希
HSET user:1 name John
HSET user:1 email john@example.com
HSET user:1 age 30
# 分组后的哈希
HSET user:1:info name John
HSET user:1:info email john@example.com
HSET user:1:info age 30
  1. 分散数据: 将大哈希拆分为多个小的哈希,每个小哈希存储一部分字段。这样可以将数据分散到多个哈希键中,降低单个哈希键的大小。
sql 复制代码
# 原始大哈希
HSET user:1 name John
HSET user:1 email john@example.com
HSET user:1 age 30
# 分散后的哈希
HSET user:1:info name John
HSET user:1:contact email john@example.com
HSET user:1:profile age 30
  1. 使用多个哈希键: 将相关的信息存储在多个独立的哈希键中,而不是一个大哈希中。这样可以避免一个键变得过大。
ruby 复制代码
# 使用多个哈希键
HSET user:1:info name John
HSET user:1:contact email john@example.com
HSET user:1:profile age 30
  1. 定期清理不需要的字段: 定期检查哈希中的字段,清理掉不再需要的字段。这可以通过HDEL命令来实现。
ruby 复制代码
# 定期清理不需要的字段
HDEL user:1:info unwanted_field

选择合适的方案取决于具体的业务需求和数据访问模式。这些方案可以根据应用程序的情况来调整和组合,以解决大Hash键问题。

2.4: Sorted Set

对于有序集合(Sorted Set)数据结构,处理大键(big key)问题可以采取以下一些方案:

  1. 分割有序集合: 将一个大的有序集合分割成多个小的有序集合,每个小集合只包含部分元素。这样可以降低单个有序集合的大小。

    ruby 复制代码
    # 原始大有序集合
    ZADD user:1:scores 100 "Alice"
    ZADD user:1:scores 200 "Bob"
    ZADD user:1:scores 300 "Charlie"
    # 分割后的小有序集合
    ZADD user:1:scores:part1 100 "Alice"
    ZADD user:1:scores:part2 200 "Bob"
    ZADD user:1:scores:part3 300 "Charlie"
  2. 按分数范围存储: 如果有序集合的元素有一定的范围,可以将元素按照分数范围存储在不同的有序集合中,以减小每个有序集合的大小。

    ruby 复制代码
    # 按分数范围存储
    ZADD user:1:scores:0to100 100 "Alice"
    ZADD user:1:scores:101to200 200 "Bob"
    ZADD user:1:scores:201to300 300 "Charlie"
  3. 定期修剪: 定期检查有序集合,删除掉不再需要的元素,以保持有序集合的合理大小。可以使用ZREMRANGEBYRANKZREMRANGEBYSCORE命令来移除一定范围内的元素。

    ruby 复制代码
    # 定期修剪有序集合
    ZREMRANGEBYRANK user:1:scores 0 99
  4. 使用其他数据结构: 如果有序集合不再适用,可以考虑使用其他数据结构,如哈希(Hash)或字符串(String),具体取决于数据的特性和访问模式。

    ruby 复制代码
    # 使用哈希或字符串
    HSET user:1:info score 100

选择合适的方案取决于应用的具体需求。在设计时,需要根据数据的特性和访问模式选择合适的数据结构和拆分策略,以避免大键问题。

三:热key解决方案

热 Key 问题解决方案

增加 Redis 实例复本数量

对于出现热 KeyRedis 实例,我们可以通过水平扩容增加副本数量,将读请求的压力分担到不同副本节点上。

二级缓存(本地缓存)

当出现热 Key 以后,把热 Key 加载到系统的 JVM 中。后续针对这些热 Key 的请求,会直接从 JVM 中获取,而不会走到 Redis 层。这些本地缓存的工具很多,比如 Ehcache,或者 Google GuavaCache 工具,或者直接使用 HashMap 作为本地缓存工具都是可以的。

使用本地缓存需要注意两个问题:

  • 如果对热 Key 进行本地缓存,需要防止本地缓存过大,影响系统性能;
  • 需要处理本地缓存和 Redis 集群数据的一致性问题。

热 Key 备份

通过前面的分析,我们可以了解到,之所以出现热 Key,是因为有大量的对同一个 Key 的请求落到同一个 Redis 实例上,如果我们可以有办法将这些请求打散到不同的实例上,防止出现流量倾斜的情况,那么热 Key 问题也就不存在了。

那么如何将对某个热 Key 的请求打散到不同实例上呢?我们就可以通过热 Key 备份的方式,基本的思路就是,我们可以给热 Key 加上前缀或者后缀,把一个热 Key 的数量变成 Redis 实例个数 N 的倍数 M,从而由访问一个 Redis Key 变成访问 N * MRedis KeyN * MRedis Key 经过分片分布到不同的实例上,将访问量均摊到所有实例。

ini 复制代码
// N 为 Redis 实例个数,M 为 N 的 2倍
const M = N * 2
//生成随机数
random = GenRandom(0, M)
//构造备份新 Key
bakHotKey = hotKey + "_" + random
data = redis.GET(bakHotKey)
if data == NULL {
    data = redis.GET(hotKey)
    if data == NULL {
        data = GetFromDB()
        // 可以利用原子锁来写入数据保证数据一致性
        redis.SET(hotKey, data, expireTime)
        redis.SET(bakHotKey, data, expireTime + GenRandom(0, 5))
    } else {
        redis.SET(bakHotKey, data, expireTime + GenRandom(0, 5))
    }
}

在这段代码中,通过一个大于等于 1 小于 M 的随机数,得到一个 bakHotKey,程序会优先访问 bakHotKey,在得不到数据的情况下,再访问原来的 hotkey,并将 hotkey 的内容写回 bakHotKey。值得注意的是,bakHotKey 的过期时间是 hotkey 的过期时间加上一个较小的随机正整数,这是通过坡度过期的方式,保证在 hotkey 过期时,所有 bakHotKey 不会同时过期而造成缓存雪崩。

相关推荐
Penge6662 小时前
Go 接口编译期断言
后端
我是一颗柠檬2 小时前
【MySQL全面教学】MySQL面试高频考点汇总Day15(2026年)
数据库·后端·mysql·面试
拽着尾巴的鱼儿3 小时前
springboot openfeign 自定义feign 接口重试机制
java·spring boot·后端
Ceelog3 小时前
久坐党自救指南:屏幕前 8 小时,身体到底在经历什么
前端·后端
XS0301064 小时前
并发编程 六
java·后端
雪宫街道4 小时前
synchronized 锁的范围:对象锁、类锁与代码块锁
java·jvm·后端·面试
XS0301065 小时前
Spring Bean 作用域 & 生命周期
java·后端·spring
彦为君5 小时前
JavaSE-07-异常机制
java·开发语言·后端·python·spring
我是一颗柠檬6 小时前
【MySQL全面教学】MySQL性能优化实战Day13(2026年)
数据库·后端·sql·mysql·性能优化·database