第488集Redis热点key治理实战案例
|字数总计:3.5k|阅读时长:14分钟|阅读量:
Redis热点key治理实战案例
1. 概述
1.1 实战案例的重要性
实战案例是学习Redis热点key治理的最佳方式,通过真实项目的案例,可以深入理解热点key治理的实际应用和效果。
本文内容:
- 真实项目案例:从实际项目中总结的经验
- 问题分析:如何发现和分析热点key问题
- 解决方案:具体的解决方案和实施过程
- 效果评估:治理前后的效果对比
1.2 本文内容结构
本文将从以下几个方面分享Redis热点key治理的实战案例:
- 案例1:电商商品详情页:高并发商品详情页热点key治理
- 案例2:社交平台用户信息:用户信息缓存热点key治理
- 案例3:新闻资讯平台:热门文章热点key治理
- 案例4:直播平台:直播间信息热点key治理
- 经验总结:从案例中总结的经验和教训
2. 案例1:电商商品详情页
2.1 问题背景
2.1.1 业务场景
业务场景:
- 平台:大型电商平台
- 功能:商品详情页展示
- 流量:峰值QPS 10万+
- 问题:某些热门商品详情页访问量巨大
2.1.2 问题现象
问题现象:
- Redis CPU使用率:某些时段达到90%+
- 响应时间:商品详情页响应时间从50ms增加到200ms+
- 错误率:偶尔出现Redis连接超时错误
- 用户投诉:用户反馈页面加载慢
2.1.3 问题分析
问题分析:
- 监控发现:通过监控发现某些商品key的QPS达到5万+
- 单点压力:大量请求集中在单个Redis key上
- 网络瓶颈:Redis网络带宽成为瓶颈
- 连接数:Redis连接数接近上限
2.2 解决方案
2.2.1 方案设计
治理方案:
- 本地缓存:热点商品使用本地缓存(Caffeine)
- 动态分片:超热点商品(QPS > 10000)使用分片
- 限流保护:对热点商品进行限流
- 预热机制:系统启动时预热热门商品
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; 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; ProductDetail product = l1Cache.getIfPresent(cacheKey); if (product != null) { return product; } boolean isHotProduct = isHotProduct(productId); if (isHotProduct) { RateLimiter limiter = rateLimiters.computeIfAbsent(productId, id -> RateLimiter.create(5000)); if (!limiter.tryAcquire()) { return getFallbackProduct(productId); } product = getFromShardedCache(productId); } else { product = getFromRedis(cacheKey); } if (product == null) { product = loadFromDatabase(productId); if (product != null) { if (isHotProduct) { setToShardedCache(productId, product); } else { setToRedis(cacheKey, product); } } } 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); }
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 方案设计
治理方案:
- 本地缓存:热点用户信息使用本地缓存
- 互斥锁:缓存失效时使用分布式锁防止击穿
- 预热机制:定时预热热点用户信息
- 随机过期时间:避免缓存雪崩
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();
public UserInfo getUserInfo(Long userId) { String cacheKey = "user:info:" + userId; UserInfo userInfo = localCache.getIfPresent(cacheKey); if (userInfo != null) { return userInfo; } String userJson = redisTemplate.opsForValue().get(cacheKey); if (userJson != null) { userInfo = JSON.parseObject(userJson, UserInfo.class); if (isHotUser(userId)) { localCache.put(cacheKey, userInfo); } return userInfo; } 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) { int expireSeconds = 3600 + (int)(Math.random() * 600); 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); } }
|
3.3 效果评估
治理效果:
- 缓存击穿减少:使用互斥锁后,缓存击穿减少95%+
- 数据库压力降低:数据库连接数从峰值降至正常水平
- 响应时间优化:平均响应时间从150ms降至60ms
4. 案例3:新闻资讯平台
4.1 问题背景
4.1.1 业务场景
业务场景:
- 平台:新闻资讯平台
- 功能:文章详情页展示
- 流量:突发流量,某些热门文章访问量激增
- 问题:热门文章成为热点key
4.2 解决方案
4.2.1 方案设计
治理方案:
- 本地缓存:热门文章使用本地缓存
- 智能预热:基于历史数据预测热门文章并预热
- 动态分片:超热门文章使用分片
- 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();
public Article getArticle(Long articleId) { String cacheKey = "article:" + articleId; Article article = localCache.getIfPresent(cacheKey); if (article != null) { return article; } String articleJson = redisTemplate.opsForValue().get(cacheKey); if (articleJson != null) { article = JSON.parseObject(articleJson, Article.class); if (isHotArticle(articleId)) { localCache.put(cacheKey, article); } return article; } article = articleMapper.selectById(articleId); if (article != null) { 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); } }
@Component public class ArticlePreloader { @Autowired private ArticleService articleService;
@Scheduled(cron = "0 0 6 * * ?") public void intelligentPreload() { List<Long> hotArticleIds = getYesterdayHotArticles(); 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 方案设计
治理方案:
- 本地缓存:热门直播间信息使用本地缓存
- 实时更新:直播间信息实时更新,使用消息通知
- 分片处理:超热门直播间使用分片
- 限流保护:对热门直播间进行限流
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();
public LiveRoom getLiveRoom(Long roomId) { String cacheKey = "live:room:" + roomId; LiveRoom room = localCache.getIfPresent(cacheKey); if (room != null) { return room; } String roomJson = redisTemplate.opsForValue().get(cacheKey); if (roomJson != null) { room = JSON.parseObject(roomJson, LiveRoom.class); if (isHotRoom(roomId)) { localCache.put(cacheKey, room); } return room; } room = liveRoomMapper.selectById(roomId); if (room != null) { 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(); liveRoomMapper.updateById(room); redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(room), 5, TimeUnit.MINUTES); localCache.put(cacheKey, room); notifyCacheUpdate(cacheKey, room); } private boolean isHotRoom(Long roomId) { String cacheKey = "live:room:" + roomId; return hotKeyDetector.isHotKey(cacheKey, 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 实践建议
建议:
- 实时检测:建立完善的热点key检测机制
- 本地缓存:热点key优先使用本地缓存
- 动态调整:根据实际情况动态调整策略
- 监控告警:实时监控,及时告警
- 持续优化:根据效果持续优化
7. 总结
7.1 核心要点
- 检测是基础:首先要能检测到热点key
- 本地缓存最有效:本地缓存是最有效的治理方案
- 组合使用:组合使用多种方案,取长补短
- 动态调整:根据实际情况动态调整策略
- 持续优化:根据效果持续优化
7.2 关键理解
- 没有万能方案:不同场景需要不同的方案
- 本地缓存优先:热点key优先使用本地缓存
- 监控重要:完善的监控是治理的基础
- 持续优化:治理是一个持续的过程
7.3 最佳实践
- 建立检测机制:实时检测热点key
- 使用本地缓存:热点key使用本地缓存
- 动态调整策略:根据实际情况调整
- 完善监控告警:实时监控,及时告警
- 持续优化改进:根据效果持续优化
相关文章: