Redis 大 Key 与热 Key 治理实战:基于 MySQL 高并发读场景的内存分析与拆分方案 一、问题场景:一场大促引发的缓存危机 某电商系统大促期间,商品详情接口 QPS 从日常 500 飙升到 2 万。为了扛住高并发读,团队在 MySQL 前加了 Redis 缓存。起初效果显著,数据库压力骤降,但活动开始一小时后,Redis 延迟出现周期性毛刺,部分请求响应从 2ms 劣化到 200ms,甚至出现超时。监控显示 Redis 实例 CPU 使用率接近 100%,但整体 QPS 并不高。
排查发现两个典型问题:
某个商品详情缓存 value 是一个完整的 JSON,包含商品基础信息、SKU 列表、评论摘要、推荐列表等,序列化后超过 5MB ,属于典型的大 Key。
该商品正好是活动主推款,单 key 的 QPS 超过 8000 ,属于典型的热 Key。
大 Key 导致 Redis 单线程在处理该 key 的读写时占用大量 CPU 时间,热 Key 又使这个操作被高频触发,最终阻塞其他请求。这引发了我们对 Redis 大 Key 与热 Key 的系统性治理。
二、大 Key 与热 Key 的本质与影响 2.1 定义
大 Key :单个 key 的 value 过大。对于 String 类型,通常认为 value 超过 10KB 就算大 Key;对于 Hash/List/Set/ZSet 等集合类型,元素数量超过 1 万或整体内存超过 1MB 即应警惕。问题本质是单次操作的时间复杂度和网络传输量过大。
热 Key :单个 key 被访问的频率远超其他 key。例如一个 key 的 QPS 占实例总 QPS 的 10% 以上,或绝对 QPS 超过 1000。问题本质是访问倾斜,导致单点资源竞争。
2.2 影响对比
维度
大 Key 的影响
热 Key 的影响
内存
内存占用不均,单个 key 占用过大,可能触发内存淘汰或 OOM
通常内存占用不大,但如果热 Key 也是大 Key,则问题叠加
CPU
序列化/反序列化、数据结构遍历、内存分配等操作耗时高
单 key 高 QPS 使 Redis 单线程 CPU 集中处理该 key
网络
单次请求返回大量数据,占用带宽,容易打满网卡
高频请求本身造成大量网络小包,消耗连接和带宽
阻塞
删除大 Key 会阻塞主线程(Redis 6.0 之前 DEL 是同步的),导致整个实例不可用
热 Key 操作频繁,若每个操作都很重,会阻塞其他请求
可用性
大 Key 在 RDB 持久化或主从同步时可能造成延迟和内存压力
热 Key 容易成为缓存击穿点,一旦失效,大量请求穿透到 MySQL
下面这张 ASCII 图展示了 Redis 单线程处理请求时,一个大 Key 操作阻塞后续所有请求的场景:
1 2 3 4 5 6 7 8 9 10 11 12 客户端请求队列 │ ▼ ┌─────────────────────────────┐ │ Redis 主线程(单线程) │ │ │ │ 处理请求1(正常) │ │ 处理请求2(大 Key 读) │ ← 耗时 5ms,阻塞后续请求 │ 处理请求3(正常) │ ← 等待中 │ 处理请求4(热 Key 写) │ ← 等待中 │ ... │ └─────────────────────────────┘
三、定位大 Key 与热 Key:从命令行到监控 3.1 redis-cli –bigkeys 快速扫描 redis-cli --bigkeys 会扫描整个实例,统计每种数据类型中最大的 key,并给出 top 1 的 key 名称和大小。该命令使用 SCAN 而非 KEYS,不会阻塞主线程,但会消耗一定 CPU 和内存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 redis-cli -h 127.0.0.1 -p 6379 --bigkeys [00.00%] Biggest string found so far '"product:detail:10001"' with 5242880 bytes [00.00%] Biggest hash found so far '"user:cart:888"' with 12000 fields ... -------- summary ------- Sampled 100000 keys in the keyspace! Total key length in bytes is 4800000 (avg len 48.00) Biggest string found '"product:detail:10001"' has 5242880 bytes Biggest hash found '"user:cart:888"' has 12000 fields ...
注意:--bigkeys 只能给出 key 的 value 大小或元素个数,不能直接给出内存占用,且只给出 top 1,对于大 Key 数量较多的情况需结合其他工具。
3.2 memory usage 精确分析内存 memory usage key [samples] 返回指定 key 及其子元素实际占用的内存字节数。对于 Hash、List 等集合类型,可以指定采样数量(默认 5),采样越多结果越精确但越耗时。该命令在 Redis 4.0+ 可用。
1 2 3 4 5 6 7 redis-cli -h 127.0.0.1 -p 6379 memory usage product:detail:10001 redis-cli -h 127.0.0.1 -p 6379 memory usage user:cart:888 samples 100
3.3 object freq 识别热 Key object freq key 返回 key 的访问频率计数,但只有在 Redis 配置了 LFU 淘汰策略(maxmemory-policy 为 allkeys-lfu 或 volatile-lfu)时才有效。LFU 内部使用对数计数器,返回值是一个 0-255 的近似值,并非精确访问次数,但足以反映相对热度。
1 2 3 4 5 6 7 8 9 10 11 12 redis-cli -h 127.0.0.1 -p 6379 config get maxmemory-policy redis-cli -h 127.0.0.1 -p 6379 object freq product:detail:10001 redis-cli -h 127.0.0.1 -p 6379 object freq product:detail:10002
另外,Redis 6.0+ 提供了 redis-cli --hotkeys 命令,可以扫描整个 keyspace 并输出访问频率最高的 key。使用前提同样是启用 LFU 策略。
1 2 3 4 5 6 7 8 redis-cli -h 127.0.0.1 -p 6379 --hotkeys [00.00%] Hot key '"product:detail:10001"' found with freq 220 ...
3.4 监控指标与日志 生产环境不能让开发者频繁执行命令,需要建立监控体系。关键指标包括:
**instantaneous_ops_per_sec**:整体 QPS,异常升高说明可能存在热 Key。
**keyspace_hits / keyspace_misses**:缓存命中率,命中率突然下降可能有大 Key 被淘汰或热 Key 失效。
latency 监控 :redis-cli --latency 或 redis-cli --latency-history 可观察延迟毛刺,结合大 Key 操作时间。
慢查询日志 :slowlog get 可发现执行时间超过阈值的命令,大 Key 的 HGETALL、SMEMBERS、DEL 等常见于慢查询。
1 2 3 4 5 6 7 8 9 redis-cli -h 127.0.0.1 -p 6379 slowlog get 10
四、治理实战一:大 Key 拆分 4.1 拆分策略选择 大 Key 拆分的核心思路是将单个大 value 分解为多个小 value ,常见策略如下:
原 Key 类型
拆分策略
适用场景
String(大 JSON)
拆成 Hash,字段拆分
对象属性较多且读取常按需取字段
Hash(元素多)
按业务维度分桶,如 key:{hash}
用户数据、订单数据等可哈希分片
List / Set / ZSet
按时间或业务键拆成多个集合
消息队列、排行榜等
对于 String 类型的大 JSON,最直接的是将其拆成 Hash,每个字段存储 JSON 中的一部分。这样读取时可按需获取字段,减少网络传输和内存拷贝。如果 Hash 的字段也很多,可以进一步哈希分桶,例如 product:detail:10001 拆成多个 Hash:product:detail:10001:base、product:detail:10001:sku、product:detail:10001:comments。
4.2 代码示例:商品详情大 Key 拆分为 Hash 字段 以下示例使用 Spring Boot 3 + RedisTemplate,将原本 5MB 的 JSON 大 Key 拆分为多个 Hash 字段存储。假设商品详情包含 baseInfo、skuList、commentSummary、recommendList 四个部分。
依赖配置(pom.xml 片段) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > com.fasterxml.jackson.core</groupId > <artifactId > jackson-databind</artifactId > </dependency > </dependencies >
application.yml 1 2 3 4 5 6 7 8 9 10 spring: redis: host: 127.0 .0 .1 port: 6379 timeout: 3s lettuce: pool: max-active: 20 max-idle: 10 min-idle: 5
Java 服务类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import java.util.HashMap;import java.util.Map;@Service public class ProductDetailCacheService { private final StringRedisTemplate redisTemplate; private final ObjectMapper objectMapper; public ProductDetailCacheService (StringRedisTemplate redisTemplate, ObjectMapper objectMapper) { this .redisTemplate = redisTemplate; this .objectMapper = objectMapper; } public void saveProductDetail (String productId, ProductDetail detail) { String hashKey = "product:detail:" + productId; Map<String, String> fields = new HashMap <>(); try { fields.put("baseInfo" , objectMapper.writeValueAsString(detail.getBaseInfo())); fields.put("skuList" , objectMapper.writeValueAsString(detail.getSkuList())); fields.put("commentSummary" , objectMapper.writeValueAsString(detail.getCommentSummary())); fields.put("recommendList" , objectMapper.writeValueAsString(detail.getRecommendList())); } catch (Exception e) { throw new RuntimeException ("序列化商品详情失败" , e); } redisTemplate.opsForHash().putAll(hashKey, fields); } public String getProductDetailPart (String productId, String field) { String hashKey = "product:detail:" + productId; Object value = redisTemplate.opsForHash().get(hashKey, field); return value != null ? value.toString() : null ; } public Map<Object, Object> getFullProductDetail (String productId) { String hashKey = "product:detail:" + productId; return redisTemplate.opsForHash().entries(hashKey); } }
对应的 ProductDetail 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import java.util.List;public class ProductDetail { private BaseInfo baseInfo; private List<Sku> skuList; private CommentSummary commentSummary; private List<Product> recommendList; } class BaseInfo { }class Sku { }class CommentSummary { }class Product { }
运行环境 :JDK 17,Spring Boot 3.2.5,Redis 7.0.11。验证日期:2026-08-30。
拆分后,商品详情接口只需读取 baseInfo 和 commentSummary 两个字段,单次网络传输从 5MB 降到几十 KB,Redis 操作耗时显著下降。
五、治理实战二:热 Key 本地缓存与多级缓存 5.1 多级缓存架构 对于热 Key,最有效的办法是让请求尽量不落到 Redis 。在应用服务器本地增加一层缓存(如 Caffeine),将热点数据缓存到 JVM 堆内,读取路径变为:
1 2 用户请求 → 本地缓存(Caffeine) → Redis → MySQL ↓ 命中返回 ↓ 命中返回 ↓ 回源
本地缓存访问延迟在纳秒级,且不占用 Redis 连接和网络带宽。但需注意数据一致性问题:本地缓存可能滞后。对于商品详情这类读多写少、容忍秒级延迟的场景非常合适。
5.2 代码示例:Caffeine + Redis 两级缓存 以下示例使用 Caffeine 作为本地缓存,Redis 作为二级缓存,MySQL 作为最终数据源。
添加 Caffeine 依赖 1 2 3 4 5 <dependency > <groupId > com.github.ben-manes.caffeine</groupId > <artifactId > caffeine</artifactId > <version > 3.1.8</version > </dependency >
Caffeine 配置类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.github.benmanes.caffeine.cache.Cache;import com.github.benmanes.caffeine.cache.Caffeine;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.util.concurrent.TimeUnit;@Configuration public class CacheConfig { @Bean public Cache<String, ProductDetail> localCache () { return Caffeine.newBuilder() .maximumSize(10_000 ) .expireAfterWrite(10 , TimeUnit.SECONDS) .recordStats() .build(); } }
商品服务类(两级缓存读取) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import com.github.benmanes.caffeine.cache.Cache;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service public class ProductService { private final Cache<String, ProductDetail> localCache; private final StringRedisTemplate redisTemplate; private final ObjectMapper objectMapper; private final ProductRepository productRepository; public ProductService (Cache<String, ProductDetail> localCache, StringRedisTemplate redisTemplate, ObjectMapper objectMapper, ProductRepository productRepository) { this .localCache = localCache; this .redisTemplate = redisTemplate; this .objectMapper = objectMapper; this .productRepository = productRepository; } public ProductDetail getProductDetail (String productId) { String cacheKey = "product:detail:" + productId; String localKey = productId; ProductDetail localValue = localCache.getIfPresent(localKey); if (localValue != null ) { return localValue; } String redisValue = redisTemplate.opsForValue().get(cacheKey); if (redisValue != null ) { try { ProductDetail detail = objectMapper.readValue(redisValue, ProductDetail.class); localCache.put(localKey, detail); return detail; } catch (Exception e) { } } ProductDetail detail = productRepository.findById(productId); if (detail != null ) { try { String json = objectMapper.writeValueAsString(detail); redisTemplate.opsForValue().set(cacheKey, json, 60 , TimeUnit.SECONDS); localCache.put(localKey, detail); } catch (Exception e) { } } return detail; } }
运行环境 :JDK 17,Spring Boot 3.2.5,Redis 7.0.11,Caffeine 3.1.8。
此方案将热 Key 的 QPS 压力从 Redis 转移到应用服务器本地,Redis 侧 QPS 可降低 90% 以上。需要注意本地缓存的内存占用,避免引入新的内存问题。
六、治理实战三:访问聚合(Singleflight 模式) 6.1 热 Key 访问聚合原理 即使有本地缓存,在缓存失效或未命中的瞬间,大量并发请求可能同时打到 Redis 甚至 MySQL。访问聚合的思路是:对于同一个 key 的并发请求,只允许一个请求真正向后端发起加载,其他请求等待并共享结果 。这种模式在 Go 语言中叫 singleflight,在 Java 中可以用 CompletableFuture 实现。
1 2 3 4 5 请求1 ──┐ 请求2 ──┼──> 合并为一个加载请求 ──> Redis / MySQL 请求3 ──┘ │ ▼ 所有请求共享同一个结果
6.2 代码示例:基于 CompletableFuture 的请求合并 以下工具类实现了一个简单的 singleflight 机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import java.util.concurrent.CompletableFuture;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.ExecutionException;import java.util.function.Function;public class SingleFlight <K, V> { private final ConcurrentHashMap<K, CompletableFuture<V>> flights = new ConcurrentHashMap <>(); public V doOnce (K key, Function<K, V> loader) throws ExecutionException, InterruptedException { CompletableFuture<V> future = flights.get(key); if (future == null ) { CompletableFuture<V> newFuture = new CompletableFuture <>(); CompletableFuture<V> oldFuture = flights.putIfAbsent(key, newFuture); if (oldFuture == null ) { future = newFuture; try { V result = loader.apply(key); newFuture.complete(result); } catch (Throwable t) { newFuture.completeExceptionally(t); } finally { flights.remove(key); } } else { future = oldFuture; } } return future.get(); } }
使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class HotKeyCacheService { private final SingleFlight<String, String> singleFlight = new SingleFlight <>(); private final RedisCache redisCache; private final MysqlRepository mysqlRepository; public String getHotData (String key) throws ExecutionException, InterruptedException { return singleFlight.doOnce(key, k -> { String value = redisCache.get(k); if (value == null ) { value = mysqlRepository.query(k); redisCache.set(k, value, 60 ); } return value; }); } }
运行环境 :JDK 17。验证日期:2026-08-30。
在实际生产环境中,还需要实现超时控制、失败重试和并发上限,但上述代码已展示核心思想:合并并发请求,避免缓存击穿。
七、RDB/AOF 与内存参数调优建议 治理大 Key 和热 Key 的同时,也需要调整 Redis 持久化和内存相关参数,以降低整体资源消耗和阻塞风险。
7.1 RDB 持久化调优 1 2 3 4 5 6 7 8 9 # redis.conf # 开启 RDB 压缩,牺牲少量 CPU 换取更小的磁盘文件 rdbcompression yes # 调整 save 触发条件,避免频繁 fork 子进程 save 900 1 save 300 10 save 60 10000 # 设置子进程内存共享,避免写时复制过多 rdb-del-sync-files no
大 Key 会导致 RDB 持久化时 fork 子进程时间变长,以及复制时内存翻倍。拆分后 RDB 文件更小,fork 更快。
7.2 AOF 持久化调优 1 2 3 4 5 6 7 # 使用 RDB 前导 + AOF 追加,减少重写开销 aof-use-rdb-preamble yes # 每秒同步一次,平衡性能与数据安全 appendfsync everysec # 自动重写触发条件 auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb
热 Key 会导致 AOF 记录大量重复命令,通过多