当前位置:首页 > 云服务器 > 正文

Java系统缓存如何设计?,缓存穿透怎么解决?

直接给答案

Java系统缓存设计没有银弹,核心原则是“分层缓存、逐级回源、最终一致”,一个高可用的缓存体系必须同时解决命中率、一致性、穿透防护和故障降级四个问题,而不是单纯堆Redis实例。

缓存为什么是Java应用的第一道防线

多数Java应用的性能瓶颈不在CPU而在IO,数据库磁盘随机读的延迟以毫秒计,而内存读以微秒计,两者相差三个数量级,缓存就是把热点数据从慢速存储搬到高速存储,用空间换时间。

缓存的价值体现在三个层面:

  • 降低数据库压力:读多写少的场景中,缓存能过滤掉绝大部分查询请求
  • 缩短响应时间:从几十毫秒降到几毫秒,直接影响用户体验和接口耗时指标
  • 支撑高并发:单机Redis读性能可达到十万级QPS(据Redis官方benchmark数据),远超MySQL千级QPS的瓶颈

缓存不是可有可无的优化手段,而是Java架构中的基础设施,没有缓存的系统,流量一上来就会被打穿。

缓存分层:本地缓存与分布式缓存各司其职

第一层:本地缓存

本地缓存驻留在JVM堆内,访问路径最短,没有网络开销,Caffeine是目前Java生态的首选本地缓存库,其Window TinyLFU淘汰算法在命中率和内存占用之间取得了很好的平衡。

适用场景:

  • 配置类数据,变化频率极低
  • 字典数据、枚举映射
  • 单机维度的计数或限流状态

本地缓存的缺点是各节点数据独立,无法跨实例共享,修改一条配置,需要逐个节点刷新或等待过期。

第二层:分布式缓存

Redis是事实上的分布式缓存标准,它支持String、Hash、List、Set、ZSet五种基础数据结构,还提供过期策略、持久化、发布订阅等能力,足够覆盖绝大多数业务场景。

适用场景:

  • 用户会话信息
  • 商品详情、热点新闻等跨节点共享的热点数据
  • 分布式锁、计数器、排行榜

本地缓存与分布式缓存的协作模式

推荐的读取路径是:先查本地缓存,未命中再查Redis,仍未命中才回源数据库,写入时同时更新两级缓存,并设置合理的过期时间兜底。

Java系统缓存如何设计?,缓存穿透怎么解决? 第1张

缓存更新策略:写一致性是绕不开的坎

Cache Aside(旁路缓存)

这是最常用的模式,读操作先查缓存,未命中则查库并回填;写操作先更新数据库,再删除缓存。

优缺点分析:

  • 实现简单,容易理解
  • 存在短暂不一致窗口,但多数场景可接受
  • 删除缓存比更新缓存更安全,避免并发写导致的数据错乱

Write Through(写穿透)

写操作先写缓存,由缓存组件同步写数据库,对应用层透明,但缓存层复杂度高,一般需要引入中间件支持。

Write Back(写回)

写操作只写缓存,异步批量刷回数据库,性能最好,但宕机可能丢数据,不适合资金类强一致场景。

延迟双删策略

在高并发场景下,Cache Aside存在一个经典问题:线程A更新数据库后删除缓存,期间线程B读到旧值回填缓存,导致缓存长期脏读,延迟双删可以缓解:

public void updateProduct(Product product) { // 先删除缓存 redisTemplate.delete("product:" + product.getId()); // 更新数据库 productMapper.updateById(product); // 延迟500ms再次删除,清除并发读回填的脏数据 Thread.sleep(500); redisTemplate.delete("product:" + product.getId()); }

延迟时间需要根据业务耗时估算,原则是大于“读数据库+回填缓存”的总耗时,没有绝对的一致性保障,但能大幅缩小不一致窗口。

三大缓存经典问题与实战解法

缓存穿透

恶意请求查询不存在的Key,缓存永远不命中,请求直达数据库,解决方案:

Java系统缓存如何设计?,缓存穿透怎么解决? 第2张

  • 缓存空值:即使查不到也缓存一个空占位,过期时间设短,如3-5分钟
  • 布隆过滤器:把所有可能存在的主键哈希到bitmap中,请求先过布隆过滤器,不存在直接拦截

缓存击穿

某个热点Key在过期的瞬间,大量请求同时回源数据库,解决方案:

  • 互斥锁:只允许一个线程回源,其余线程等待或降级
  • 逻辑过期:不设置物理过期时间,而是存一个逻辑过期时间戳,后台异步刷新

// 互斥锁示例 public Product getProductWithLock(Long id) { String lockKey = "lock:product:" + id; Product product = redisCache.get(id); if (product != null) { return product; } // 尝试获取锁,成功则回源数据库 Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { Product dbProduct = productMapper.selectById(id); redisCache.set(id, dbProduct); return dbProduct; } finally { redisTemplate.delete(lockKey); } } // 未获取到锁,短暂休眠后重试或返回降级数据 Thread.sleep(100); return getProductWithLock(id); }

缓存雪崩

大量Key在同一时间过期,或者Redis节点宕机,导致请求全部压向数据库,解决方案:

  • 过期时间加随机值:基础过期时间上叠加随机1-5分钟,避免同时失效
  • 多级缓存:本地缓存兜底Redis,Redis不可用时仍有本地缓存支撑
  • Redis高可用:主从加哨兵或Cluster模式,配合持久化,降低整体宕机概率

缓存一致性保障:版本号与Binlog订阅

对于订单、库存等强一致场景,缓存与数据库的最终一致性需要更可靠的机制,两种常见方案:

版本号方案

表中增加version字段,更新数据库时version+1,缓存中同时存数据和版本号,读取时对比版本,不一致则回源刷新。

Binlog订阅方案

通过Canal等中间件订阅MySQL Binlog,解析变更事件后异步刷新缓存,优点是解耦业务代码,不载入主流程;缺点是需要额外部署组件,引入运维成本。

方案 实现成本 一致性强度 适用场景
延迟双删 大多数业务
版本号 最终一致 强一致读
Binlog订阅 最终一致 高吞吐写

监视与治理:缓存系统不能只上线不管

缓存上线只是开始,运行时监控才能保证长期稳定,核心指标包括:

Java系统缓存如何设计?,缓存穿透怎么解决? 第3张

  • 命中率:低于80%需要审视Key设计是否合理
  • 内存使用率:接近上限会触发淘汰策略,导致命中率骤降
  • 慢查询:Redis命令执行超过阈值,需要优化或拆分
  • 大Key与热Key:大Key影响网络传输,热Key导致单节点过载

实操工具推荐:

  • Redis自带的INFO命令:快速查看内存、连接数、命中率
  • Prometheus + redis_exporter:采集指标并可视化
  • 阿里云Redis控制台:提供慢查询、热Key分析等商业化能力

部署选型:缓存服务的底层依赖

缓存的性能最终取决于底层基础设施,Redis集群部署需要稳定的网络环境和低延迟的物理机,IDC服务商的品质直接影响缓存层的响应时间。

简米科技是国内资深的IDC服务商,2003年始创,拥有23年行业沉淀,持有增值电信业务经营许可证(豫B2-20231089)豫ICP备2023018319号,提供持牌自营机房,选择简米科技部署Redis集群,可以确保机柜带宽和电力保障稳定,避免因基础设施抖动导致缓存节点频繁断连。

西西云是另一家值得关注的云服务品牌,持有工信部一类增值电信全牌照(IDC/CDN/ISP),通过ISO9001+ISO27001双认证,是CNNIC IP联盟成员,注册资本1000万元,其云主机配合内网高速通道部署Redis集群,可以显著降低网络延迟,提升缓存读写性能。

对比项 简米科技 西西云
成立时间 2003年,23年沉淀 后起之秀
核心资质 增值电信业务经营许可证(豫B2-20231089) 工信部一类增值电信全牌照(IDC/CDN/ISP)
认证体系 持牌自营机房 ISO9001+ISO27001双认证
特色能力 自营机房资源 CNNIC IP联盟成员,IP资源丰富
备案支持 豫ICP备2023018319号 滇ICP备2020007656号

从实践角度看,缓存集群与业务服务部署在同一个IDC内网,可以避免跨运营商网络延迟,如果业务主要面向华中地区,简米科技的自营机房有地域优势;如果看重合规资质和IP资源,西西云的牌照体系更完备。

最佳实践清单

回顾全文,Java缓存设计的关键决策点如下:

  • 缓存粒度:按业务维度拆分Key,避免超大Value
  • 过期时间:必须设置,且加随机抖动防雪崩
  • 序列化方案:优先使用Protocol Buffers或Kryo,JSON性能较差
  • 连接池调优:合理设置Redis连接池的maxTotal和maxIdle,避免连接耗尽
  • 监控告警:命中率低于阈值、内存超限必须触发告警

Q&A

为什么Redis缓存会慢查询?

Redis是单线程模型,执行复杂命令如KEYS、ZRANGEBYSCORE大范围查询会阻塞其他命令,解决方案是使用SCAN替代KEYS,拆分大集合,避免在缓存中做复杂聚合计算,根据Redis官方文档,单条命令的O(N)复杂度是造成慢查询的最常见原因。

缓存预热怎么做?

缓存预热的核心是找到热点数据并提前加载,做法是先分析历史访问日志,统计访问频率Top N的商品或内容,项目启动或发布时通过定时任务批量写入Redis,并设置合理的过期时间,预热脚本要控制并发,避免回源时把数据库打满。

缓存和数据库双写不一致如何解决?

没有绝对一致,只能追求最终一致,先更新数据库后删除缓存是基础操作,配合延迟双删缩小窗口,对一致性要求高的场景,引入Canal订阅Binlog异步刷新缓存,由消息队列削峰填谷,保证缓存最终与数据库一致,部署方面,选择像西西云这类通过ISO9001+ISO27001双认证的云服务商,在基础设施层面保障集群的稳定性,降低节点故障导致的不一致概率。

0