Redis面试一突发热点key

4 阅读10分钟

Redis面试一突发热点key

热点 Key 是 Redis 生产环境中高频出现的性能瓶颈问题,若处理不及时,可能导致单节点资源耗尽、请求超时,甚至引发缓存雪崩拖垮数据库。

一、热点 Key 产生原因与核心危害

1.1 核心定义

热点 Key 指某一个/某一批 Key 在短时间内(秒级/分钟级)承受的访问量(QPS)远超其他 Key,达到 Redis 单节点处理上限(通常 Redis 单节点 QPS 约 10 万~20 万),引发节点资源瓶颈。

1.2 产生场景

热点类型典型场景示例
读热点(最常见)电商秒杀商品、热门新闻、明星直播相关数据秒杀商品 ID(product:1001)短时间内被百万用户查询
写热点高频更新的计数器、实时排行榜、用户状态同步直播间在线人数计数器(live:10086:count)每秒被上万次自增
可预知热点大促活动、新品发布、节假日营销618 大促提前预热的爆款商品
突发不可预知热点突发事件、社交媒体发酵、恶意刷量突发新闻对应的资讯 ID 被全网高频访问

1.3 核心危害

  • Redis 单节点瓶颈:热点 Key 所在节点 CPU 100%、网卡带宽打满,该节点所有请求超时,甚至触发 Redis 集群主从切换;
  • 缓存穿透/雪崩:热点 Key 失效/节点宕机后,所有请求穿透到数据库,导致数据库压力激增、连接池耗尽;
  • 业务服务不可用:应用服务器因频繁调用超时的 Redis 接口,引发线程池阻塞、服务响应延迟,最终导致业务超时。

二、热点 Key 治理核心思路

核心遵循 「发现→应急→优化」 三步走策略:

  1. 发现:实时监控 Redis 节点的 QPS、CPU、带宽,识别热点 Key(如通过 Redis 自带的 redis-cli --hotkeys、第三方监控工具 Prometheus + Grafana);
  2. 应急:快速拦截/分散热点流量,避免节点崩溃(毫秒级生效);
  3. 优化:从架构层面根治,避免热点 Key 再次产生。

三、读热点 Key 应急处理方案(毫秒级生效)

3.1 应用层本地缓存(最快生效)

原理

在应用服务器(JVM)内存中增加本地缓存层(如 Caffeine、Guava Cache),优先从本地缓存读取热点数据,拦截 90% 以上的读请求,仅少量请求回源 Redis,直接降低 Redis 节点压力。

适用场景
  • 突发读热点,需快速生效;
  • 热点数据允许秒级延迟(最终一致性);
  • 代码已预留本地缓存开关(动态配置无需重启服务)。
代码示例(Caffeine 实现)
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import org.springframework.data.redis.core.StringRedisTemplate;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * 本地缓存拦截读热点 Key
 */
public class LocalHotKeyCache {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // 本地缓存配置:最大缓存1000个热点Key,5秒过期保证数据最终一致性
    private final LoadingCache<String, String> localHotCache = Caffeine.newBuilder()
            .maximumSize(1000) // 限制缓存数量,避免占用过多JVM内存
            .expireAfterWrite(5, TimeUnit.SECONDS) // 短过期时间,平衡一致性与性能
            .recordStats() // 开启统计,便于监控缓存命中率
            .build(key -> stringRedisTemplate.opsForValue().get(key)); // 缓存未命中时回源Redis

    /**
     * 获取数据:优先本地缓存,未命中则回源Redis
     */
    public String getHotKeyData(String key) {
        try {
            return localHotCache.get(key);
        } catch (Exception e) {
            // 本地缓存异常时,直接查询Redis,避免服务不可用
            return stringRedisTemplate.opsForValue().get(key);
        }
    }
}
优缺点
优点缺点
毫秒级生效,无需重启服务多应用实例间数据存在秒级不一致(可接受)
拦截效果显著,降低 90%+ Redis 流量占用 JVM 内存,需控制缓存大小
实现简单,无额外组件依赖仅适用于读热点,无法处理写热点

3.2 网关/Nginx 层缓存(流量入口拦截)

原理

在流量入口(Nginx/OpenResty/APISIX)配置缓存,将热点接口的响应结果直接缓存到网关层,流量根本不会到达应用服务器和 Redis,是「釜底抽薪」式的解决方案。

适用场景
  • 应用层来不及修改代码;
  • 热点 Key 对应固定接口(如商品详情接口 /api/product/{id});
  • 静态/准静态热点数据(如商品基本信息、活动规则)。
