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")@Overridepublic int identity(int x) {return x;}}
我知道,我知道緩存這種方法毫無意義。 但是我想衡量緩存層的開銷(在緩存命中期間)。 每個緩存配置將具有其自己的ApplicationContext
因為您不能在一個上下文中混合使用不同的代理模式:
public abstract class BaseConfig {@Beanpublic Calculator calculator() {return new PlainCalculator();}}@Configuration
class NoCachingConfig extends BaseConfig {}@Configuration
class ManualCachingConfig extends BaseConfig {@Bean@Overridepublic Calculator calculator() {return new CachingCalculatorDecorator(super.calculator());}
}@Configuration
abstract class CacheManagerConfig extends BaseConfig {@Beanpublic 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@Overridepublic Calculator calculator() {return new SpringInstrumentedCalculator();}}@Configuration
@EnableCaching(mode = AdviceMode.ASPECTJ)
class AspectJCustomAspect extends CacheManagerConfig {@Bean@Overridepublic 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;}@Overridepublic 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;}
}
SpringInstrumentedCalculator
和ManuallyInstrumentedCalculator
與PlainCalculator
完全相同,但是它們分別由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生成的子類,而aspectJCustom
是ManuallyInstrumentedCalculator
的實例,其中編織了我的自定義方面。
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 runtimeNoCaching 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