Перейти к содержанию

Cache

Модуль предоставляет AOP реализацию Cache'ей.

Implementations

Examples

Cache Implementation

Для начала требуется зарегистрировать типизированный Cache сигнатуру.

Интерфейс Cache должен наследоваться только от предоставляемых Kora'ой реализаций: CaffeineCache / RedisCache.

Для такого Cache будет сгенерирована и добавлена в граф реализация, ее можно будет использовать как Bean для внедрения зависимостей.

Single key

В случае если ключ кэша представляет собой 1 аргумент, то требуется зарегистрировать Cache с сигнатурой соответствующей типам ключа и значения.

public interface DummyCache extends CaffeineCache<String, String> { }

Composite key

В случае если ключ кэша представляет собой N аргументов, то требуется зарегистрировать Cache с использованием CacheKey интерфейса который отвечает за композитный ключ, где типизированной сигнатурой указываются N аргументов ключа.

Пример для Cache где композитный ключ состоит из 2 элементов:

public interface DummyCache extends CaffeineCache<CacheKey.Key2<String, BigDecimal>, String> { }

Для ключей из 3ых составляющих будет использоваться CacheKey.Key3 и так далее.

Config

Для регистрации Cache и указания конфига требуется проаннотировать аннотацией @Cache где аргумент value означает полный путь к конфига.

@Cache("my.full.cache.config.path")
public interface DummyCache extends CaffeineCache<CacheKey.Key2<String, BigDecimal>, String> { }

AOP

Допустим имеется класс:

@Component
public class CacheExample {

    public Long getValue(String arg1, BigDecimal arg2) {
        return ThreadLocalRandom.current().nextLong();
    }

    public Long putValue(String arg1, BigDecimal arg2) {
        return ThreadLocalRandom.current().nextLong();
    }

    public void evictValue(String arg1, BigDecimal arg2) {
        // do nothing
    }

    public void evictAll() {
        // do nothing
    }
}

Для него регистрируем соответствующий Cache:

@Cache("dummy")
public interface DummyCache extends CaffeineCache<CacheKey.Key2<String, BigDecimal>, String> { }

Get

Для кэширования и получения значения из кэша для метода getValue() следует проаннотировать его аннотацией @Cacheable.

Ключ для кэша составляет из аргументов метода, порядок аргументов имеет значение, в данном случае он будет составляться из значений arg1 и arg2.

@Component
public class CacheExample {

    @Cacheable(DummyCache.class)
    public Long getValue(String arg1, BigDecimal arg2) {
        return ThreadLocalRandom.current().nextLong();
    }

    public Long putValue(String arg1, BigDecimal arg2) {
        return ThreadLocalRandom.current().nextLong();
    }

    public void evictValue(String arg1, BigDecimal arg2) {
        // do nothing
    }

    public void evictAll() {
        // do nothing
    }
}

Put

Для добавления значений в кэш через метод putValue() следует проаннотировать его аннотацией @CachePut.

Метод проаннотированный @CachePut будет вызван и его значение положено во все кэш определенный в value.

Ключ для кэша составляет из аргументов метода, порядок аргументов имеет значение, в данном случае он будет составляться из значений arg1 и arg2.

@Component
public class CacheExample {

    @Cacheable(DummyCache.class)
    public Long getValue(String arg1, BigDecimal arg2) {
        return ThreadLocalRandom.current().nextLong();
    }

    @CachePut(DummyCache.class)
    public Long putValue(String arg1, BigDecimal arg2) {
        return ThreadLocalRandom.current().nextLong();
    }

    public void evictValue(String arg1, BigDecimal arg2) {
        // do nothing
    }

    public void evictAll() {
        // do nothing
    }
}

Invalidate

Для удаления значения по ключу из кэша через метод evictValue() следует проаннотировать его аннотацией @CacheInvalidate.

Метод проаннотированный @CacheInvalidate будет вызван и затем по ключу для кэша определенного в value будут удалены значения по ключу.

Ключ для кэша составляет из аргументов метода, порядок аргументов имеет значение, в данном случае он будет составляться из значений arg1 и arg2.

@Component
public class CacheExample {

    @Cacheable(DummyCache.class)
    public Long getValue(String arg1, BigDecimal arg2) {
        return ThreadLocalRandom.current().nextLong();
    }