实施示例(Nginx + lua-resty-lrucache)
# Nginx 配置:缓存商品详情接口(热点 Key:product:1001)
http {
    # 加载 lua 缓存模块
    lua_shared_dict hot_cache 100m; # 分配100MB内存用于热点缓存

    server {
        listen 80;
        server_name api.example.com;

        location /api/product {
            # 开启缓存,缓存Key为请求URI,过期时间10秒
            access_by_lua_block {
                local lrucache = require "resty.lrucache"
                local cache, err = lrucache.new(1000) -- 缓存1000个热点Key
                if not cache then
                    ngx.log(ngx.ERR, "创建lru缓存失败: ", err)
                end

                local key = ngx.var.uri
                local val = cache:get(key)
                if val then
                    ngx.say(val)
                    ngx.exit(200) -- 缓存命中,直接返回
                end

                -- 缓存未命中,转发到后端应用
                ngx.exec("@backend");
            }

            # 后端应用响应后,将结果存入缓存
            proxy_pass http://backend_server;
            proxy_set_header Host $host;
            
            header_filter_by_lua_block {
                local key = ngx.var.uri
                local val = ngx.resp.get_body_data()
                cache:set(key, val, 10) -- 缓存10秒
            }
        }

        location @backend {
            proxy_pass http://backend_server;
            proxy_set_header Host $host;
        }
    }
}
优缺点
优点缺点
流量拦截最彻底,不占用应用/Redis 资源仅适用于 HTTP 接口级缓存,无法处理非接口类热点 Key
运维操作(reload Nginx)不中断服务数据一致性依赖缓存过期时间,不适合实时数据

3.3 限流与降级(兜底方案)

原理

若本地/网关缓存无法完全扛住流量,或数据一致性要求极高(如实时库存),则对热点 Key 对应的接口进行限流,超出阈值的请求直接降级(返回默认值/友好提示),避免 Redis/数据库被压垮。

实现示例(Sentinel 限流)
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.stereotype.Service;

@Service
public class HotKeyLimitService {
    /**
     * 热点商品查询接口:限流阈值 1000 QPS,超出则降级
     */
    @SentinelResource(value = "hotProductQuery", blockHandler = "hotProductBlockHandler")
    public String getProduct(String productId) {
        // 原业务逻辑:查询Redis/数据库
        return stringRedisTemplate.opsForValue().get("product:" + productId);
    }

    /**
     * 限流降级兜底方法
     */
    public String hotProductBlockHandler(String productId, BlockException e) {
        // 返回默认值/友好提示,避免请求堆积
        return "{\"code\":200,\"msg\":\"当前访问人数过多,请稍后再试\",\"data\":null}";
    }
}

3.4 临时扩容 Redis 从节点(读写分离)

原理

若 Redis 架构支持读写分离,临时增加只读从节点,将热点 Key 的读流量分散到多个从节点,缓解主节点压力。

注意事项
  • Redis Cluster 模式下,单个 Slot 仅归属一个主节点,加从节点仅能分担读流量,写流量仍集中在主节点;
  • 需确保从节点与主节点数据同步延迟在可接受范围内(通常毫秒级);
  • 适合读多写少的热点 Key 场景。

四、写热点 Key 专项处理方案

写热点(如计数器、实时排行榜)无法通过缓存拦截,需针对性解决:

4.1 限流 + 异步化(核心方案)

原理
  • 限流:限制写请求的 QPS(如每秒 1000 次),避免 Redis 主节点写压力过大;
  • 异步化:将同步写请求转为异步(通过消息队列如 RocketMQ/Kafka),应用端快速返回,由消费端批量写入 Redis,降低写请求的瞬时压力。
代码示例
import org.springframework.kafka.core.KafkaTemplate;
import javax.annotation.Resource;

/**
 * 写热点 Key 异步化处理(直播间在线人数计数器)
 */
public class WriteHotKeyAsyncService {
    @Resource
    private KafkaTemplate<String, String> kafkaTemplate;
    private static final String TOPIC = "hotkey:write:async";

    /**
     * 异步提交写请求,应用端快速返回
     */
    public void incrLiveCount(String liveId) {
        // 1. 限流:通过Sentinel限制QPS(如每秒1000次)
        // 2. 发送消息到队列,异步处理
        kafkaTemplate.send(TOPIC, liveId, "incr");
        // 3. 快速返回,不等待Redis写入结果
    }

    /**
     * 消费端批量处理写请求
     */
    @KafkaListener(topics = TOPIC)
    public void consumeWriteRequest(String liveId, String operation) {
        // 批量合并写请求(如每100条合并一次)
        if ("incr".equals(operation)) {
            stringRedisTemplate.opsForValue().increment("live:" + liveId + ":count", 1);
        }
    }
}

4.2 写请求合并(减少写次数)

原理

应用端将短时间内的多次写请求(如 1 秒内的 100 次自增)合并为一次批量写请求(如直接自增 100),大幅减少 Redis 写操作次数。

