Redis热点key治理实战案例

1. 概述

1.1 实战案例的重要性

实战案例是学习Redis热点key治理的最佳方式,通过真实项目的案例,可以深入理解热点key治理的实际应用和效果。

本文内容

  • 真实项目案例:从实际项目中总结的经验
  • 问题分析:如何发现和分析热点key问题
  • 解决方案:具体的解决方案和实施过程
  • 效果评估:治理前后的效果对比

1.2 本文内容结构

本文将从以下几个方面分享Redis热点key治理的实战案例:

  1. 案例1:电商商品详情页:高并发商品详情页热点key治理
  2. 案例2:社交平台用户信息:用户信息缓存热点key治理
  3. 案例3:新闻资讯平台:热门文章热点key治理
  4. 案例4:直播平台:直播间信息热点key治理
  5. 经验总结:从案例中总结的经验和教训

2. 案例1:电商商品详情页

2.1 问题背景

2.1.1 业务场景

业务场景

  • 平台:大型电商平台
  • 功能:商品详情页展示
  • 流量:峰值QPS 10万+
  • 问题:某些热门商品详情页访问量巨大

2.1.2 问题现象

问题现象

  • Redis CPU使用率:某些时段达到90%+
  • 响应时间:商品详情页响应时间从50ms增加到200ms+
  • 错误率:偶尔出现Redis连接超时错误
  • 用户投诉:用户反馈页面加载慢

2.1.3 问题分析

问题分析

  1. 监控发现:通过监控发现某些商品key的QPS达到5万+
  2. 单点压力:大量请求集中在单个Redis key上
  3. 网络瓶颈:Redis网络带宽成为瓶颈
  4. 连接数:Redis连接数接近上限

2.2 解决方案

2.2.1 方案设计

治理方案

  1. 本地缓存:热点商品使用本地缓存(Caffeine)
  2. 动态分片:超热点商品(QPS > 10000)使用分片
  3. 限流保护:对热点商品进行限流
  4. 预热机制:系统启动时预热热门商品

2.2.2 实施过程

实施步骤

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
@Service
public class ProductDetailService {

@Autowired
private ProductMapper productMapper;

@Autowired
private RedisTemplate<String, String> redisTemplate;

// L1缓存:本地缓存(热点商品)
private final Cache<String, ProductDetail> l1Cache = Caffeine.newBuilder()
.maximumSize(5000)
.expireAfterWrite(3, TimeUnit.MINUTES)
.expireAfterAccess(2, TimeUnit.MINUTES)
.recordStats()
.build();

// 热点商品分片配置
private final Map<Long, Integer> productShardConfig = new ConcurrentHashMap<>();

// 限流器
private final Map<Long, RateLimiter> rateLimiters = new ConcurrentHashMap<>();

/**
* 获取商品详情(综合治理)
*/
public ProductDetail getProductDetail(Long productId) {
String cacheKey = "product:detail:" + productId;

// 1. L1缓存(本地缓存)
ProductDetail product = l1Cache.getIfPresent(cacheKey);
if (product != null) {
return product;
}

// 2. 检查是否是热点商品
boolean isHotProduct = isHotProduct(productId);

if (isHotProduct) {
// 热点商品:限流保护
RateLimiter limiter = rateLimiters.computeIfAbsent(productId,
id -> RateLimiter.create(5000)); // 每秒5000次

if (!limiter.tryAcquire()) {
// 限流,返回降级数据
return getFallbackProduct(productId);
}

// 使用分片
product = getFromShardedCache(productId);
} else {
// 非热点商品:直接读取
product = getFromRedis(cacheKey);
}

if (product == null) {
// 3. 从数据库加载
product = loadFromDatabase(productId);

if (product != null) {
// 写入缓存
if (isHotProduct) {
setToShardedCache(productId, product);
} else {
setToRedis(cacheKey, product);
}
}
}

// 4. 回填L1缓存(如果是热点商品)
if (isHotProduct && product != null) {
l1Cache.put(cacheKey, product);
}

return product;
}

/**
* 从分片缓存获取
*/
private ProductDetail getFromShardedCache(Long productId) {
int shardCount = productShardConfig.getOrDefault(productId, 10);
int shardIndex = (int) (productId % shardCount);

String shardKey = "product:detail:" + productId + ":shard:" + shardIndex;
String productJson = redisTemplate.opsForValue().get(shardKey);

if (productJson != null) {
return JSON.parseObject(productJson, ProductDetail.class);
}

return null;
}

/**
* 写入分片缓存
*/
private void setToShardedCache(Long productId, ProductDetail product) {
int shardCount = productShardConfig.getOrDefault(productId, 10);

// 写入所有分片
for (int i = 0; i < shardCount; i++) {
String shardKey = "product:detail:" + productId + ":shard:" + i;
redisTemplate.opsForValue().set(
shardKey,
JSON.toJSONString(product),
1,
TimeUnit.HOURS
);
}
}

/**
* 检查是否是热点商品
*/
private boolean isHotProduct(Long productId) {
String cacheKey = "product:detail:" + productId;
return hotKeyDetector.isHotKey(cacheKey, 2000); // QPS > 2000
}

/**
* 降级数据
*/
private ProductDetail getFallbackProduct(Long productId) {
// 返回简化的商品信息
ProductDetail product = new ProductDetail();
product.setId(productId);
product.setName("商品信息加载中,请稍后再试");
return product;
}
}

2.3 效果评估

2.3.1 治理前

治理前指标

  • Redis CPU使用率:峰值90%+
  • 平均响应时间:200ms+
  • 错误率:0.1%
  • 用户投诉:每天10+起

2.3.2 治理后

治理后指标

  • Redis CPU使用率:峰值降至40%
  • 平均响应时间:降至80ms
  • 错误率:降至0.01%
  • 用户投诉:降至每天1-2起

2.3.3 效果分析

效果分析

  • 本地缓存命中率:热点商品本地缓存命中率达到85%+
  • Redis压力降低:热点商品对Redis的访问减少80%+
  • 响应速度提升:本地缓存访问速度是Redis的10倍+
  • 系统稳定性提升:Redis不再成为瓶颈

3. 案例2:社交平台用户信息

3.1 问题背景

3.1.1 业务场景

业务场景

  • 平台:大型社交平台
  • 功能:用户信息展示
  • 流量:峰值QPS 5万+
  • 问题:某些明星用户的信息访问量巨大

3.1.2 问题现象

问题现象

  • 热点key:某些用户信息key的QPS达到3万+
  • 缓存击穿:缓存失效时,大量请求直接访问数据库
  • 数据库压力:数据库连接数接近上限

3.2 解决方案

3.2.1 方案设计

治理方案

  1. 本地缓存:热点用户信息使用本地缓存
  2. 互斥锁:缓存失效时使用分布式锁防止击穿
  3. 预热机制:定时预热热点用户信息
  4. 随机过期时间:避免缓存雪崩

3.2.2 实施过程

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
@Service
public class UserInfoService {

@Autowired
private UserMapper userMapper;

@Autowired
private RedisTemplate<String, String> redisTemplate;

@Autowired
private RedissonClient redissonClient;

// 本地缓存
private final Cache<String, UserInfo> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();

/**
* 获取用户信息(热点key治理)
*/
public UserInfo getUserInfo(Long userId) {
String cacheKey = "user:info:" + userId;

// 1. 从本地缓存获取
UserInfo userInfo = localCache.getIfPresent(cacheKey);
if (userInfo != null) {
return userInfo;
}

// 2. 从Redis获取
String userJson = redisTemplate.opsForValue().get(cacheKey);
if (userJson != null) {
userInfo = JSON.parseObject(userJson, UserInfo.class);
// 回填本地缓存
if (isHotUser(userId)) {
localCache.put(cacheKey, userInfo);
}
return userInfo;
}

// 3. 缓存未命中,使用互斥锁防止击穿
return getUserInfoWithLock(userId, cacheKey);
}

/**
* 使用互斥锁获取用户信息
*/
private UserInfo getUserInfoWithLock(Long userId, String cacheKey) {
String lockKey = "lock:user:info:" + userId;
RLock lock = redissonClient.getLock(lockKey);

try {
if (lock.tryLock(100, 10, TimeUnit.MILLISECONDS)) {
try {
// 双重检查
String userJson = redisTemplate.opsForValue().get(cacheKey);
if (userJson != null) {
UserInfo userInfo = JSON.parseObject(userJson, UserInfo.class);
if (isHotUser(userId)) {
localCache.put(cacheKey, userInfo);
}
return userInfo;
}

// 从数据库加载
UserInfo userInfo = userMapper.selectById(userId);
if (userInfo != null) {
// 写入Redis(随机过期时间)
int expireSeconds = 3600 + (int)(Math.random() * 600); // 1小时 ± 10分钟
redisTemplate.opsForValue().set(
cacheKey,
JSON.toJSONString(userInfo),
expireSeconds,
TimeUnit.SECONDS
);

// 写入本地缓存
if (isHotUser(userId)) {
localCache.put(cacheKey, userInfo);
}
}

return userInfo;
} finally {
lock.unlock();
}
} else {
// 获取锁失败,等待后重试
Thread.sleep(50);
return getUserInfo(userId);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("获取锁被中断", e);
}
}

private boolean isHotUser(Long userId) {
String cacheKey = "user:info:" + userId;
return hotKeyDetector.isHotKey(cacheKey, 1000); // QPS > 1000
}
}

3.3 效果评估

治理效果

  • 缓存击穿减少:使用互斥锁后,缓存击穿减少95%+
  • 数据库压力降低:数据库连接数从峰值降至正常水平
  • 响应时间优化:平均响应时间从150ms降至60ms

4. 案例3:新闻资讯平台

4.1 问题背景

4.1.1 业务场景

业务场景

  • 平台:新闻资讯平台
  • 功能:文章详情页展示
  • 流量:突发流量,某些热门文章访问量激增
  • 问题:热门文章成为热点key

4.2 解决方案

4.2.1 方案设计

治理方案

  1. 本地缓存:热门文章使用本地缓存
  2. 智能预热:基于历史数据预测热门文章并预热
  3. 动态分片:超热门文章使用分片
  4. CDN缓存:结合CDN缓存

4.2.2 实施过程

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@Service
public class ArticleService {

@Autowired
private ArticleMapper articleMapper;

@Autowired
private RedisTemplate<String, String> redisTemplate;

// 本地缓存
private final Cache<String, Article> localCache = Caffeine.newBuilder()
.maximumSize(2000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();

/**
* 获取文章(热点key治理)
*/
public Article getArticle(Long articleId) {
String cacheKey = "article:" + articleId;

// 1. 从本地缓存获取
Article article = localCache.getIfPresent(cacheKey);
if (article != null) {
return article;
}

// 2. 从Redis获取
String articleJson = redisTemplate.opsForValue().get(cacheKey);
if (articleJson != null) {
article = JSON.parseObject(articleJson, Article.class);
// 检查是否是热门文章
if (isHotArticle(articleId)) {
localCache.put(cacheKey, article);
}
return article;
}

// 3. 从数据库加载
article = articleMapper.selectById(articleId);
if (article != null) {
// 写入Redis
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(article), 2, TimeUnit.HOURS);
// 如果是热门文章,写入本地缓存
if (isHotArticle(articleId)) {
localCache.put(cacheKey, article);
}
}

return article;
}

private boolean isHotArticle(Long articleId) {
String cacheKey = "article:" + articleId;
return hotKeyDetector.isHotKey(cacheKey, 500); // QPS > 500
}
}

@Component
public class ArticlePreloader {

@Autowired
private ArticleService articleService;

/**
* 智能预热(基于历史数据)
*/
@Scheduled(cron = "0 0 6 * * ?") // 每天早上6点执行
public void intelligentPreload() {
// 1. 获取昨日热门文章
List<Long> hotArticleIds = getYesterdayHotArticles();

// 2. 预热到本地缓存
for (Long articleId : hotArticleIds) {
try {
articleService.getArticle(articleId);
} catch (Exception e) {
log.error("Preload article failed: {}", articleId, e);
}
}

log.info("Preloaded {} hot articles", hotArticleIds.size());
}

private List<Long> getYesterdayHotArticles() {
// 从统计系统获取昨日热门文章
// 简化处理
return new ArrayList<>();
}
}

5. 案例4:直播平台

5.1 问题背景

5.1.1 业务场景

业务场景

  • 平台:直播平台
  • 功能:直播间信息展示
  • 流量:热门直播间访问量巨大
  • 问题:热门直播间信息成为热点key

5.2 解决方案

5.2.1 方案设计

治理方案

