Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > Redis BigKey与HotKey

Redis之BigKey与HotKey问题详解

作者:d3y1

在Redis的实际使用过程中,BigKey和 HotKey是两个常见且可能导致严重性能问题的问题,它们可能导致 Redis 实例响应变慢、内存使用不均、甚至服务不可用,下面就来介绍一下这两个的问题

概述

在 Redis 的实际使用过程中,BigKey(大键)和 HotKey(热键)是两个常见且可能导致严重性能问题的问题。它们可能导致 Redis 实例响应变慢、内存使用不均、甚至服务不可用。

BigKey 指的是单个键值对占用内存过大的情况。

HotKey 指的是某个键的访问频率远高于其他键的情况。

这两个问题往往同时出现,相互影响,需要系统性地进行预防和处理。

BigKey 问题

什么是 BigKey

BigKey 是指单个键值对占用内存过大的键。Redis 官方建议:

超过这些阈值的键可以被视为 BigKey。

BigKey 的危害

1. 内存占用不均

BigKey 会占用大量内存,可能导致 Redis 实例内存使用不均衡。在 Redis Cluster 中,这会导致某些节点的内存使用率远高于其他节点,引发内存倾斜。

2. 阻塞主线程

Redis 是单线程模型,BigKey 的操作会阻塞主线程:

操作影响说明
DEL删除大键会阻塞主线程,时间复杂度 O(N)
HGETALL获取所有字段会阻塞主线程
LRANGE大范围获取列表元素会阻塞
KEYS遍历所有键会严重阻塞
FLUSHDB/FLUSHALL清空数据库会长时间阻塞

3. 网络带宽消耗

BigKey 的读写操作会消耗大量网络带宽,影响其他请求的响应速度。

4. 持久化问题

5. 主从同步延迟

BigKey 的同步会导致主从复制延迟增加,影响数据一致性。

BigKey 的检测

1. 使用 redis-cli --bigkeys

Redis 自带的 --bigkeys 命令可以扫描并统计大键:

redis-cli --bigkeys -i 0.1

参数说明:

输出示例:

-------- summary -------
Sampled 506 keys in the keyspace!
Total key length in bytes is 1885 (avg len 3.73)

Biggest string found 'user:1001:profile' has 10240 bytes
Biggest   list found 'order:queue' has 10003 items
Biggest    set found 'online:users' has 8005 items
Biggest   hash found 'product:info' has 5012 fields

506 keys with 506 types

2. 使用 SCAN 命令

编写脚本使用 SCAN 命令遍历所有键并检查大小:

#!/bin/bash
redis-cli --scan --pattern "*" | while read key; do
    size=$(redis-cli memory usage "$key")
    if [ $size -gt 10240 ]; then
        echo "BigKey: $key, Size: $size bytes"
    fi
done

3. 使用 MEMORY USAGE 命令

redis-cli memory usage your_key

4. 使用 Redis 慢查询日志

配置慢查询阈值:

redis-cli config set slowlog-log-slower-than 10000  # 10ms
redis-cli slowlog get 10

5. 使用 Redis 模块

BigKey 的解决方案

1. 拆分 BigKey

Hash 拆分示例

原始结构:

user:1001:info -> {name: "张三", age: 30, address: "...", ...} (5000+ fields)

拆分后:

user:1001:info:base -> {name: "张三", age: 30}
user:1001:info:contact -> {phone: "...", email: "..."}
user:1001:info:address -> {province: "...", city: "..."}

List 拆分示例

原始结构:

order:queue -> [order1, order2, ..., order10000]

拆分后:

order:queue:0 -> [order1, ..., order1000]
order:queue:1 -> [order1001, ..., order2000]
...
order:queue:9 -> [order9001, ..., order10000]

2. 使用合适的数据结构

场景推荐数据结构避免使用
简单键值对StringHash (少量字段时)
对象属性HashString (JSON)
去重集合SetList
排序集合ZSetSet + 排序
计数器String (INCR)Hash

3. 压缩数据

4. 异步删除 BigKey

使用 UNLINK 命令替代 DEL

redis-cli unlink your_big_key

UNLINK 会在后台线程中删除键,不会阻塞主线程。

5. 分批次操作

对于大集合的操作,分批次进行:

# 原始方式(会阻塞)
redis.hgetall("big_hash")

# 改进方式(分批次)
cursor = 0
while True:
    cursor, data = redis.hscan("big_hash", cursor, count=100)
    process(data)
    if cursor == 0:
        break

6. 设置过期时间

为 BigKey 设置合理的过期时间,避免数据无限累积:

redis-cli expire your_key 3600

HotKey 问题

什么是 HotKey

HotKey 是指某个键的访问频率远高于其他键的键。通常表现为:

HotKey 的危害

1. CPU 负载不均

在 Redis Cluster 中,HotKey 所在节点的 CPU 负载会远高于其他节点:

节点 A: CPU 95% (包含 HotKey)
节点 B: CPU 20%
节点 C: CPU 15%

2. 网络带宽瓶颈

HotKey 的高频访问会消耗大量网络带宽,影响其他请求。

3. 缓存击穿

当 HotKey 过期时,大量请求会同时穿透到后端数据库,导致数据库压力骤增。

4. 请求堆积

由于 Redis 是单线程,HotKey 的处理会导致其他请求排队等待。

5. 主从同步压力

HotKey 的频繁更新会增加主从同步的负担。

HotKey 的检测

1. 使用 Redis INFO 命令

redis-cli info stats

关注 keyspace_hitskeyspace_misses 指标。

2. 使用 MONITOR 命令(谨慎使用)

redis-cli monitor | grep "your_key"

注意MONITOR 会严重影响性能,仅用于调试,不要在生产环境使用。

