【JAVA架构师成长之路】【Redis】第17集:Redis热门Key问题分析与解决方案 ...

打印 上一主题 下一主题

主题 961|帖子 961|积分 2883

30分钟自学教程:Redis热门Key问题分析与解决方案

目标

  • 理解热门Key的定义、危害及成因。
  • 掌握本地缓存、分片、读写分离等核心解决策略。
  • 可以或许通过代码实现热门Key的读写优化。
  • 学会熔断降级、主动探测等应急方案。

教程内容

0~2分钟:热门Key的定义与核心影响



  • 定义:某个或少数Key被极端高频访问(如百万QPS),导致Redis单节点负载过高。
  • 典范场景

    • 秒杀商品详情页的库存Key。
    • 热门微博、新闻的评论列表Key。

  • 危害

    • Redis单节点CPU/网络过载,引发性能抖动。
    • 集群模式下数据倾斜,部分节点压力过大。


2~5分钟:代码模拟热门Key场景(Java示例)

  1. // 模拟高频访问热点Key(Java示例)  
  2. @GetMapping("/product/{id}")  
  3. public Product getProduct(@PathVariable String id) {  
  4.     String key = "product:" + id;  
  5.     Product product = redisTemplate.opsForValue().get(key);  
  6.     if (product == null) {  
  7.         product = productService.loadFromDB(id);  
  8.         redisTemplate.opsForValue().set(key, product, 1, TimeUnit.HOURS);  
  9.     }  
  10.     return product;  
  11. }  
  12. // 使用JMeter模拟1000线程并发访问id=1001的接口  
复制代码
问题复现


  • Redis监控表现product:1001的QPS飙升,单节点CPU占用超过90%。

5~12分钟:解决方案1——本地缓存(Caffeine)



  • 原理:在应用层缓存热门数据,减少对Redis的直接访问。
  • 代码实现(Spring Boot集成Caffeine)
  1. // 配置Caffeine本地缓存  
  2. @Bean  
  3. public CacheManager cacheManager() {  
  4.     CaffeineCacheManager cacheManager = new CaffeineCacheManager();  
  5.     cacheManager.setCaffeine(Caffeine.newBuilder()  
  6.         .expireAfterWrite(10, TimeUnit.SECONDS) // 短暂过期,防止数据不一致  
  7.         .maximumSize(1000));  
  8.     return cacheManager;  
  9. }  
  10. // 使用本地缓存  
  11. @Cacheable(value = "productCache", key = "#id")  
  12. public Product getProductWithLocalCache(String id) {  
  13.     return redisTemplate.opsForValue().get("product:" + id);  
  14. }  
复制代码


  • 优势

    • 将99%的哀求拦截在应用层,极大降低Redis压力。
    • 适合读多写少且容忍短暂不一致的场景。


12~20分钟:解决方案2——Key分片(Sharding)



  • 原理:将热门Key拆分为多个子Key,分散访问压力。
  • 代码实现(动态分片)
  1. // 写入时随机分片  
  2. public void setProductShard(Product product, int shardCount) {  
  3.     String baseKey = "product:" + product.getId();  
  4.     for (int i = 0; i < shardCount; i++) {  
  5.         String shardKey = baseKey + ":shard_" + i;  
  6.         redisTemplate.opsForValue().set(shardKey, product, 1, TimeUnit.HOURS);  
  7.     }  
  8. }  
  9. // 读取时随机选择一个分片  
  10. public Product getProductShard(String id, int shardCount) {  
  11.     int shardIndex = new Random().nextInt(shardCount);  
  12.     String shardKey = "product:" + id + ":shard_" + shardIndex;  
  13.     return redisTemplate.opsForValue().get(shardKey);  
  14. }  
复制代码


  • 扩展

    • 分片数目可根据并发量动态调解(如QPS每增加1万,分片数+1)。


20~25分钟:解决方案3——读写分离与代理中间件



  • 原理:通过读写分离或代理层(如Twemproxy、Codis)分散哀求。
  • 代码实现(读写分离)
  1. // 配置读写分离数据源(Spring Boot示例)  
  2. @Configuration  
  3. public class RedisConfig {  
  4.     @Bean  
  5.     public RedisConnectionFactory writeConnectionFactory() {  
  6.         // 主节点配置  
  7.         RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("master-host", 6379);  
  8.         return new LettuceConnectionFactory(config);  
  9.     }  
  10.     @Bean  
  11.     public RedisConnectionFactory readConnectionFactory() {  
  12.         // 从节点配置  
  13.         RedisStandaloneConfiguration config = new RedisStandaloneConfiguration("slave-host", 6379);  
  14.         return new LettuceConnectionFactory(config);  
  15.     }  
  16.     @Bean  
  17.     public RedisTemplate<String, Object> redisTemplate(  
  18.         @Qualifier("writeConnectionFactory") RedisConnectionFactory writeFactory,  
  19.         @Qualifier("readConnectionFactory") RedisConnectionFactory readFactory  
  20.     ) {  
  21.         RedisTemplate<String, Object> template = new RedisTemplate<>();  
  22.         template.setConnectionFactory(writeFactory); // 默认写主  
  23.         template.setDefaultSerializer(new StringRedisSerializer());  
  24.         return template;  
  25.     }  
  26. }  
  27. // 读操作手动切换至从节点  
  28. public Product getProductFromSlave(String id) {  
  29.     RedisTemplate slaveTemplate = createSlaveRedisTemplate(); // 从从节点获取连接  
  30.     return slaveTemplate.opsForValue().get("product:" + id);  
  31. }  
复制代码

25~28分钟:应急处理方案


  • 熔断降级(Sentinel熔断示例)
  1. @SentinelResource(value = "getProduct", blockHandler = "handleBlock")  
  2. public Product getProduct(String id) {  
  3.     // 正常业务逻辑...  
  4. }  
  5. public Product handleBlock(String id, BlockException ex) {  
  6.     return new Product("默认商品", 0.0); // 返回兜底数据  
  7. }  
复制代码

  • 动态热门探测与主动扩容
  1. // 热点Key监控器(伪代码)  
  2. public class HotKeyDetector {  
  3.     private ConcurrentHashMap<String, AtomicLong> counter = new ConcurrentHashMap<>();  
  4.     @Scheduled(fixedRate = 1000)  
  5.     public void detectHotKeys() {  
  6.         counter.entrySet().removeIf(entry -> {  
  7.             if (entry.getValue().get() > 10000) { // QPS超过1万判定为热点  
  8.                 expandSharding(entry.getKey()); // 触发分片扩容  
  9.                 return true;  
  10.             }  
  11.             return false;  
  12.         });  
  13.     }  
  14.     private void expandSharding(String key) {  
  15.         // 动态增加分片数量并迁移数据  
  16.     }  
  17. }  
复制代码

28~30分钟:总结与优化方向



  • 核心原则:分散哀求、就近缓存、动态调解。
  • 高级优化

    • 使用Redis Cluster主动分片。
    • 结合一致性哈希算法优化分片策略。
    • 通过监控体系(如Prometheus)实时预警热门Key。


练习与拓展

练习

  • 使用Caffeine实现一个本地缓存,拦截高频访问的product:1001哀求。
  • 修改分片代码,支持根据Key的QPS动态调解分片数目。
保举拓展

  • 研究阿里开源的Tair对热门Key的优化方案。
  • 学习Redis Cluster的Gossip协议与数据迁徙机制。
  • 探索代理中间件(如Twemproxy)的源码实现。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

前进之路

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表