缓存设计需要注意的坑

本文探讨缓存设计中常见的四个问题:缓存与DB同步失败、数据版本冲突、并发查询导致DB压力增大及缓存穿透。提供解决方案,如重试机制、版本控制、计数器与锁以及默认值策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天讲的这个话题,我相信是众多工程师和团队的痛。从我刚开始工作,那时候构建本地缓存,到后续memcache, Redis的出现,到现在各种分布式集群的缓存,例如redis Cluster等产品的出现,缓存越来越发达和复杂了,缓存对我们的系统也越发重要,现在很难相信一个后端服务里没有缓存的存在。在这篇文章里,我会和大家分享一下过去踩到的缓存坑,然后试图给出一些解决方案,大家可以一起讨论,最终拿出更好的方法。由于篇幅有限,所以这里的缓存讨论,只局限于后端服务的缓存,并且不涉及具体的框架,对于H5,iOS和Android等前端缓存的讨论,会在以后的文章里呈现出来。

案例1,缓存和DB的同步更新不在同一个事务里并且没有重试补偿机制

为了减少系统间的依赖,不同系统的数据更新往往不放在同一个事务里,采用MQ来进行通信。大家可以看下图,后台系统CRM更新产品数据到DB,Product系统收到异步消息通知后,更新最新数据到缓存。这是一个最常见的缓存应用场景,我相信很多团队都是这样用的。在这个Case里容易出现的问题在于,如果批处理任务收到消息后服务crash掉了,缓存没有正常更新,就出现了与DB的数据不同步,前端系统一直不能读到最新数据,导致业务异常。

TIM截图20170714084654

解决方案:

1. 失败消息一定要建立一定时间间隔的重试机制

2. 系统要有缓存更新的报警机制,方便更新失败或者重试超时后,可以人工介入进行补偿。

案例2, 同一数据被1个以上的服务执行写操作,其中一个服务的缓存数据没有版本控制

这也是两个不同服务更新数据过程中很常见的情况,见下图,CRM系统更新了某个用户的Profile, 保存更新数据库后,通过MQ通知用户系统更新缓存,由于是异步更新延迟,在缓存更新前,用户系统收到前端的指令,读取了当前缓存里的用户数据,做了修改,并更新到DB中。出现的结果就是数据库里的CRM的更新被错误覆盖。

TIM截图20170714084646

解决方案:缓存里的数据有一个标志位可以作为更新数据库数据的依据(Update_time or Version), 如果缓存里数据时间与数据库时间不能匹配,意味着另外一个服务更新了该数据,那么就先从DB里读取最新数据版本,然后在新版本上提交数据。

案例3, 并发查询缓存中同一数据,如果缓存没命中,导致DB瞬时被打爆

做促销活动的时候,存在大量用户的并发访问某一个特定商品,该商品数据缓存失效,或者做了数据更改,但是对应缓存还没有更新,那么所有这些访问将同时直接被作用到DB上。

TIM截图20170714084637

解决方案:做一个计数器或者锁(没有特别复杂逻辑的话,可以直接用HashMap),如果发现某个KEY缓存没有命中,那么在计数器+1, 然后访问数据库,拿到结果更新缓存,清理掉计数器中的key。 在这个过程中,如果有第二个线程或者更多的线程需要访问这个KEY时,发现计数器的值>1 或者被加锁, 那么wait, 直到计数器清理掉,当然,这个技术器阈值是可以在配置文件里配置的,不一定是1。

案例4, 缓存没有设置默认值,被攻击,缓存一直保持在被“穿透”状态

这个情况,和案例3比较类似,都是缓存无法命中,但不一样的地方在于,数据的KEY值是无法控制的,所以没法简单的用计数器和锁来处理, 比方,被人为攻击,制造的大量的无效userID访问。

解决方案:所有没有在缓存的KEY,全部分配一个默认VALUE “UNKOWN-KEY” ,具体是什么情况下,将默认值分配给没有命中的KEY, 这个可以根据自己的业务系统来定,比方说,可以根据特定的IP段,或者没有命中的总次数等,然后我们就可以 决定是否继续访问DB还是直接返回默认值给前端,拒绝本次数据访问。这种做法的核心在于,每次数据访问,都会有缓存结果返回,根据系统的情况来决定是否要进一步访问DB。

总结,今天列举的这几个案例,归纳起来,可以总结为以下几点:

1. 保证缓存同步

2. 减少缓存并发

3. 杜绝缓存穿透

缓存与背后的DB是相互依存的关系,缓存系统的设计原则,就是将访问的异常处理或者压力尽可能的前置处理掉,将DB还原成它最初本来的存储功能。

