当前位置: 首页 > news >正文

【架构实战】热点数据架构:本地缓存+多级缓存

一、热点数据问题分析

在高并发系统中,某些数据的访问量远高于其他数据,这就是热点数据

热点数据的特征:

  • 访问频率极高(QPS可能是普通数据的100倍)
  • 数据量小(通常是单条或少量数据)
  • 变化频率低(相对稳定)

常见的热点数据场景:

场景数据访问量特点
秒杀商品库存极高瞬间高并发
热门商品商品详情很高持续高并发
用户排行排行榜定期更新
系统配置配置数据中等很少变化
热门用户用户信息相对稳定

热点数据的问题:

高并发请求 → 数据库连接池耗尽 → 其他请求无法获取连接 → 级联故障

解决方案:多级缓存架构

请求 → L1本地缓存 → L2分布式缓存 → L3数据库

二、多级缓存架构设计

1. 三层缓存的特点

层级存储特点适用场景
L1本地内存毫秒级,单机热点数据
L2Redis微秒级,分布式常用数据
L3数据库毫秒级,持久化所有数据

2. 缓存命中率

总请求数 = L1命中 + L2命中 + L3命中 理想情况: - L1命中率:80%(热点数据) - L2命中率:15%(常用数据) - L3命中率:5%(冷数据)

三、L1本地缓存实现

1. Caffeine缓存

基础配置:

@ConfigurationpublicclassCacheConfig{@BeanpublicLoadingCache<Long,Product>productCache(){returnCaffeine.newBuilder().maximumSize(10000)// 最多缓存10000条.expireAfterWrite(5,TimeUnit.MINUTES)// 5分钟过期.recordStats()// 记录统计信息.build(id->loadProductFromRedis(id));}@BeanpublicLoadingCache<Long,User>userCache(){returnCaffeine.newBuilder().maximumSize(50000).expireAfterAccess(10,TimeUnit.MINUTES)// 10分钟未访问则过期.recordStats().build(id->loadUserFromRedis(id));}}

使用示例:

@ServicepublicclassProductService{@AutowiredprivateLoadingCache<Long,Product>productCache;publicProductgetProduct(Longid){try{returnproductCache.get(id);}catch(Exceptione){log.error("获取商品缓存失败",e);returnloadProductFromRedis(id);}}// 缓存统计publicCacheStatsgetCacheStats(){returnproductCache.stats();}}

2. 缓存更新策略

策略1:主动更新(Push)

@ServicepublicclassProductService{@AutowiredprivateLoadingCache<Long,Product>productCache;@AutowiredprivateKafkaTemplatekafkaTemplate;publicvoidupdateProduct(Productproduct){// 1. 更新数据库productMapper.updateById(product);// 2. 清除本地缓存productCache.invalidate(product.getId());// 3. 发送消息,通知其他节点清除缓存kafkaTemplate.send("product:update",product.getId().toString());}@KafkaListener(topics="product:update")publicvoidonProductUpdate(StringproductId){productCache.invalidate(Long.parseLong(productId));log.info("清除本地缓存: {}",productId);}}

策略2:被动更新(Pull)

@ServicepublicclassProductService{@AutowiredprivateLoadingCache<Long,Product>productCache;@AutowiredprivateRedisTemplateredisTemplate;publicProductgetProduct(Longid){// 检查Redis中的版本号Stringversion=(String)redisTemplate.opsForValue().get("product:version:"+id);StringlocalVersion=(String)productCache.getIfPresent(id+":version");// 版本不一致,清除本地缓存if(!Objects.equals(version,localVersion)){productCache.invalidate(id);}returnproductCache.get(id);}}

3. 缓存预热

@ComponentpublicclassCacheWarmer{@AutowiredprivateLoadingCache<Long,Product>productCache;@AutowiredprivateProductMapperproductMapper;@PostConstructpublicvoidwarmUp(){log.info("开始缓存预热...");// 加载热点商品(销量TOP 1000)List<Product>hotProducts=productMapper.selectHotProducts(1000);for(Productproduct:hotProducts){productCache.put(product.getId(),product);}log.info("缓存预热完成,共{}条数据",hotProducts.size());}}

四、L2分布式缓存(Redis)

1. Redis缓存配置

@ConfigurationpublicclassRedisConfig{@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactoryconnectionFactory){RedisTemplate<String,Object>template=newRedisTemplate<>();template.setConnectionFactory(connectionFactory);// 使用Jackson序列化Jackson2JsonRedisSerializerjackson2JsonRedisSerializer=newJackson2JsonRedisSerializer(Object.class);ObjectMapperom=newObjectMapper();om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);StringRedisSerializerstringRedisSerializer=newStringRedisSerializer();// key采用String的序列化方式template.setKeySerializer(stringRedisSerializer);template.setHashKeySerializer(stringRedisSerializer);// value采用jackson的序列化方式template.setValueSerializer(jackson2JsonRedisSerializer);template.setHashValueSerializer(jackson2JsonRedisSerializer);template.afterPropertiesSet();returntemplate;}}

2. Redis缓存操作

@ServicepublicclassProductService{@AutowiredprivateRedisTemplate<String,Object>redisTemplate;privatestaticfinalStringPRODUCT_KEY_PREFIX="product:";privatestaticfinallongCACHE_TIMEOUT=30;// 30分钟publicProductgetProductFromRedis(Longid){Stringkey=PRODUCT_KEY_PREFIX+id;// 先查RedisProductproduct=(Product)redisTemplate.opsForValue().get(key);if(product!=null){returnproduct;}// Redis未命中,查数据库product=productMapper.selectById(id);if(product!=null){// 写入RedisredisTemplate.opsForValue().set(key,product,CACHE_TIMEOUT,TimeUnit.MINUTES);}else{// 缓存空值,防止穿透redisTemplate.opsForValue().set(key,"NULL",5,TimeUnit.MINUTES);}returnproduct;}}

3. 缓存穿透防护

问题:查询一个不存在的数据,每次都会穿透到数据库

解决方案1:缓存空值

publicProductgetProduct(Longid){Stringkey="product:"+id;Objectcached=redisTemplate.opsForValue().get(key);// 缓存中存在"NULL"标记if("NULL".equals(cached)){returnnull;}if(cached!=null){return(Product)cached;}// 查数据库Productproduct=productMapper.selectById(id);if(product==null){// 缓存空值redisTemplate.opsForValue().set(key,"NULL",5,TimeUnit.MINUTES);}else{redisTemplate.opsForValue().set(key,product,30,TimeUnit.MINUTES);}returnproduct;}

解决方案2:布隆过滤器

@ConfigurationpublicclassBloomFilterConfig{@BeanpublicBloomFilter<Long>productBloomFilter(){// 预期10万条数据,误判率1%BloomFilter<Long>filter=BloomFilter.create(Funnels.longFunnel(),100000,0.01);// 预热:加载所有存在的商品IDList<Long>productIds=productMapper.selectAllIds();productIds.forEach(filter::put);returnfilter;}}@ServicepublicclassProductService{@AutowiredprivateBloomFilter<Long>productBloomFilter;publicProductgetProduct(Longid){// 布隆过滤器判断是否可能存在if(!productBloomFilter.mightContain(id)){returnnull;// 一定不存在}// 继续查询缓存和数据库returngetProductFromCache(id);}}

4. 缓存雪崩防护

问题:大量缓存同时过期,导致请求全部打到数据库

解决方案:随机过期时间

publicvoidsetProductCache(Longid,Productproduct){Stringkey="product:"+id;// 基础过期时间 + 随机时间(0-5分钟)longtimeout=30+newRandom().nextInt(5);redisTemplate.opsForValue().set(key,product,timeout,TimeUnit.MINUTES);}

5. 缓存热key防护

问题:某个key的访问量极高,Redis单线程处理不过来

解决方案1:本地缓存

// 热key自动降级到本地缓存@ServicepublicclassProductService{privatefinalLoadingCache<Long,Product>hotKeyCache=Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(1,TimeUnit.MINUTES).build(id->getProductFromRedis(id));publicProductgetProduct(Longid){// 检查是否是热keyif(isHotKey(id)){returnhotKeyCache.get(id);}returngetProductFromRedis(id);}privatebooleanisHotKey(Longid){// 基于访问频率判断longcount=redisTemplate.opsForValue().increment("hotkey:count:"+id);returncount>1000;// 1秒内访问超过1000次}}

解决方案2:Redis集群

使用Redis Cluster分散热key的访问压力

五、缓存一致性保证

1. 更新流程

更新数据库 → 删除缓存 → 通知其他节点 为什么先更新数据库? - 如果先删除缓存,更新数据库失败,缓存已删除,会导致数据不一致 - 如果先更新数据库,删除缓存失败,下次查询会重新加载最新数据

2. 消息通知

@ServicepublicclassProductService{@AutowiredprivateKafkaTemplatekafkaTemplate;@TransactionalpublicvoidupdateProduct(Productproduct){// 1. 更新数据库productMapper.updateById(product);// 2. 删除本地缓存productCache.invalidate(product.getId());// 3. 删除Redis缓存redisTemplate.delete("product:"+product.getId());// 4. 发送消息,通知其他节点kafkaTemplate.send("product:update",newProductUpdateEvent(product.getId()));}@KafkaListener(topics="product:update")publicvoidonProductUpdate(ProductUpdateEventevent){// 清除本地缓存productCache.invalidate(event.getProductId());log.info("收到更新通知,清除缓存: {}",event.getProductId());}}

3. 缓存版本号

@ServicepublicclassProductService{publicProductgetProduct(Longid){// 获取缓存版本号Stringversion=(String)redisTemplate.opsForValue().get("product:version:"+id);// 从本地缓存获取Productcached=productCache.getIfPresent(id);// 版本一致,直接返回if(cached!=null&&Objects.equals(version,cached.getVersion())){returncached;}// 版本不一致或缓存未命中,重新加载Productproduct=getProductFromRedis(id);productCache.put(id,product);returnproduct;}}

六、监控与告警

1. 缓存监控指标

@ComponentpublicclassCacheMonitor{@AutowiredprivateLoadingCache<Long,Product>productCache;@Scheduled(fixedRate=60000)publicvoidmonitorCache(){CacheStatsstats=productCache.stats();log.info("缓存统计 - 命中率: {}%, 加载成功: {}, 加载失败: {}",String.format("%.2f",stats.hitRate()*100),stats.loadSuccessCount(),stats.loadFailureCount());// 发送到监控系统metricsService.record("cache.hit.rate",stats.hitRate());metricsService.record("cache.size",productCache.size());}}

2. 告警规则

groups:-name:cache_alertsrules:-alert:CacheHitRateLowexpr:cache_hit_rate < 0.7for:5mlabels:severity:warningannotations:summary:"缓存命中率低于70%"-alert:CacheLoadFailureexpr:cache_load_failure_total>100for:1mlabels:severity:criticalannotations:summary:"缓存加载失败次数过多"

七、总结

多级缓存架构有效应对热点数据:

  • L1本地缓存:毫秒级响应,减轻Redis压力
  • L2分布式缓存:数据一致性,支持分布式
  • L3数据库:数据持久化,最终一致性

实施要点:

  1. 合理设置缓存大小和过期时间
  2. 实现缓存穿透、雪崩、热key防护
  3. 保证缓存一致性
  4. 完善监控告警

思考题:你们系统有没有热点数据?如何处理的?


个人观点,仅供参考

  1. List item
http://www.jsqmd.com/news/575867/

相关文章:

  • 华为交换机流量统计配置避坑指南:为什么你的统计结果总是0?(GigabitEthernet接口实战)
  • Graphormer科研级部署:Supervisor自动重启+日志tail -f监控配置
  • ChatGPT_JCM版本控制策略:项目迭代与版本管理方法
  • 造相-Z-Image-Turbo与Vue.js构建AI绘图平台:前端工程化实践
  • iOS 15+ 越狱实战:A8-A11设备高效解锁与专业部署指南
  • Whisky实战指南:5大核心场景下的Windows程序跨平台运行解决方案
  • tweets_analyzer 进阶技巧:如何自定义过滤器和导出高级分析报告
  • Attu:Milvus可视化管理工具如何颠覆传统向量数据库操作流程?
  • Realistic Vision V5.1 惊艳作品集:基于卷积神经网络的人像摄影风格迁移
  • PLC与变频器通信的三种高效控制方案解析
  • ArduRemoteID:基于ESP32的无人机远程识别开源解决方案
  • Qwen3.5-2B效果展示:服装设计稿→识别风格/面料/剪裁→生成电商详情页文案
  • 生信小白也能搞定的实验室内部工具:手把手教你用SequenceServer+Docker搭建专属BLAST查询网站
  • 效率倍增:用快马AI一键生成互联网电商商品筛选组件代码
  • 2026年AI趋势监控平台能力榜:主流站点效能与覆盖度解析
  • 漫画脸描述生成保姆级教程:如何调试生成结果提升SD绘图匹配度
  • iOS 15+ 设备越狱实战指南:A8-A11 芯片全流程适配方案
  • B站视频收藏难?开源工具BilibiliDown通过多线程技术实现批量下载,效率提升85%
  • 红外图像处理实战:基于DifIISR的超分辨率重建保姆级教程(附CVPR 2025最新方法)
  • 实战指南:基于快马平台快速构建opencode协作应用界面
  • Lychee-rerank-mm模型服务网格化:基于Istio的微服务部署
  • Python原生AOT编译实战指南(2026 LTS版正式启用倒计时)
  • Graphormer部署案例:混合云架构下本地GPU+远程Web界面协同工作流
  • 3个颠覆性功能:重新定义你的Total War模组开发体验
  • 别再手动点确认了!Zabbix 7.0 告警自动推送到钉钉群,附完整脚本和消息模板
  • WRNavigationBar最佳实践:10个实用技巧提升你的iOS开发效率
  • 被百度网盘限速逼疯了?用这款开源工具让下载速度提升70倍
  • 从fishros案例到可运行项目:在快马平台快速构建视觉巡线机器人实战应用
  • 【2026年阿里巴巴春招- 4月1日-算法岗-第二题- 神奇的魔术】(题目+思路+JavaC++Python解析+在线测试)
  • Hugo Coder响应式设计解析:如何在所有设备上完美显示