首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

SpringBoot中2种热点KEY缓存优化策略

  • 25-04-25 12:42
  • 4078
  • 9361
juejin.cn

所谓热点KEY,是指在缓存或数据库中被频繁访问的少量键值,这些键往往承载了系统中大部分的访问流量。

根据二八原则,通常20%的数据承担了80%的访问量,甚至在某些极端情况下,单个KEY可能会吸引系统超过50%的流量。

当这些热点KEY没有得到合理处理时,可能导致:

  • 缓存节点CPU使用率飙升
  • 网络带宽争用
  • 缓存服务响应延迟增加
  • 缓存穿透导致数据库压力骤增
  • 在极端情况下,甚至引发系统雪崩

本文将深入探讨SpringBoot中三种主流的热点KEY缓存优化策略,提升系统在面对热点KEY时的性能表现。

1. 分级缓存策略

1.1 原理解析

分级缓存策略采用多层次的缓存架构,通常包括本地缓存(L1)和分布式缓存(L2)。当访问热点KEY时,系统首先查询本地内存缓存,避免网络开销;仅当本地缓存未命中时,才请求分布式缓存。

开源实现有JetCache、J2Cache

这种策略能有效降低热点KEY对分布式缓存的访问压力,同时大幅提升热点数据的访问速度。

分级缓存的核心工作流程:

  1. 请求首先访问本地缓存(如Caffeine)
  2. 本地缓存命中直接返回数据(纳秒级)
  3. 本地缓存未命中,请求分布式缓存(如Redis)
  4. 分布式缓存命中,返回数据并回填本地缓存
  5. 分布式缓存未命中,查询数据源并同时更新本地和分布式缓存

1.2 实现方式

步骤1:添加相关依赖

xml
代码解读
复制代码
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-data-redisartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-cacheartifactId> dependency> <dependency> <groupId>com.github.ben-manes.caffeinegroupId> <artifactId>caffeineartifactId> dependency>

步骤2:配置分级缓存管理器

scss
代码解读
复制代码
@Configuration @EnableCaching public class LayeredCacheConfig { @Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { LayeredCacheManager cacheManager = new LayeredCacheManager( createLocalCacheManager(), createRedisCacheManager(redisConnectionFactory) ); return cacheManager; } private CacheManager createLocalCacheManager() { CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); // 本地缓存配置 - 为热点KEY特别优化 caffeineCacheManager.setCaffeine(Caffeine.newBuilder() .initialCapacity(100) // 初始大小 .maximumSize(1000) // 最大缓存对象数 .expireAfterWrite(1, TimeUnit.MINUTES) // 写入后1分钟过期 .recordStats()); // 开启统计 return caffeineCacheManager; } private CacheManager createRedisCacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)) // Redis缓存10分钟过期 .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(config) .build(); } }

步骤3:实现自定义分级缓存管理器

typescript
代码解读
复制代码
public class LayeredCacheManager implements CacheManager { private final CacheManager localCacheManager; // 本地缓存(L1) private final CacheManager remoteCacheManager; // 分布式缓存(L2) private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>(); public LayeredCacheManager(CacheManager localCacheManager, CacheManager remoteCacheManager) { this.localCacheManager = localCacheManager; this.remoteCacheManager = remoteCacheManager; } @Override public Cache getCache(String name) { return cacheMap.computeIfAbsent(name, this::createLayeredCache); } @Override public Collection<String> getCacheNames() { Set<String> names = new LinkedHashSet<>(); names.addAll(localCacheManager.getCacheNames()); names.addAll(remoteCacheManager.getCacheNames()); return names; } private Cache createLayeredCache(String name) { Cache localCache = localCacheManager.getCache(name); Cache remoteCache = remoteCacheManager.getCache(name); return new LayeredCache(name, localCache, remoteCache); } // 分级缓存实现 static class LayeredCache implements Cache { private final String name; private final Cache localCache; private final Cache remoteCache; public LayeredCache(String name, Cache localCache, Cache remoteCache) { this.name = name; this.localCache = localCache; this.remoteCache = remoteCache; } @Override public String getName() { return name; } @Override public Object getNativeCache() { return this; } @Override public ValueWrapper get(Object key) { // 先查本地缓存 ValueWrapper localValue = localCache.get(key); if (localValue != null) { return localValue; } // 本地未命中,查远程缓存 ValueWrapper remoteValue = remoteCache.get(key); if (remoteValue != null) { // 回填本地缓存 localCache.put(key, remoteValue.get()); return remoteValue; } return null; } @Override public T get(Object key, Class type) { // 先查本地缓存 T localValue = localCache.get(key, type); if (localValue != null) { return localValue; } // 本地未命中,查远程缓存 T remoteValue = remoteCache.get(key, type); if (remoteValue != null) { // 回填本地缓存 localCache.put(key, remoteValue); return remoteValue; } return null; } @Override public T get(Object key, Callable valueLoader) { // 先查本地缓存 ValueWrapper localValue = localCache.get(key); if (localValue != null) { return (T) localValue.get(); } // 本地未命中,查远程缓存 ValueWrapper remoteValue = remoteCache.get(key); if (remoteValue != null) { // 回填本地缓存 T value = (T) remoteValue.get(); localCache.put(key, value); return value; } // 远程也未命中,调用值加载器 try { T value = valueLoader.call(); if (value != null) { // 同时更新本地和远程缓存 put(key, value); } return value; } catch (Exception e) { throw new ValueRetrievalException(key, valueLoader, e); } } @Override public void put(Object key, Object value) { localCache.put(key, value); remoteCache.put(key, value); } @Override public void evict(Object key) { localCache.evict(key); remoteCache.evict(key); } @Override public void clear() { localCache.clear(); remoteCache.clear(); } } }

步骤4:在服务中使用分级缓存

kotlin
代码解读
复制代码
@Service public class ProductService { private final ProductRepository productRepository; public ProductService(ProductRepository productRepository) { this.productRepository = productRepository; } // 使用自定义缓存处理热点商品数据 @Cacheable(value = "products", key = "#id", cacheManager = "cacheManager") public Product getProductById(Long id) { // 模拟数据库访问延迟 try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return productRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException("Product not found: " + id)); } // 处理热门商品列表 @Cacheable(value = "hotProducts", key = "'top' + #limit", cacheManager = "cacheManager") public List getHotProducts(int limit) { // 复杂查询获取热门商品 return productRepository.findTopSellingProducts(limit); } // 更新商品信息 - 同时更新缓存 @CachePut(value = "products", key = "#product.id", cacheManager = "cacheManager") public Product updateProduct(Product product) { return productRepository.save(product); } // 删除商品 - 同时删除缓存 @CacheEvict(value = "products", key = "#id", cacheManager = "cacheManager") public void deleteProduct(Long id) { productRepository.deleteById(id); } }

1.3 优缺点分析

优点

  • 显著降低热点KEY的访问延迟,本地缓存访问速度可达纳秒级
  • 大幅减轻分布式缓存的负载压力,提高系统整体吞吐量
  • 减少网络IO开销,节约带宽资源
  • 即使分布式缓存短暂不可用,本地缓存仍可提供服务,增强系统弹性

缺点

  • 增加了系统复杂度,需管理两层缓存
  • 存在数据一致性挑战,不同节点的本地缓存可能不同步
  • 本地缓存占用应用服务器内存资源
  • 适合读多写少的场景,写入频繁场景效果有限

适用场景

  • 高频访问且相对稳定的热点数据(如商品详情、用户配置)
  • 读多写少的业务场景
  • 对访问延迟敏感的关键业务
  • 分布式缓存面临高负载的系统

2. 缓存分片策略

2.1 原理解析

缓存分片策略针对单个热点KEY可能导致的单点压力问题,通过将一个热点KEY拆分为多个物理子KEY,将访问负载均匀分散到多个缓存节点或实例上。这种策略在不改变业务逻辑的前提下,有效提升了系统处理热点KEY的能力。

其核心原理是:

  1. 将一个逻辑上的热点KEY映射为多个物理子KEY
  2. 访问时,随机或按某种规则选择一个子KEY进行操作
  3. 写入时,同步更新所有子KEY,保证数据一致性
  4. 通过分散访问压力,避免单个缓存节点的性能瓶颈

2.2 实现方式

步骤1:创建缓存分片管理器

arduino
代码解读
复制代码
@Component public class ShardedCacheManager { private final RedisTemplate<String, Object> redisTemplate; private final Random random = new Random(); // 热点KEY分片数量 private static final int DEFAULT_SHARDS = 10; // 分片KEY的有效期略有差异,避免同时过期 private static final int BASE_TTL_MINUTES = 30; private static final int TTL_VARIATION_MINUTES = 10; public ShardedCacheManager(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } /** * 获取分片缓存的值 */ public T getValue(String key, Class type) { // 随机选择一个分片 String shardKey = generateShardKey(key, random.nextInt(DEFAULT_SHARDS)); return (T) redisTemplate.opsForValue().get(shardKey); } /** * 设置分片缓存的值 */ public void setValue(String key, Object value) { // 写入所有分片 for (int i = 0; i < DEFAULT_SHARDS; i++) { String shardKey = generateShardKey(key, i); // 计算略有差异的TTL,避免同时过期 int ttlMinutes = BASE_TTL_MINUTES + random.nextInt(TTL_VARIATION_MINUTES); redisTemplate.opsForValue().set( shardKey, value, ttlMinutes, TimeUnit.MINUTES ); } } /** * 删除分片缓存 */ public void deleteValue(String key) { // 删除所有分片 List<String> keys = new ArrayList<>(DEFAULT_SHARDS); for (int i = 0; i < DEFAULT_SHARDS; i++) { keys.add(generateShardKey(key, i)); } redisTemplate.delete(keys); } /** * 生成分片KEY */ private String generateShardKey(String key, int shardIndex) { return String.format("%s:%d", key, shardIndex); } }

步骤2:创建热点KEY识别和处理组件

typescript
代码解读
复制代码
@Component public class HotKeyDetector { private final RedisTemplate<String, Object> redisTemplate; private final ShardedCacheManager shardedCacheManager; // 热点KEY计数器的Hash名称 private static final String HOT_KEY_COUNTER = "hotkey:counter"; // 热点判定阈值 - 每分钟访问次数 private static final int HOT_KEY_THRESHOLD = 1000; // 热点KEY记录 private final Set<String> detectedHotKeys = ConcurrentHashMap.newKeySet(); public HotKeyDetector(RedisTemplate<String, Object> redisTemplate, ShardedCacheManager shardedCacheManager) { this.redisTemplate = redisTemplate; this.shardedCacheManager = shardedCacheManager; // 启动定时任务,定期识别热点KEY scheduleHotKeyDetection(); } /** * 记录KEY的访问次数 */ public void recordKeyAccess(String key) { redisTemplate.opsForHash().increment(HOT_KEY_COUNTER, key, 1); } /** * 检查KEY是否是热点KEY */ public boolean isHotKey(String key) { return detectedHotKeys.contains(key); } /** * 使用合适的缓存策略获取值 */ public T getValue(String key, Class type, Supplier dataLoader) { if (isHotKey(key)) { // 使用分片策略处理热点KEY T value = shardedCacheManager.getValue(key, type); if (value != null) { return value; } // 分片中没有找到,从数据源加载并更新分片 value = dataLoader.get(); if (value != null) { shardedCacheManager.setValue(key, value); } return value; } else { // 对于非热点KEY,使用常规方式处理 T value = (T) redisTemplate.opsForValue().get(key); if (value != null) { return value; } // 缓存未命中,记录访问并从数据源加载 recordKeyAccess(key); value = dataLoader.get(); if (value != null) { redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES); } return value; } } /** * 定期识别热点KEY的任务 */ private void scheduleHotKeyDetection() { ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); executor.scheduleAtFixedRate(() -> { try { // 获取所有KEY的访问计数 Map<Object, Object> counts = redisTemplate.opsForHash().entries(HOT_KEY_COUNTER); // 清空之前识别的热点KEY Set<String> newHotKeys = new HashSet<>(); // 识别新的热点KEY for (Map.Entry<Object, Object> entry : counts.entrySet()) { String key = (String) entry.getKey(); int count = ((Number) entry.getValue()).intValue(); if (count > HOT_KEY_THRESHOLD) { newHotKeys.add(key); // 对新发现的热点KEY,预热分片缓存 if (!detectedHotKeys.contains(key)) { preloadHotKeyToShards(key); } } } // 更新热点KEY集合 detectedHotKeys.clear(); detectedHotKeys.addAll(newHotKeys); // 清除计数器,开始新一轮计数 redisTemplate.delete(HOT_KEY_COUNTER); } catch (Exception e) { // 异常处理 e.printStackTrace(); } }, 1, 1, TimeUnit.MINUTES); } /** * 预热热点KEY到分片缓存 */ private void preloadHotKeyToShards(String key) { // 获取原始缓存中的值 Object value = redisTemplate.opsForValue().get(key); if (value != null) { // 将值复制到所有分片 shardedCacheManager.setValue(key, value); } } }

步骤3:在服务中集成热点KEY处理

arduino
代码解读
复制代码
@Service public class EnhancedProductService { private final ProductRepository productRepository; private final HotKeyDetector hotKeyDetector; public EnhancedProductService(ProductRepository productRepository, HotKeyDetector hotKeyDetector) { this.productRepository = productRepository; this.hotKeyDetector = hotKeyDetector; } /** * 获取商品信息,自动处理热点KEY */ public Product getProductById(Long id) { String cacheKey = "product:" + id; return hotKeyDetector.getValue(cacheKey, Product.class, () -> { // 从数据库加载产品信息 return productRepository.findById(id) .orElseThrow(() -> new ProductNotFoundException("Product not found: " + id)); }); } /** * 获取热门商品列表,自动处理热点KEY */ public List getHotProducts(int limit) { String cacheKey = "products:hot:" + limit; return hotKeyDetector.getValue(cacheKey, List.class, () -> { // 从数据库加载热门商品 return productRepository.findTopSellingProducts(limit); }); } /** * 更新商品信息,同时处理缓存 */ public Product updateProduct(Product product) { Product savedProduct = productRepository.save(product); // 清除所有相关缓存 String cacheKey = "product:" + product.getId(); if (hotKeyDetector.isHotKey(cacheKey)) { // 如果是热点KEY,清除分片缓存 hotKeyDetector.getShardedCacheManager().deleteValue(cacheKey); } else { // 常规缓存清除 redisTemplate.delete(cacheKey); } return savedProduct; } }

2.3 优缺点分析

优点

  • 有效分散单个热点KEY的访问压力
  • 不依赖于特定的缓存架构,可适用于多种缓存系统
  • 对客户端透明,无需修改调用方代码
  • 可动态识别和调整热点KEY的处理策略
  • 通过错峰过期时间,避免缓存雪崩问题

缺点

  • 增加写入开销,需同步更新多个缓存分片
  • 实现复杂度较高,需维护热点KEY检测和分片逻辑
  • 额外的内存占用(一个值存储多份)
  • 可能引入短暂的数据不一致窗口

适用场景

  • 特定KEY访问频率远高于其他KEY的场景
  • 读多写少的数据(商品详情、活动信息等)
  • 大型促销活动、爆款商品等可预见的流量突增场景
  • Redis集群面临单个KEY访问热点问题的系统

两种策略对比

特性分级缓存策略缓存分片策略
主要解决问题热点KEY访问延迟热点KEY单点压力
实现复杂度中等高
额外存储开销中等高
写入性能影响中等大
一致性保障最终一致最终一致
对原有代码改动中等大
适用热点类型通用热点超级热点

总结

在实际应用中,我们可以根据业务特点和系统架构选择合适的策略,甚至将多种策略组合使用,构建更加健壮的缓存体系。

无论选择哪种策略,都应当结合监控、预热、降级等最佳实践,才能真正发挥缓存的价值,保障系统在面对热点KEY时的性能和稳定性。

最后,缓存优化是一个持续改进的过程,随着业务发展和流量变化,需要不断调整和优化缓存策略,才能确保系统始终保持高性能和高可用性。

注:本文转载自juejin.cn的风象南的文章"https://juejin.cn/post/7496396598731620388"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

103
后端
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top