原文链接: http://www.eeboard.com/news/bug/

<think>嗯,用户想了解Caffeine本地缓存的使用指南。从问题描述看,用户可能是个Java开发者,正在寻找传统ConcurrentMap或Guava缓存的替代方案。用户特意强调"本地缓存",说明需要进程内的高性能解决方案,而非分布式缓存。 参考站内资料,Caffeine确实是当前Java生态中性能最优秀的本地缓存库,尤其在SpringBoot 2.x后成为官方默认推荐。用户可能更关注实际应用而非理论,所以需要提供可直接落地的代码示例和配置参数。 用户没有说明具体使用场景,但提到"使用指南",说明需要从基础到进阶的完整路径。需要覆盖这几个核心点: 1. 基础依赖配置 - 特别是SpringBoot项目如何集成 2. 缓存策略的配置方式(大小/时间/引用) 3. 经典的三段式使用模板(查缓存-无则计算-回填) 4. 性能优化技巧(异步加载、刷新策略等) 特别注意两个技术细节需要强调: - 显式空值处理:用Optional避免缓存穿透 - 权重函数的使用场景:当缓存对象大小不一时特别有用 用户可能遇到的典型点: 1. 过期策略配置错误导致内存溢出 2. 未使用refreshAfterWrite造成缓存雪崩 3. 同步加载阻塞线程池 这些都需要在示例代码中通过注释预警。 对于高阶开发者,可以补充: 1. 与Spring Cache抽象层的整合 2. 缓存监控方案(比如通过micrometer暴露指标) 3. 持久化方案(但需提醒本地缓存的持久化不是主要设计目标) 最后需要强调内存管理,特别是当缓存大对象时建议配置权重评估器。所有代码示例必须用明确的泛型类型,避免用户复制时出现类型推断错误。</think>以下是根据官方文档整理的Caffeine本地缓存使用指南,包含核心配置和最佳实践: ### 一、基础配置 ```java // 创建缓存实例(基本配置) Cache<String, Object> cache = Caffeine.newBuilder() .maximumSize(10_000) // 基于条目数淘汰 .expireAfterWrite(5, TimeUnit.MINUTES) // 写入5分钟后过期 .weakKeys() // 使用弱引用存储键 .build(); // 读写操作 Object value = cache.get("key", k -> fetchFromDB(k)); // 自动加载 cache.put("key", new Object()); // 显式写入 cache.invalidate("key"); // 手动失效 ``` ### 二、高级配置策略 | **策略类型** | **配置方法** | **适用场景** | |--------------------|------------------------------------------|--------------------------| | 基于时间淘汰 | `.expireAfterAccess(10, TimeUnit.MINUTES)` | 高频读取数据 | | 基于权重淘汰 | `.maximumWeight(1_000_000)`<br>`.weigher((k,v) -> v.size())` | 缓存大小不均衡对象 | | 异步刷新 | `.refreshAfterWrite(1, TimeUnit.MINUTES)` | 热点数据自动更新 | | 软引用值 | `.softValues()` | 防止内存溢出 | ### 三、SpringBoot集成示例 1. 添加依赖: ```xml <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>3.1.8</version> </dependency> ``` 2. 配置缓存管理器: ```java @Bean public CacheManager cacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); manager.setCaffeine(Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(30, TimeUnit.MINUTES) .recordStats()); // 开启统计 return manager; } ``` 3. 业务层使用: ```java @Cacheable(value = "users", key = "#userId") public User getUser(String userId) { return userDao.findById(userId); // 自动缓存 } ``` ### 四、性能优化技巧 1. **异步加载**(减少阻塞): ```java AsyncLoadingCache<String, Data> asyncCache = Caffeine.newBuilder() .buildAsync(k -> fetchDataAsync(k)); ``` 2. **手动写入批处理**: ```java cache.putAll(Map.of("k1",v1, "k2",v2)); // 减少锁竞争 ``` 3. **统计监控**: ```java CacheStats stats = cache.stats(); System.out.println("命中率: " + stats.hitRate()); System.out.println("加载失败率: " + stats.loadFailureRate()); ``` ### 五、注意事项 1. **空值处理**:通过`Optional`避免缓存穿透 ```java cache.get(key, k -> Optional.ofNullable(db.get(k))); ``` 2. **资源清理**:定期调用`cache.cleanUp()`触发维护操作 3. **权重限制**:当缓存大型对象时,必须配置`weigher`函数 > 实测数据:在10万QPS场景下,Caffeine的读取延迟稳定在0.5ms内,显著优于Guava缓存[^2]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值