Spring缓存穿透问题修复
本文来自网易云社区。
本剧情纯属真实,犹如雷同实乃缘分。
发生
事情的发生在某天早上,天气怎样反正是忘了,只记得当时监控平台大量的数据库错误报警。 作为后端开发,当看到日志中大量的db连接获取失败,心情是复杂的。
看了下配置和实际连接数,竟然。。。没满。恩,可能是突发流量。然而没多久,一大波报警又袭来,感觉事情没那么简单。
常规措施无果,连续数次如此,看日志发现时间有点奇怪,都是间隔5分钟, 难道。。。缓存失效了? 看业务代码和缓存配置,很有可能。
业务代码表示
@Cacheable(value = "item_volume", key = "'item_' + #gid", unless = "#result == null") public Item queryiItem(long gid) { Optional<Item> optional = itemService.getItem(null, gid); return optional.orNull(); }
注: cache的实现用的是spring->
初步分析
没错,换配置item_volume过期时间5分钟,也就是说,当缓存过期后,此时如果有大量请求,那么这些请求都会因为缓存失效而请求数据库。 看起来情形是这样的:
如果假设成立,那就是spring在处理缓存的时候,如果没有命中,直接穿透执行实际操作(db查询),也就是说,中间是不加锁的。
这样就解释通了,但这是bug吗,还是spring认为是个feature, 这是个问题。
发展
Talk is cheap, show me the code. --linux之爹
关键是,code在哪。又得上套路了:套路: 既然是AOP,找找Advice。 最直接能想到,就是在spring中找所有Advice接口的继承树,然而数量太多,逐个寻找验证实在是耗时。
熟悉spring事务的同学应该能想到@Transactional的Advice是TransactionInterceptor,那么cache是否对应对一个CacheInterceptor呢。一看,还真有,那就好办了,而且看起就是要找的。
修改代码
顺着CacheInterceptor的invoke方法,定位到CacheAspectSupport.execute,看代码实现,确实没加锁,那就加个锁呗:
private Lock lock = new ReentrantLock(); //execute中部分代码 lock.lock(); try { result = findCachedItem(contexts.get(CacheableOperation.class)); if (result == null) { result = new SimpleValueWrapper(invokeOperation(invoker)); } collectPutRequests(contexts.get(CachePutOperation.class), result.get(), cachePutRequests); for (CachePutRequest cachePutRequest : cachePutRequests) { cachePutRequest.apply(result.get()); } processCacheEvicts(contexts.get(CacheEvictOperation.class), false, result.get()); } finally { lock.unlock(); }
其中,lock相关为新加部分。
高潮
代码是改好了,怎么生效呢。还得回头看看CacheInterceptor是如何注入的,也不难找到:
那就写个类 MyCacheAspectSupport.java.txt 来代替CacheInterceptor,然后注入。这里又会用到一些 套路:bean覆盖 、套路:beanname规则 等。
方法1:由于ProxyCachingConfiguration没有指定Advice的name,那就用默认的:
<bean id="errorHandler" class="org.springframework.cache.interceptor.SimpleCacheErrorHandler"/> <bean name="org.springframework.cache.interceptor.CacheInterceptor#0" class="org.springframework.cache.interceptor.MyCacheAspectSupport" p:errorHandler-ref="errorHandler" p:cacheManager-ref="cacheManager"/>
验证下,可以工作,然而总觉得哪里不对,恩,如果有多个bean。。。
方法2: 注意到ProxyCachingConfiguration中Advisor的name了吗,那就定义Advisor:
<bean id="errorHandler" class="org.springframework.cache.interceptor.SimpleCacheErrorHandler"/> <bean name="myCacheAdvice" class="org.springframework.cache.interceptor.MyCacheAspectSupport" p:errorHandler-ref="errorHandler" p:cacheManager-ref="cacheManager"/> <bean id="annotationCacheOperationSource" class="org.springframework.cache.annotation.AnnotationCacheOperationSource"/> <bean name="org.springframework.cache.config.internalCacheAdvisor" class="org.springframework.cache.interceptor.BeanFactoryCacheOperationSourceAdvisor" p:adviceBeanName="myCacheAdvice" p:cacheOperationSource-ref="annotationCacheOperationSource" />
验证下,可以工作。其实还可以有3:
方法3:实现BeanPostProcessor接口
@Autowired private MyCacheAspectSupport mycacheAdvice; @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (CacheInterceptor.class.isAssignableFrom(bean.getClass())) { return mycacheAdvice; } return bean; }
验证下,可以工作。
结尾
好了,问题解决,测了下性能也没太大下降(<1%,场景不同,仅供参考),终于又可以愉快的使用Cacheable等注解了。
spring和相关衍生拥有相当大的代码量,好在有很多套路都是通用的,利用这些套路能让我们解决问题事半功倍。
注: 文中spring版本为4.2.6.RELEASE
本文来自网易云社区,经作者王大喜授权发布。
- 关于spring redis 缓存配置错误的问题
- 开发过程中,利用Spring ServletContextAware 处理JS缓存问题。
- 修复升级myeclipseforspring-8.6插件后不能启动的问题
- 重建缓存文件修复Windows7缩略图错乱问题图解
- springboot+mybatis+redis 二级缓存问题实例详解
- windows 无法完成修复问题 无法清除 ARP 缓存
- spring boot整合shiro后,部分注解(Cache缓存、Transaction事务等)失效的问题
- 使用Spring Data Redis实现缓存遇到的一些问题
- 修复dubbo注解与spring aop冲突的问题
- memcache 缓存穿透的问题
- [bug]小程序弹出层滚动穿透问题修复
- Spring项目中添加Redis服务与缓存同步问题
- 从.Net到Java学习第七篇——SpringBoot Redis 缓存穿透
- Spring对iBatis封装导致的缓存问题
- 学习笔记:cache 和spring cache 技术---本地缓存-分布式缓存,缓存穿透,雪崩,和热点key的问题
- 关于spring service层的mybatis缓存问题,待解决
- Spring cloud config server 缓存问题
- jpa+spring jdbc混合作战引发的缓存问题
- 缓存穿透、缓存并发、缓存失效问题以及解决方案
- Redis关于缓存雪崩和缓存穿透等问题