    @CachePut(DummyCache.class)
    public Long putValue(String arg1, BigDecimal arg2) {
        return ThreadLocalRandom.current().nextLong();
    }

    @CacheInvalidate(DummyCache.class)
    public void evictValue(String arg1, BigDecimal arg2) {
        // do nothing
    }

    public void evictAll() {
        // do nothing
    }
}
InvalidateAll

Для удаления всех значений из кэша через метод evictAll() следует проаннотировать его аннотацией @CacheInvalidate и указать параметр invalidateAll = true.

Метод проаннотированный @CacheInvalidate будет вызван и затем будут удалены все из кэша определенного в value.

@Component
public class CacheExample {

    @Cacheable(DummyCache.class)
    public Long getValue(String arg1, BigDecimal arg2) {
        return ThreadLocalRandom.current().nextLong();
    }

    @CachePut(DummyCache.class)
    public Long putValue(String arg1, BigDecimal arg2) {
        return ThreadLocalRandom.current().nextLong();
    }

    @CacheInvalidate(DummyCache.class)
    public void evictValue(String arg1, BigDecimal arg2) {
        // do nothing
    }

    @CacheInvalidate(value = DummyCache.class, invalidateAll = true)
    public void evictAll() {
        // do nothing
    }
}

Reactor Mono

Все примеры были бы аналогичны и поддерживаются для Reactor Mono, пример такого класса:

@Component
public class CacheableTargetMono {

    @Cacheable(DummyCache.class)
    public Mono<Long> getValue(String arg1, BigDecimal arg2) {
        return Mono.just(ThreadLocalRandom.current().nextLong());
    }

    @CachePut(DummyCache.class)
    public Mono<Long> putValue(String arg1, BigDecimal arg2) {
        return Mono.just(ThreadLocalRandom.current().nextLong());
    }

    @CacheInvalidate(DummyCache.class)
    public Mono<Void> evictValue(String arg1, BigDecimal arg2) {
        return Mono.empty();
    }

    @CacheInvalidate(DummyCache.class, invalidateAll = true)
    public Mono<Void> evictAll() {
        return Mono.empty();
    }
}

Несколько кешей

В случае если у вас есть несколько кешей требуется использовать для базового и самого распространенного кеша модуль Default конфигурации, а кэш другой имплементации размечать в аннотации в поле tags, подставляя туда нужную имплементацию CacheManager.

Конфигурация будет выглядеть следующим образом:

@KoraApp
public interface ApplicationModules extends RedisCacheModule, CaffeineCacheModule { }

Реализации:

@Cache("my.caffeine.cache.config")
public interface CacheCaffeine extends CaffeineCache<String, String> { }

@Cache("my.redis.cache.config")
public interface CacheRedis extends RedisCache<String, String> { }

А сам класс с кешами так:

@Component
public class CacheableExample {

    @Cacheable(CacheCaffeine.class)
    @Cacheable(CacheRedis.class)
    public Mono<Long> getValueCaffeine(String arg1) {
        return Mono.just(ThreadLocalRandom.current().nextLong());
    }
}

Порядок вызова кэшей соответствует порядку в котором объявлены аннотации.

Порядок аргументов для ключа

В случае если метод принимает не нужные аргументы, которые не хочется использовать как часть ключа для кэша либо же порядок аргументов не соответствует порядку аргументов для ключа кэша, следует использовать атрибут аннотации parameters в котором определить какие именно аргументы использовать и в каком порядке.

В примере ниже ключ для кэша для обоих методов будет составлен идентично.

В случае если в каком либо месте у вас будет не соответствие ключа по сигнатуре аргументов метода, вы получите ошибку на этапе компиляции.

@Component
public class CacheExample {

    @Cacheable(DummyCache.class)
    public Long getValue(String arg1, BigDecimal arg2) {
        return ThreadLocalRandom.current().nextLong();
    }

    @Cacheable(value = DummyCache.class, parameters = {"arg1", "arg2"})
    public Long getValue(BigDecimal arg2, String arg3, String arg1) {
        return ThreadLocalRandom.current().nextLong();
    }
}

Конфигурации

В текущий момент реализованы две имплементации для кешей:

Caffeine

Описание конфигурации:

  • expireAfterWrite - Время по истечении которого значение для ключа будет удалено, отчитывается после добавления значения. (Optional)
  • expireAfterAccess - Время по истечении которого значение для ключа будет удалено, отчитывается после операции чтения. (Optional)
  • initialSize - Начальный размер кэша (помогает избежать ресазинга в случае активного набухания кэша) (Optional)
  • maximumSize - Максимальный размер кэша (При достижении границы или чуть ранее будет исключать из кэша наименее актуальные значения) (Optional)

Предоставляет модуль CaffeineCacheModule для использования Caffeine.

Пример конфигурации для my.cache.config кэша:

my {
  cache {
    config { 
      expireAfterWrite = 10s
      expireAfterAccess = 10s
      initialSize = 10
      maximumSize = 10
    }
  }
}

Dependencies

Java:

annotationProcessor "ru.tinkoff.kora:annotation-processors"
implementation "ru.tinkoff.kora:cache-caffeine"

Kotlin:

ksp "ru.tinkoff.kora:annotation-processors"
implementation "ru.tinkoff.kora:cache-caffeine"

Module:

@KoraApp
public interface ApplicationModules extends CaffeineCacheModule { }

Redis

Описание конфигурации подключения к Redis (Lettuce клиент):

  • uri - URI редиса
  • user - Юзер (Optional)
  • password - Пароль юзера (Optional)
  • database - Номер базы (Optional)
  • protocol - Протокол (Optional)
  • socketTimeout - Время таймаута коннекта (Optional)
  • commandTimeout - Время таймаута команды (Optional)

Описание конфигурации Redis Cache:

  • expireAfterWrite - При записи устанавливает время expiration
  • expireAfterAccess - При чтении устанавливает время expiration

Предоставляет модули RedisCacheModule для использования Redis.

Пример конфигурации для my.cache.config кэша.

Требуется обязательно сконфигурировать клиент Lettuce для доступа в Redis.

lettuce {
  uri = "redis://locahost:6379"
  user = "admin"
  password = "12345"
  database = 1
  protocol = "REP3"
  socketTimeout = 15s
  commandTimeout = 15s
}

my {
  cache {
   config {
      expireAfterWrite = 10s
      expireAfterAccess = 10s
    }
  }
}

Dependencies

Java:

annotationProcessor "ru.tinkoff.kora:annotation-processors"
implementation "ru.tinkoff.kora:cache-redis"

Kotlin:

ksp "ru.tinkoff.kora:annotation-processors"
implementation "ru.tinkoff.kora:cache-redis"

Module:

@KoraApp
public interface ApplicationModules extends RedisCacheModule { }

Loadable Cache

Библиотека предоставляет компонент для построения сущности, которая объединяет операции GET и PUT, без использования AOP аннотаций - LoadableCache

Кеширование блокирующих операций

Иногда у нас есть долгая операция, которую бы мы хотели кешировать и запускать на отдельном потоке исполнения при использовании асинхронного апи.
Для создания такого CacheLoader можно воспользоваться фабричным методом CacheLoader.blocking, он создаёт такой cache, который при вызове CacheLoader.loadAsync вызовет метод load, из примера ниже, на переданном ExecutorService. При этом вызов CacheLoader.load вызовет 'load' на том же потоке

@Module
interface ApplicationModules {
  default LoadableCache<String, String> someEntityLoadCache(DummyCache cache, SomeService someService, ExecutorService executor) {
    return cache.asLoadable(CacheLoader.blocking(someService::loadEntity, executor));
  }
}

@Component
class ServiceExample {
  private final LoadableCache<String, String> loadableCache;

  public OtherService(LoadableCache<SomeEntity> loadableCache) {
      this.loadableCache = loadableCache;
  }
}

Кеширование неблокирующих операций

По аналогии с методом CacheLoader.blocking, существуют фабричные методы CacheLoader.nonBlocking и CacheLoader.async, которые просто умеют кешировать результат переданной функции без Executor.

Supported Types

Поддерживаемые типы возвращаемых методов для AOP:

Java:

  • Обычный метод
  • Project Reactor (Mono)

Kotlin:

  • Обычный метод
  • Suspend