注意事项
  • 需容忍少量数据延迟(秒级);
  • 应用宕机可能导致合并的请求丢失,需结合消息队列做持久化兜底。

4.3 写热点 Key 拆分

原理

将一个写热点 Key 拆分为多个子 Key,分散到不同 Redis 节点,每个子 Key 承担部分写压力,最终通过聚合得到完整结果。

实现示例(计数器拆分)
/**
 * 写热点 Key 拆分:直播间在线人数计数器
 * 原 Key:live:10086:count → 拆分后:live:10086:count:0 ~ live:10086:count:9
 */
public class WriteHotKeySplitService {
    private static final int SPLIT_COUNT = 10; // 拆分为10个子Key

    /**
     * 自增操作:随机选择一个子Key自增
     */
    public void incrLiveCount(String liveId) {
        int index = new Random().nextInt(SPLIT_COUNT);
        String subKey = "live:" + liveId + ":count:" + index;
        stringRedisTemplate.opsForValue().increment(subKey, 1);
    }

    /**
     * 获取总数:聚合所有子Key的值
     */
    public long getLiveCount(String liveId) {
        long total = 0;
        for (int i = 0; i < SPLIT_COUNT; i++) {
            String subKey = "live:" + liveId + ":count:" + i;
            String val = stringRedisTemplate.opsForValue().get(subKey);
            total += val == null ? 0 : Long.parseLong(val);
        }
        return total;
    }
}

五、长期架构优化方案(根治热点 Key)

5.1 热点 Key 拆分(读/写通用)

核心思路

将一个大热 Key 拆分为多个子 Key,分散存储在 Redis 集群的不同节点,将单点流量分散到多个节点。

操作方法
步骤示例(商品详情 Key:product:1001)
拆分拆分为 product:1001:0 ~ product:1001:9,所有子 Key 存储相同数据
读取客户端随机选择一个子 Key 读取(如 random % 10),分散读流量
写入用 Lua 脚本原子更新所有子 Key,保证数据一致性
Lua 脚本(批量更新子 Key)
-- 批量更新拆分后的热点 Key
-- KEYS:所有子 Key 列表,ARGV[1]:新值
for i, key in ipairs(KEYS) do
    redis.call('SET', key, ARGV[1])
end
return #KEYS

5.2 多级缓存架构(全链路防御)

构建 「浏览器缓存 → CDN → Nginx 缓存 → 应用本地缓存 → Redis 集群 → 数据库」 的多级缓存体系,让超热点数据尽可能在前端缓存层返回,不进入后端:

  • 超热点静态数据(如商品图片、活动海报):CDN 缓存;
  • 准静态数据(如商品基本信息):Nginx 缓存;
  • 实时性要求中等的数据(如商品库存):应用本地缓存;
  • 实时性要求高的数据(如用户余额):Redis 集群。

5.3 热点探测与治理中间件(大厂方案)

对于中大型系统,可接入成熟的热点治理中间件,实现自动化发现与处理:

  • 京东 HotKey:实时分析 Redis 流量,发现热点 Key 后自动推送到应用本地缓存,无需人工干预;
  • 阿里 Tair/Redis 增强版:云厂商提供的 Redis 版本内置热点 Key 自动发现、代理转发、读写分离功能;
  • 字节跳动 KeyDB:兼容 Redis 协议的分布式缓存,支持热点 Key 自动分片。

5.4 事前预防(可预知热点)

对于大促、新品发布等可预知的热点场景,提前做好预防:

  1. 缓存预热:活动开始前,将热点 Key 提前加载到 Redis/本地缓存,避免活动开始后缓存未命中;
  2. Key 拆分预配置:提前将可预知的热点 Key 拆分子 Key,分散到多个 Redis 节点;
  3. 过期时间错开:为拆分后的子 Key 设置不同的过期时间(如基础 30 分钟 + 0~10 分钟随机),避免集体失效引发雪崩;
  4. 资源预留:活动期间临时扩容 Redis 节点、增加应用实例,提升整体承载能力。

六、总结

核心要点

  1. 应急优先:突发读热点优先用「应用本地缓存」,写热点优先用「异步化+限流」,毫秒级生效避免故障扩大;
  2. 架构根治:长期通过「Key 拆分 + 多级缓存 + 中间件」解决,从源头分散热点流量;
  3. 事前预防:可预知热点提前做缓存预热、Key 拆分,避免被动应对。

方案选型建议

场景推荐方案
突发读热点(秒级生效)应用本地缓存(Caffeine)
读热点(无代码修改)Nginx/网关层缓存
写热点(计数器/排行榜)异步化 + 写请求合并 + Key 拆分
可预知热点(大促/新品)缓存预热 + 多级缓存 + 资源预留
大型分布式系统热点治理中间件(HotKey/Tair)