  1. 本地缓存:热门直播间信息使用本地缓存
  2. 实时更新:直播间信息实时更新,使用消息通知
  3. 分片处理:超热门直播间使用分片
  4. 限流保护:对热门直播间进行限流

5.2.2 实施过程

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
@Service
public class LiveRoomService {

@Autowired
private LiveRoomMapper liveRoomMapper;

@Autowired
private RedisTemplate<String, String> redisTemplate;

// 本地缓存
private final Cache<String, LiveRoom> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.MINUTES) // 直播间信息变化频繁,过期时间短
.build();

/**
* 获取直播间信息(热点key治理)
*/
public LiveRoom getLiveRoom(Long roomId) {
String cacheKey = "live:room:" + roomId;

// 1. 从本地缓存获取
LiveRoom room = localCache.getIfPresent(cacheKey);
if (room != null) {
return room;
}

// 2. 从Redis获取
String roomJson = redisTemplate.opsForValue().get(cacheKey);
if (roomJson != null) {
room = JSON.parseObject(roomJson, LiveRoom.class);
// 检查是否是热门直播间
if (isHotRoom(roomId)) {
localCache.put(cacheKey, room);
}
return room;
}

// 3. 从数据库加载
room = liveRoomMapper.selectById(roomId);
if (room != null) {
// 写入Redis
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(room), 5, TimeUnit.MINUTES);
// 如果是热门直播间,写入本地缓存
if (isHotRoom(roomId)) {
localCache.put(cacheKey, room);
}
}

return room;
}

/**
* 更新直播间信息(实时更新)
*/
public void updateLiveRoom(LiveRoom room) {
String cacheKey = "live:room:" + room.getId();

// 1. 更新数据库
liveRoomMapper.updateById(room);

// 2. 更新Redis
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(room), 5, TimeUnit.MINUTES);

// 3. 更新本地缓存
localCache.put(cacheKey, room);

// 4. 通知其他实例更新本地缓存
notifyCacheUpdate(cacheKey, room);
}

private boolean isHotRoom(Long roomId) {
String cacheKey = "live:room:" + roomId;
return hotKeyDetector.isHotKey(cacheKey, 3000); // QPS > 3000
}

private void notifyCacheUpdate(String cacheKey, LiveRoom room) {
// 通过消息队列通知其他实例
CacheUpdateEvent event = new CacheUpdateEvent();
event.setKey(cacheKey);
event.setEventType("UPDATE");
event.setData(JSON.toJSONString(room));

kafkaTemplate.send("cache-update", JSON.toJSONString(event));
}
}

6. 经验总结

6.1 关键经验

6.1.1 检测是基础

经验

  • 实时检测:必须能够实时检测到热点key
  • 多维度监控:QPS、响应时间、错误率等多维度监控
  • 自动告警:发现热点key后自动告警

6.1.2 本地缓存最有效

经验

  • 优先使用:热点key优先使用本地缓存
  • 命中率高:本地缓存命中率通常能达到80%+
  • 性能提升:本地缓存访问速度是Redis的10倍+

6.1.3 组合使用多种方案

经验

  • 多级缓存:本地缓存 + Redis + 数据库
  • 动态调整:根据实际情况动态调整治理策略
  • 综合方案:组合使用多种方案,取长补短

6.2 常见问题

6.2.1 数据一致性问题

问题:本地缓存和Redis数据可能不一致。

解决方案

  • 主动更新:数据更新时主动更新多级缓存
  • 消息通知:通过消息队列通知其他实例更新
  • 定期校验:定期校验数据一致性

6.2.2 内存占用问题

问题:本地缓存占用应用内存。

解决方案

  • 合理设置大小:根据实际情况设置缓存大小
  • LRU淘汰:使用LRU算法淘汰不常用的数据
  • 只缓存热点:只缓存真正的热点key

6.3 最佳实践

6.3.1 实践建议

建议

  1. 实时检测:建立完善的热点key检测机制
  2. 本地缓存:热点key优先使用本地缓存
  3. 动态调整:根据实际情况动态调整策略
  4. 监控告警:实时监控,及时告警
  5. 持续优化:根据效果持续优化

7. 总结

7.1 核心要点

  1. 检测是基础:首先要能检测到热点key
  2. 本地缓存最有效:本地缓存是最有效的治理方案
  3. 组合使用:组合使用多种方案,取长补短
  4. 动态调整:根据实际情况动态调整策略
  5. 持续优化:根据效果持续优化

7.2 关键理解

  1. 没有万能方案:不同场景需要不同的方案
  2. 本地缓存优先:热点key优先使用本地缓存
  3. 监控重要:完善的监控是治理的基础
  4. 持续优化:治理是一个持续的过程

7.3 最佳实践

  1. 建立检测机制:实时检测热点key
  2. 使用本地缓存:热点key使用本地缓存
  3. 动态调整策略:根据实际情况调整
  4. 完善监控告警:实时监控,及时告警
  5. 持续优化改进:根据效果持续优化

相关文章