一:什么是Redis 大Key
在Redis中,"big key" 指的是一个占用较多内存空间的键。当一个键的值占用的内存超过了Redis的配置阈值时,它就被认为是一个 "big key"。大的键可以导致一些性能问题,因为它们需要更多的内存和网络带宽来存储和传输。
"big key" 可能会对Redis的性能产生负面影响,因为它们可能导致内存碎片,增加数据传输的成本,降低缓存的效率等。一些常见导致大键问题的场景包括:
- 字符串过大: 当字符串值的大小远远超过了Redis的配置限制时,它可能成为一个大键。
- Hash、Set、List等数据结构的元素过多: 当一个Hash、Set或List中的元素数量非常庞大时,也可能导致它成为一个大键。
为了避免 "big key" 问题,可以采取以下一些措施:
- 合理设置内存限制: 在Redis的配置中,可以设置最大使用的内存限制,通过合理设置该值,可以防止大键问题对系统造成过大的影响。
- 合理使用数据结构: 选择合适的数据结构来存储数据,避免在单个键中存储过多的元素。
- 定期清理不需要的数据: 对于不再需要的大键,可以定期清理或采取适当的缓存策略,以确保内存得到有效利用。
- 使用分布式缓存: 在某些情况下,将大量数据分布到多个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)数据结构可以采取以下一些方案:
-
数据分割: 如果字符串的值非常大,可以考虑将其分割为多个小的字符串,并使用多个键来存储这些小字符串片段。
ruby# 原始大字符串 SET user:1:description "Very long description..." # 分割后的小字符串片段 SET user:1:description:part1 "Very long" SET user:1:description:part2 " description..."
-
使用压缩算法: 对于文本型的数据,可以考虑使用Redis支持的压缩功能,如
zstd
压缩算法,以减小存储空间。ruby# 使用压缩算法 SET user:1:description "Compressed description..." ZSTD
注意:需要在客户端应用中进行解压缩,因为Redis本身并不会自动解压缩存储的值。
-
使用其他数据结构: 如果字符串的值具有结构化信息,可以考虑使用其他数据结构,如Hash或List。这样可以更好地组织数据并避免一个键变得过大。
ruby# 使用Hash HSET user:1:info description "Very long description..."
-
数据清理: 定期清理不再需要的字符串数据,以确保只保留必要的信息。
ruby# 定期清理字符串数据 DEL user:1:description:old
选择适当的方法取决于应用的具体需求和访问模式。需要根据数据的特性来选择合适的数据结构和拆分策略,以避免大键问题。
2.2: List
处理大键(big key)的问题对于列表(List)结构可以采取以下一些策略:
-
分割列表: 如果列表中包含的元素过多,可以考虑将列表分割成多个小的列表。每个小列表只包含部分元素,这样可以降低单个列表的长度。
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
-
定期修剪: 定期检查列表,删除掉不再需要的元素,以保持列表的合理大小。可以使用
LTRIM
命令来截取列表,保留指定范围内的元素。ruby# 定期修剪列表 LTRIM user:1:activity_log 0 99
上述命令保留了列表中的前100个元素,删除了其余的元素。
-
使用其他数据结构: 如果列表中的元素有一定的关联性或结构化信息,考虑使用其他数据结构,如有序集合(Sorted Set)或哈希(Hash)。这样可以更灵活地管理数据,避免一个大列表的问题。
ruby# 使用有序集合 ZADD user:1:activity_log timestamp1 item1 ZADD user:1:activity_log timestamp2 item2
-
使用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)问题可以采取以下一些方案:
-
分割集合: 如果集合中元素很多,可以考虑将其分割成多个小的集合。每个小集合只包含部分元素,这样可以降低单个集合的大小。
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
-
使用有序集合(Sorted Set): 如果集合中的元素有一定的顺序关系,可以考虑使用有序集合来存储数据。这样可以更方便地处理一部分数据,而不是将所有元素存储在一个集合中。
ruby# 使用有序集合 ZADD user:1:interests 1 interest1 ZADD user:1:interests 2 interest2 ZADD user:1:interests 3 interest3
-
使用HyperLogLog: 如果集合的目的是为了计算基数(元素的不重复数量),可以考虑使用HyperLogLog数据结构。它可以在极大规模的数据集合上估计基数,而且占用的内存相对较小。
ruby# 使用HyperLogLog PFADD user:1:interests interest1 interest2 interest3
-
定期清理不需要的元素: 定期检查集合,删除掉不再需要的元素,以保持集合的合理大小。可以使用
SREM
命令来移除指定的元素。ruby# 定期清理集合元素 SREM user:1:interests unwanted_interest
选择合适的方案取决于应用的具体需求。在设计时,需要根据数据的特性和访问模式选择合适的数据结构和拆分策略,以避免大键问题。
2.4: Hash
针对Hash数据结构的大键问题,以下是一些具体的方案:
- 字段分组: 如果一个哈希中的字段数量庞大,可以考虑将字段进行适当的分组,然后使用多个哈希键来存储这些分组。例如,将字段按照某种规则划分为多个小组,然后每个小组使用一个独立的哈希键存储。
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
- 分散数据: 将大哈希拆分为多个小的哈希,每个小哈希存储一部分字段。这样可以将数据分散到多个哈希键中,降低单个哈希键的大小。
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
- 使用多个哈希键: 将相关的信息存储在多个独立的哈希键中,而不是一个大哈希中。这样可以避免一个键变得过大。
ruby
# 使用多个哈希键
HSET user:1:info name John
HSET user:1:contact email john@example.com
HSET user:1:profile age 30
- 定期清理不需要的字段: 定期检查哈希中的字段,清理掉不再需要的字段。这可以通过
HDEL
命令来实现。
ruby
# 定期清理不需要的字段
HDEL user:1:info unwanted_field
选择合适的方案取决于具体的业务需求和数据访问模式。这些方案可以根据应用程序的情况来调整和组合,以解决大Hash键问题。
2.4: Sorted Set
对于有序集合(Sorted Set)数据结构,处理大键(big key)问题可以采取以下一些方案:
-
分割有序集合: 将一个大的有序集合分割成多个小的有序集合,每个小集合只包含部分元素。这样可以降低单个有序集合的大小。
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"
-
按分数范围存储: 如果有序集合的元素有一定的范围,可以将元素按照分数范围存储在不同的有序集合中,以减小每个有序集合的大小。
ruby# 按分数范围存储 ZADD user:1:scores:0to100 100 "Alice" ZADD user:1:scores:101to200 200 "Bob" ZADD user:1:scores:201to300 300 "Charlie"
-
定期修剪: 定期检查有序集合,删除掉不再需要的元素,以保持有序集合的合理大小。可以使用
ZREMRANGEBYRANK
或ZREMRANGEBYSCORE
命令来移除一定范围内的元素。ruby# 定期修剪有序集合 ZREMRANGEBYRANK user:1:scores 0 99
-
使用其他数据结构: 如果有序集合不再适用,可以考虑使用其他数据结构,如哈希(Hash)或字符串(String),具体取决于数据的特性和访问模式。
ruby# 使用哈希或字符串 HSET user:1:info score 100
选择合适的方案取决于应用的具体需求。在设计时,需要根据数据的特性和访问模式选择合适的数据结构和拆分策略,以避免大键问题。
三:热key解决方案
热 Key 问题解决方案
增加 Redis 实例复本数量
对于出现热 Key
的 Redis
实例,我们可以通过水平扩容增加副本数量,将读请求的压力分担到不同副本节点上。
二级缓存(本地缓存)
当出现热 Key
以后,把热 Key
加载到系统的 JVM
中。后续针对这些热 Key
的请求,会直接从 JVM
中获取,而不会走到 Redis
层。这些本地缓存的工具很多,比如 Ehcache
,或者 Google Guava
中 Cache
工具,或者直接使用 HashMap
作为本地缓存工具都是可以的。
使用本地缓存需要注意两个问题:
- 如果对热
Key
进行本地缓存,需要防止本地缓存过大,影响系统性能; - 需要处理本地缓存和
Redis
集群数据的一致性问题。
热 Key 备份
通过前面的分析,我们可以了解到,之所以出现热 Key
,是因为有大量的对同一个 Key
的请求落到同一个 Redis
实例上,如果我们可以有办法将这些请求打散到不同的实例上,防止出现流量倾斜的情况,那么热 Key
问题也就不存在了。
那么如何将对某个热 Key
的请求打散到不同实例上呢?我们就可以通过热 Key
备份的方式,基本的思路就是,我们可以给热 Key
加上前缀或者后缀,把一个热 Key
的数量变成 Redis
实例个数 N
的倍数 M
,从而由访问一个 Redis
Key
变成访问 N * M
个 Redis
Key
。 N * M
个 Redis
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
不会同时过期而造成缓存雪崩。