性能分析:动态代理失败导致缓存策略失效

本文详细记录了一次针对首页改版后的接口压力测试过程,从压测原因、分析到问题解决。在压测中发现接口响应时间过长,主要原因是缓存策略失效,导致大量SQL查询,使得线程阻塞。问题在于对于列表查询接口的本地缓存未生效,由于Spring AOP动态代理原理,this调用未被拦截增强。解决方案是调整缓存注解位置或通过获取代理对象执行方法以启用缓存。

压测原因

首页改版,所以要对首页进行压测,首页展示过程中涉及多个接口,需要进行压测。

压测分析

第一次排查

简单查看压测请求情况,初步观察日志可以额看到十个中有一到两个请求出现几百甚至上千毫秒的接口相应,排查TraceID之后发现为:接口调用到普通的测试环境和性能环境,导致部分请求走到测试环境,普通的测试环境只有1C2G,而生产环境的配置为40核64G/92G,所以相应结果较长也在意料之中。

同时排查过程中还发现部分接口调用链极短,这与线上的请求调用是不相符的,排查发现是请求头Header未设置数据导致很多请求被过滤到,所以在请求头上加入对应参数再次压测。

第二次排查

第二次压测过程,惨不忍睹,整体请求成功率只有76%左右,查询部分接口的响应时间很长,甚至直接溢出500,可以看到product服务的(ProductLimitFacade.queryProductLimitByXXX)由于接口请求时间过长,导致sofa线程池被打满,从而出现性能测试曲线出现急剧下降以及请求报错。

  • TPS断崖下跌
  • 请求时长由毫秒级\十几毫秒增长到上千毫秒,打满清空多次成正弦曲线状态

原本计划采用缓存来提升整体的查询性能,但是发现代码中已经使用了XXCache本地缓存,但是在看日志过程中还是发现存在大量的查询SQL(产品限额信息需要根据产品列表循环查询)。

排查SQL日志过程中,也可以看出有大量的SQL查询日志,导致接口相应太慢,大量线程阻塞在查询限额的方法出,所以目标锁定–代码的XXCache缓存使用不当,导致缓存策略未生效。
(XXCache:内部封装的一种缓存策略,根据参数值可以设置缓存在Redis或者缓存在本地中,可以参考GuavaCache的实现方式)[参考:GuavaCache的认识]

@XXCache(value = "productLimit", key = "productYieldDXXX_#{}_#{}_#{}")
第三次排查

现在已知是缓存策略失效的问题,第一反应为注解使用不当,导致缓存穿透。因为同时查询产品限额信息,以为是未配置allowNullValue的问题导致的,但是XXCache会默认会开启空缓存数据,所以也不是缓存穿透导致的。[参考:缓存穿透、缓存击穿、缓存雪崩区别和解决方案]

  • 缓存的正常流程为:先查询缓存然后查询数据库。查询缓存,若存在数据,则返回;如果缓存没有则查询数据库;如果数据库也没有返回空。
  • 缓存穿透:缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
  • 缓存雪崩:缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
第四次排查

采用Debug调试过程中,查看发现在XXCache打断点也无法阻断this.queryProductLimitByXXX()方法执行的调试线程。但是在另一处查询产品信息时却可以进入XXCache中阻断调试线程,清晰明了,就是该方法的XXCache并未拦截,导致本地缓存没有生效。

回归代码本源,接口调用的是列表查询接口,但是真正的本地缓存的是单个产品限额查询的接口,出发点很好,毕竟产品编号列表的排列组合太多,会不必要的浪费服务器宝贵的内存资源,问题就出在for循环的this调用,我们知道Spring的AOP核心思想是基于动态代理来实现的,如:

  • 基于接口的JDK的InvocationHandler动态代理
  • 基于子类继承的CGLib的MethodInterceptor动态代理。[Spring AOP 动态代理]

调用的列表查询接口会被代理对象增强,但是this调用不是代理对象执行的方法,而是原对象的原生方法,自然没法拦截增强,于是XXCache本地缓存就没有生效。至此, 问题排查完成。

问题解决

解决办法也很简单:一种是移除this调用改为获取代理对象(实现并注入ApplicationContextAware;然后使用该上下文来获取this服务的代理对象)来执行即可拦截增强,另一种将XXCache注解下移,将注解加到Manager上即可完成。


参考:

