尊嘟假嘟~常见 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 不会同时过期而造成缓存雪崩。

相关推荐
2401_8576100312 分钟前
Spring Boot框架:电商系统的技术优势
java·spring boot·后端
杨哥带你写代码2 小时前
网上商城系统:Spring Boot框架的实现
java·spring boot·后端
camellias_2 小时前
SpringBoot(二十一)SpringBoot自定义CURL请求类
java·spring boot·后端
背水3 小时前
初识Spring
java·后端·spring
晴天飛 雪3 小时前
Spring Boot MySQL 分库分表
spring boot·后端·mysql
weixin_537590453 小时前
《Spring boot从入门到实战》第七章习题答案
数据库·spring boot·后端
AskHarries3 小时前
Spring Cloud Gateway快速入门Demo
java·后端·spring cloud
Qi妙代码4 小时前
MyBatisPlus(Spring Boot版)的基本使用
java·spring boot·后端
宇宙超级勇猛无敌暴龙战神4 小时前
Springboot整合xxl-job
java·spring boot·后端·xxl-job·定时任务