3. 使用 Redis 慢查询日志

redis-cli config set slowlog-log-slower-than 0
redis-cli slowlog get 100

4. 使用客户端统计

在应用层记录每个键的访问频率:

from collections import defaultdict

access_stats = defaultdict(int)

def get_redis(key):
    access_stats[key] += 1
    return redis.get(key)

# 定期打印统计
def print_stats():
    for key, count in sorted(access_stats.items(), key=lambda x: x[1], reverse=True)[:10]:
        print(f"{key}: {count}")

5. 使用 Redis 4.0+ 的 LFU 淘汰策略

配置 LFU(Least Frequently Used)淘汰策略:

redis-cli config set maxmemory-policy allkeys-lfu

然后使用 OBJECT FREQ 命令查看访问频率:

redis-cli object freq your_key

6. 使用第三方工具

HotKey 的解决方案

1. 本地缓存

在应用层使用本地缓存(如 Guava Cache、Caffeine)缓存 HotKey:

// 使用 Caffeine 本地缓存
Cache<String, String> localCache = Caffeine.newBuilder()
    .maximumSize(1000)
    .expireAfterWrite(1, TimeUnit.MINUTES)
    .build();

public String get(String key) {
    // 先查本地缓存
    String value = localCache.getIfPresent(key);
    if (value != null) {
        return value;
    }
    // 再查 Redis
    value = redis.get(key);
    if (value != null) {
        localCache.put(key, value);
    }
    return value;
}

2. 读写分离

对于读多写少的 HotKey,使用读写分离:

应用 -> 读请求 -> Redis 从节点
应用 -> 写请求 -> Redis 主节点

3. Key 分片

将 HotKey 拆分成多个 Key:

原始方式

hot_product:1001 -> 商品信息

分片方式

hot_product:1001:0 -> 商品信息
hot_product:1001:1 -> 商品信息(副本)
hot_product:1001:2 -> 商品信息(副本)

访问时随机选择一个分片:

import random

def get_hot_product(product_id):
    shard = random.randint(0, 2)
    return redis.get(f"hot_product:{product_id}:{shard}")

4. 备份 Key

为 HotKey 创建多个备份,分散请求:

# 写入时同步更新所有备份
def set_hot_key(key, value):
    pipe = redis.pipeline()
    for i in range(3):
        pipe.set(f"{key}:backup:{i}", value)
    pipe.execute()

# 读取时随机选择一个备份
def get_hot_key(key):
    backup = random.randint(0, 2)
    return redis.get(f"{key}:backup:{backup}")

5. 使用 Redis Cluster

在 Redis Cluster 中,HotKey 会分散到不同的节点,但需要注意:

Hash Tag 示例(会导致数据集中):

user:{1001}:profile
user:{1001}:orders
user:{1001}:cart

这些键会被分配到同一个节点。

6. 限流保护

对 HotKey 的访问进行限流:

from functools import wraps
import time

class RateLimiter:
    def __init__(self, max_calls, period):
        self.max_calls = max_calls
        self.period = period
        self.calls = {}
    
    def allow(self, key):
        now = time.time()
        if key not in self.calls:
            self.calls[key] = []
        # 清理过期记录
        self.calls[key] = [t for t in self.calls[key] if now - t < self.period]
        if len(self.calls[key]) >= self.max_calls:
            return False
        self.calls[key].append(now)
        return True

limiter = RateLimiter(max_calls=1000, period=1)  # 每秒最多 1000 次

def rate_limit(func):
    @wraps(func)
    def wrapper(key, *args, **kwargs):
        if not limiter.allow(key):
            raise Exception("Rate limit exceeded")
        return func(key, *args, **kwargs)
    return wrapper

@rate_limit
def get_hot_key(key):
    return redis.get(key)

7. 缓存预热

在系统启动或低峰期,提前加载 HotKey 到缓存:

def warm_up_cache():
    hot_keys = get_hot_keys_from_db()  # 从数据库获取热点键列表
    for key in hot_keys:
        value = db.get(key)
        redis.set(key, value, ex=3600)

8. 使用多级缓存

构建多级缓存架构:

应用 -> 本地缓存 -> Redis -> 数据库

9. 消息队列削峰

对于写请求较多的 HotKey,使用消息队列削峰:

应用 -> 消息队列 -> 消费者 -> Redis

最佳实践

1. 设计阶段

2. 开发阶段

3. 运维阶段

4. 应急处理

工具推荐

1. Redis 官方工具

工具用途
redis-cli --bigkeys检测 BigKey
redis-cli --memkeys检测占用内存最多的键
redis-cli --hotkeys检测 HotKey(LFU 模式下)
Redis Insight可视化管理工具

2. 第三方工具

工具特点
redis-rdb-tools分析 RDB 文件,找出 BigKey
redis-faina分析 MONITOR 输出,统计访问频率
Redis ExporterPrometheus 指标导出器
Redis CommanderWeb 管理界面
MedisMac 平台的 Redis 客户端

3. 云服务

总结

BigKey 和 HotKey 是 Redis 使用中的常见问题,但通过合理的设计、有效的监控和及时的优化,可以很好地避免和解决这些问题。

核心要点

  1. 预防为主:在设计阶段就考虑避免 BigKey 和 HotKey
  2. 定期巡检:使用工具定期检查,及时发现问题
  3. 合理拆分:对 BigKey 进行拆分,对 HotKey 进行分散
  4. 多级缓存:构建多级缓存架构,减轻 Redis 压力
  5. 监控告警:建立完善的监控和告警机制

到此这篇关于Redis之BigKey与HotKey问题详解的文章就介绍到这了,更多相关Redis BigKey与HotKey内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文