<think>好的,我现在需要帮助用户解决缓存失效和保持缓存一致性的问题。首先,我需要回顾一下用户提供的引用内容,看看有哪些现有的方法或方案已经被提到。用户引用了五个不同的资料,其中提到了缓存失效的情况、更新缓存策略缓存预热、分布式缓存的使用以及不同一致性需求下的解决方案。 首先,用户提到的缓存失效问题,引用[1]中描述了缓存自动失效时可能导致旧数据被重新加载到缓存中的情况。这可能发生在数据库更新后,缓存删除操作未能及时完成,导致另一个请求将旧数据再次写入缓存。这时候需要考虑如何避免这种竞态条件。 其次,引用[2]提到频繁更新缓存可能导致缓存利用率低下,特别是当数据变更频繁但缓存中的数据不被立即访问时。这提示我需要考虑缓存的更新策略,是采用更新缓存还是删除缓存(即写时更新或写时删除)。 引用[3]强调了为缓存设置过期时间的重要性,即使出现不一致,也能通过过期后的重新加载来恢复一致性。这是兜底方案,确保最终一致性。 引用[4]提到了缓存预热和分布式缓存的使用,预热可以在缓存过期前刷新数据,避免大量请求直接打到数据库。分布式缓存可能涉及到更复杂的同步机制,但这里用户的问题可能更偏向单系统缓存策略。 引用[5]则区分了不同一致性需求下的策略,低一致性可以用内存淘汰机制,高一致性需要主动更新加超时剔除。这提示需要根据业务场景选择合适的策略。 接下来,我需要综合这些信息,整理出处理缓存失效和保持缓存一致性的方案。可能需要包括以下几个步骤: 1. 选择合适的缓存更新策略:比如写时删除缓存(Cache-Aside模式中的策略),这样可以避免在写操作时直接更新缓存,减少不必要的缓存写入,提高利用率。但需要考虑删除缓存后,后续的读请求如何填充缓存。 2. 处理缓存失效时的并发问题:比如当缓存失效,多个请求同时访问数据库并试图更新缓存,可能导致数据库压力过大(缓存击穿)。可以采用互斥锁或缓存预热的方式来解决。 3. 设置合理的过期时间:即使出现不一致,也能在一定时间后自动纠正,保证最终一致性。 4. 使用延迟双删策略:在写数据库后,延迟一段时间再次删除缓存,以处理可能的旧数据被重新加载的情况,如引用[1]中的问题。 5. 引入消息队列或异步机制:确保缓存操作和数据库操作的顺序正确,避免因为网络延迟导致的数据不一致。 另外,需要考虑缓存穿透(查询不存在的数据)和缓存雪崩(大量缓存同时失效)的问题,但用户主要关注的是失效和不一致,所以可能重点放在如何同步更新和删除策略上。 可能还需要比较不同的缓存更新模式,比如Write-Through和Cache-Aside的区别,以及它们的优缺点。例如,Write-Through会在写数据库时同步更新缓存,这可能保证强一致性,但会影响写入性能,而Cache-Aside则在读时加载缓存,更适合高读取的场景。 此外,引用[5]提到的主动更新加超时剔除,可以作为高一致性需求的解决方案,比如在更新数据库后,主动更新缓存,同时设置较短的过期时间,这样即使主动更新失败,超时后仍能通过数据库重新加载正确数据。 最后,需要将这些点组织成清晰的步骤,可能分为几个部分,比如缓存更新策略、处理并发失效、兜底方案等,并给出具体的方法和示例,同时引用相关的引用资料作为支持。</think>### 处理缓存失效与保持缓存一致性的方案 #### 一、缓存失效的应对策略 1. **缓存预热机制** 在缓存过期前主动刷新数据,避免大量请求穿透到数据库。例如,通过定时任务或事件触发,提前加载热点数据到缓存[^4]。 ```python # 示例:基于时间触发的缓存预热 def preheat_cache(key, fetch_from_db): data = fetch_from_db(key) cache.set(key, data, timeout=300) # 设置5分钟过期 ``` 2. **互斥锁防击穿** 当缓存失效时,通过分布式锁控制只有一个线程查询数据库并回填缓存,其他线程等待锁释放后直接读取缓存。 ```python def get_data(key): data = cache.get(key) if data is None: if acquire_lock(key): # 获取分布式锁 data = fetch_from_db(key) cache.set(key, data) release_lock(key) else: time.sleep(0.1) return get_data(key) # 重试 return data ``` 3. **柔性过期时间** 设置两层过期时间: - 逻辑过期时间:业务层判断是否需刷新数据 - 物理过期时间:强制更新缓存 例如,物理过期设为10分钟,逻辑过期设为5分钟,超时后异步更新缓存[^5]。 --- #### 二、缓存一致性保障方案 1. **写策略选择** - **Cache-Aside模式(推荐)** 写操作直接更新数据库,并删除缓存。读操作先查缓存,未命中则读库并回填。 ```text 写流程: 1. 更新数据库 2. 删除缓存(非更新缓存)[^2] 读流程: 1. 查缓存 → 命中则返回 2. 未命中 → 查数据库 → 回填缓存 ``` - **Write-Through模式** 将缓存作为数据库代理,所有写操作同步更新缓存和数据库,适合强一致性场景,但写入性能较低。 2. **延迟双删策略** 针对极端情况下缓存与数据库不一致的问题: ```text 1. 删除缓存 2. 更新数据库 3. 等待短时间(如500ms)→ 再次删除缓存[^1] ``` 通过第二次删除清理可能的旧数据回填。 3. **异步监听Binlog** 使用数据库日志(如MySQL Binlog)监听数据变更,通过消息队列异步更新缓存,实现最终一致性。 ![Binlog监听流程](https://via.placeholder.com/600x200?text=DB→Binlog→MQ→Cache) --- #### 三、兜底与优化措施 1. **强制过期时间** 所有缓存必须设置过期时间,即使出现不一致也能通过过期自动恢复[^3]。 ```python # 示例:设置30分钟过期 cache.set("user:1001", user_data, timeout=1800) ``` 2. **降级策略分级** - 高一致性需求:主动更新 + 超时剔除(如金融交易数据) - 低一致性需求:仅依赖过期时间(如商品分类信息)[^5] 3. **监控与告警** 监控缓存命中率、数据库查询QPS等指标,当命中率低于阈值时触发预警。 --- #### 四、方案对比与选型 | 策略 | 一致性强度 | 实现复杂度 | 适用场景 | |---------------------|------------|------------|------------------------| | Cache-Aside + 超时 | 最终一致 | 低 | 大多数读多写少场景 | | Write-Through | 强一致 | 高 | 金融、医疗等关键领域 | | Binlog监听 | 最终一致 | 中 | 高并发写入场景 | ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狂点engineer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值