В Spring 3.1 появился отличный уровень
кэширования абстракции . Наконец, мы можем отказаться от всех доморощенных аспектов, декораторов и кода, загрязняющих нашу бизнес-логику, связанную с кэшированием. С тех пор мы можем просто комментировать тяжелые методы и позволить машине Spring и AOP выполнять свою работу:
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
"books"является именем кеша,
isbnпараметр становится ключом кеша, а возвращаемый
Bookобъект будет помещен под этот ключ. Значение имени кэша зависит от используемого менеджера кэша (EhCache, параллельная карта и т. Д.) — Spring упрощает подключение различных поставщиков кэширования. Но
этот пост не будет посвящен кешированию весной …
Некоторое время назад мой партнер по команде оптимизировал довольно низкоуровневый код и обнаружил возможность кеширования. Он быстро применил,
@Cacheableчтобы обнаружить, что код работает хуже, чем раньше. Он избавился от аннотации и сам осуществил кэширование, используя старое доброе
java.util.ConcurrentHashMap. Производительность была намного лучше. Он обвинял
@Cacheableи Spring AOP накладные расходы и сложность. Я не мог поверить, что слой кэширования может работать так плохо, пока мне самому не пришлось несколько раз отлаживать аспекты кэширования Spring (знаете, какая-то неприятная ошибка в моем коде, аннулирование кэша — одна из
двух самых сложных вещей в CS ). Что ж, код абстракции для кэширования гораздо сложнее, чем можно было бы ожидать (в конце концов, он просто
получает и
кладет !), Но это не обязательно означает, что он должен быть таким медленным?
В
науке мы не верим и не верим, мы измеряем и измеряем. Поэтому я написал тест, чтобы точно измерить накладные расходы
@Cacheableслой. Уровень абстракции кэширования в Spring реализован поверх Spring AOP, который в дальнейшем может быть реализован поверх прокси Java, сгенерированных CGLIB подклассов или инструментария AspectJ. Таким образом, я протестирую следующие конфигурации:
- никакого кэширования вообще — чтобы измерить, насколько быстро работает код без промежуточного уровня
- ручная обработка кэша
ConcurrentHashMapв бизнес-коде @Cacheableс CGLIB внедряет АОП@Cacheableсjava.lang.reflect.Proxyвнедрением АОП@Cacheableс использованием AspectJ для компиляции (как показывает аналогичный тест, CTW немного быстрее, чем LTW )- Самодельный аспект кэширования AspectJ — нечто среднее между ручным кэшированием в бизнес-коде и абстракцией Spring
Позвольте мне повторить: мы
не измеряем прирост производительности кэширования и
не сравниваем различные поставщики кэша. Вот почему наш метод тестирования настолько быстр, насколько это возможно, и я буду использовать самый простой
ConcurrentMapCacheManagerиз Spring. Итак, вот метод, о котором идет речь:
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;
}
}
SpringInstrumentedCalculatorи
ManuallyInstrumentedCalculatorони точно такие же, как и
PlainCalculatorони, но снабжены инструментами AspectJ Weaver во время компиляции с 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()миллионы раз и посмотрим, какая конфигурация кэширования работает лучше всего. Поскольку мы вызываем только
identity()с 16 различными аргументами, мы едва ли когда-либо касаемся самого метода, поскольку мы всегда получаем попадание в кеш. Хотите увидеть результаты?
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
наносекунды , мы говорим здесь о
3 циклах процессора на моем процессоре Intel® Core ™ 2 Duo T7300 @ 2,00 ГГц! Если это не убедит вас в том, что Java работает быстро, я не знаю, что будет. Но вернемся к нашему тесту.
Ручной кеширующий декоратор тоже довольно быстрый. Конечно, он медленнее на порядок по сравнению с чистым вызовом функции, но все равно невероятно быстр по сравнению со всеми
@Scheduledтестами. Мы видим падение на
3 порядка , с 1,8 нс до 1,5 мкс. Я особенно разочарован
@Cacheableподдержкой AspectJ. После того, как все аспекты кэширования прекомпилированы непосредственно в мою Java
.classфайл, я ожидал бы, что это будет намного быстрее по сравнению с динамическими прокси и CGLIB. Но, похоже, это не так. Все три метода Spring AOP похожи.
Самый большой сюрприз — мой собственный аспект AspectJ. Это даже быстрее, чем
CachingCalculatorDecorator! может это из-за полиморфного вызова в декораторе? Я настоятельно рекомендую вам
клонировать этот тест на GitHub и запустить его (
mvn clean testзанимает около 2 минут), чтобы сравнить ваши результаты.
Выводы
Вам может быть интересно, почему слой абстракции Spring такой медленный? Ну, во-первых, ознакомьтесь с базовой реализацией
CacheAspectSupport— это на самом деле довольно сложно. Во-вторых, это действительно так медленно? Посчитайте — вы обычно используете Spring в бизнес-приложениях, где узким местом являются база данных, сеть и внешние API. Какие задержки вы обычно видите? Миллисекунды? Десятки или сотни миллисекунд? Теперь добавьте накладные расходы в 2 мкс (в худшем случае). Для кэширования запросов к базе данных или вызовов REST это совершенно незначительно.
Неважно, какую технику вы выберете .
Но если вы кэшируете методы очень низкого уровня, близкие к металлу, такие как ресурсоемкие вычисления в памяти, уровень абстракции Spring может оказаться излишним. Суть: мера!
PS: оба
Тест и
содержание этой статьи в
формате Markdown находятся в свободном доступе.
