• 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缓存穿透问题修复

  • 相关阅读:
    linux 多进程并发服务__关于子进程回收的方法
    Qt中图元对象的多重集成
    Qmake: Q_OBJECT
    Qt Creator 启动失败 可能的解决办法
    Oracle 系统改变号SCN详解
    rman之创建恢复目录笔记
    Oracle 生成和显示执行计划的方法
    系统数据文件备份与恢复及只读数据文件备份与恢复
    没有备份、只有归档日志,如何恢复数据文件?
    ORACLE UNDO REDO查看
  • 原文地址:https://www.cnblogs.com/163yun/p/9455771.html
Copyright © 2020-2023  润新知