Spring中的@Cacheable开销

Spring 3.1引入了很棒的缓存抽象层 。 最后,我们可以放弃所有本地化的方面,装饰器和污染我们与缓存相关的业务逻辑的代码。

从那时起,我们可以简单地注释重量级方法,并让Spring和AOP机械完成工作:

@Cacheable("books")
public Book findBook(ISBN isbn) {...}

"books"是一个缓存名称, isbn参数成为缓存键,返回的Book对象将放置在该键下。 缓存名称的含义取决于基础缓存管理器(EhCache,并发映射等)– Spring使插入不同的缓存提供程序变得容易。 但是这篇文章与Spring的缓存功能无关 ...

前段时间,我的队友正在优化底层代码,并发现了缓存的机会。 他Swift应用@Cacheable只是为了发现代码的性能比以前差。 他摆脱了注释,并使用了良好的旧java.util.ConcurrentHashMap手动实现了自己的缓存。 性能要好得多。 他指责@Cacheable和Spring AOP的开销和复杂性。 我不敢相信缓存层的性能如此之差,直到我不得不自己几次调试Spring缓存方面(代码中的一些讨厌的错误,缓存无效化是CS中最难两件事之一 )。 好吧,缓存抽象代码比人们期望的要复杂得多(毕竟只是获取放入 !),但这并不一定意味着它一定那么慢吗?

科学中,我们不相信和信任,我们进行衡量和基准测试。 因此,我写了一个基准来精确测量@Cacheable层的开销。 Spring中的缓存抽象层是在Spring AOP之上实现的,可以进一步在Java代理,CGLIB生成的子类或AspectJ工具的之上实现。 因此,我将测试以下配置:

  • 完全没有缓存–无需中间层即可测量代码的速度
  • 在业务代码中使用ConcurrentHashMap进行手动缓存处理
  • @Cacheable与实现AOP的CGLIB
  • @Cacheable与实现AOP的java.lang.reflect.Proxy
  • @Cacheable与AspectJ的编译时编织(如类似的基准测试所示, CTW比LTW稍快
  • 本地的AspectJ缓存方面–在业务代码中的手动缓存和Spring抽象之间的某种程度

让我重申一下:我们没有衡量缓存的性能提升,也没有比较各种缓存提供程序。 这就是我们的测试方法尽可能快的原因,我将使用Spring中最简单的ConcurrentMapCacheManager 。 所以这是一个有问题的方法:

public interface Calculator {
 
    int identity(int x);
 
}
 
public class PlainCalculator implements Calculator {
 
    @Cacheable("identity")
    @Override
    public int identity(int x) {
        return x;
    }
 
}

我知道,我知道缓存这种方法毫无意义。 但是我想衡量缓存层的开销(在缓存命中期间)。 每个缓存配置将具有其自己的ApplicationContext因为您不能在一个上下文中混合使用不同的代理模式:

public abstract class BaseConfig {
 
    @Bean
    public Calculator calculator() {
        return new PlainCalculator();
    }
 
}
 
@Configuration
class NoCachingConfig extends BaseConfig {}
 
@Configuration
class ManualCachingConfig extends BaseConfig {
    @Bean
    @Override
    public Calculator calculator() {
        return new CachingCalculatorDecorator(super.calculator());
    }
}
 
@Configuration
abstract class CacheManagerConfig extends BaseConfig {
 
    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager();
    }
 
}
 
@Configuration
@EnableCaching(proxyTargetClass = true)
class CacheableCglibConfig extends CacheManagerConfig {}
 
@Configuration
@EnableCaching(proxyTargetClass = false)
class CacheableJdkProxyConfig extends CacheManagerConfig {}
 
@Configuration
@EnableCaching(mode = AdviceMode.ASPECTJ)
class CacheableAspectJWeaving extends CacheManagerConfig {
 
    @Bean
    @Override
    public Calculator calculator() {
        return new SpringInstrumentedCalculator();
    }
 
}
 
@Configuration
@EnableCaching(mode = AdviceMode.ASPECTJ)
class AspectJCustomAspect extends CacheManagerConfig {
 
    @Bean
    @Override
    public Calculator calculator() {
        return new ManuallyInstrumentedCalculator();
    }
 
}

每个@Configuration类代表一个应用程序上下文。 CachingCalculatorDecorator是围绕真正的计算器进行装饰的装饰器(欢迎使用1990年代):

public class CachingCalculatorDecorator implements Calculator {
 
    private final Map<Integer, Integer> cache = new java.util.concurrent.ConcurrentHashMap<Integer, Integer>();
 
    private final Calculator target;
 
    public CachingCalculatorDecorator(Calculator target) {
        this.target = target;
    }
 
    @Override
    public int identity(int x) {
        final Integer existing = cache.get(x);
        if (existing != null) {
            return existing;
        }
        final int newValue = target.identity(x);
        cache.put(x, newValue);
        return newValue;
    }
}

SpringInstrumentedCalculatorManuallyInstrumentedCalculatorPlainCalculator完全相同,但是它们分别由AspectJ编译时织布器(带有Spring和自定义方面)进行检测。 我的自定义缓存方面如下所示:

public aspect ManualCachingAspect {
 
    private final Map<Integer, Integer> cache = new ConcurrentHashMap<Integer, Integer>();
 
    pointcut cacheMethodExecution(int x): execution(int com.blogspot.nurkiewicz.cacheable.calculator.ManuallyInstrumentedCalculator.identity(int)) && args(x);
 
    Object around(int x): cacheMethodExecution(x) {
        final Integer existing = cache.get(x);
        if (existing != null) {
            return existing;
        }
        final Object newValue = proceed(x);
        cache.put(x, (Integer)newValue);
        return newValue;
    }
 
}

经过所有准备工作,我们终于可以编写基准测试了。 首先,我启动所有应用程序上下文并获取Calculator实例。 每个实例都不同。 例如, noCaching是没有包装的PlainCalculator实例, cacheableCglib是CGLIB生成的子类,而aspectJCustomManuallyInstrumentedCalculator的实例,其中编织了我的自定义方面。

private final Calculator noCaching = fromSpringContext(NoCachingConfig.class);
private final Calculator manualCaching = fromSpringContext(ManualCachingConfig.class);
private final Calculator cacheableCglib = fromSpringContext(CacheableCglibConfig.class);
private final Calculator cacheableJdkProxy = fromSpringContext(CacheableJdkProxyConfig.class);
private final Calculator cacheableAspectJ = fromSpringContext(CacheableAspectJWeaving.class);
private final Calculator aspectJCustom = fromSpringContext(AspectJCustomAspect.class);
 
private static <T extends BaseConfig> Calculator fromSpringContext(Class<T> config) {
    return new AnnotationConfigApplicationContext(config).getBean(Calculator.class);
}

我将通过以下测试来练习每个Calculator实例。 附加的累加器是必需的,否则JVM可能会优化整个循环(!):

private int benchmarkWith(Calculator calculator, int reps) {
    int accum = 0;
    for (int i = 0; i < reps; ++i) {
        accum += calculator.identity(i % 16);
    }
    return accum;
}

这是完整的卡尺测试,没有讨论任何部件:

public class CacheableBenchmark extends SimpleBenchmark {
 
    //...
 
    public int timeNoCaching(int reps) {
        return benchmarkWith(noCaching, reps);
    }
 
    public int timeManualCaching(int reps) {
        return benchmarkWith(manualCaching, reps);
    }
 
    public int timeCacheableWithCglib(int reps) {
        return benchmarkWith(cacheableCglib, reps);
    }
 
    public int timeCacheableWithJdkProxy(int reps) {
        return benchmarkWith(cacheableJdkProxy, reps);
    }
 
    public int timeCacheableWithAspectJWeaving(int reps) {
        return benchmarkWith(cacheableAspectJ, reps);
    }
 
    public int timeAspectJCustom(int reps) {
        return benchmarkWith(aspectJCustom, reps);
    }
}

希望您仍在继续我们的实验。 现在,我们将执行Calculate.identity()数百万次,并查看哪种缓存配置效果最佳。 由于我们仅使用16个不同的参数调用identity() ,因此几乎永远不会碰到方法本身,因为我们总是会遇到缓存命中的情况。 想知道结果吗?

benchmark      ns linear runtime
                  NoCaching    1.77 =
              ManualCaching   23.84 =
         CacheableWithCglib 1576.42 ==============================
      CacheableWithJdkProxy 1551.03 =============================
CacheableWithAspectJWeaving 1514.83 ============================
              AspectJCustom   22.98 =


卡尺

解释

让我们一步一步走。 首先,在Java中调用方法相当快! 1.77 纳秒 ,我们在这里谈论的是我的Intel(R)Core(TM)2 Duo CPU T7300 @ 2.00GHz上的3个CPU周期 ! 如果这不能使您确信Java是快速的,那么我不知道会怎样。 但是回到我们的测试。

手工缓存装饰器也相当快。 当然,与纯函数调用相比,它慢了一个数量级,但与所有@Scheduled基准测试相比,它仍然非常快。 我们看到下降了3个数量级 ,从1.8 ns下降到1.5μs。 我对由AspectJ支持的@Cacheable感到特别失望。 将所有缓存方面直接预编译到我的Java .class文件中之后,我希望它比动态代理和CGLIB快得多。 但这似乎并非如此。 所有这三种Spring AOP技术都是相似的。

最大的惊喜是我自定义的AspectJ方面。 它甚至比CachingCalculatorDecorator还要快! 也许是由于装饰器中的多态调用? 我强烈建议您在GitHub上克隆此基准测试并运行它( mvn clean test ,大约需要2分钟)以比较您的结果。

结论

您可能想知道为什么Spring抽象层这么慢? 好吧,首先,请检查CacheAspectSupport的核心实现-它实际上非常复杂。 其次,真的那么慢吗? 算一下-您通常在数据库,网络和外部API成为瓶颈的业务应用程序中使用Spring。 您通常会看到什么延迟? 毫秒? 几百或几百毫秒? 现在添加2μs的开销(最坏的情况)。 对于缓存数据库查询或REST调用,这是完全可以忽略的。 选择哪种技术都没关系

但是,如果要在非常接近金属的地方缓存非常低级的方法,例如CPU密集型的内存中计算,那么Spring抽象层可能会显得过大。 底线:测量!

PS: Markdown格式的本文 基准内容均可免费获得。

参考:来自Java和社区博客的JCG合作伙伴 Tomasz Nurkiewicz提供的@ @ Spring的可缓存开销

翻译自: https://www.javacodegeeks.com/2013/01/cacheable-overhead-in-spring.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值