Кэширование
Модуль предоставляет типизированные кэши для хранения результатов вычислений и повторно используемых данных,
чтобы дорогостоящие операции не приходилось выполнять при каждом обращении. Кэш можно использовать декларативно через аннотации над методами
или императивно через внедряемый интерфейс, а в качестве хранилищ доступны локальный Caffeine и внешний Redis.
Локальный Caffeine полезен для быстрого внутрипроцессного хранения, а Redis подходит для общего кэша, используемого несколькими экземплярами приложения.
Весь контракт кэша синхронный: Cache<K, V> возвращает значения напрямую, а аспекты кэширования применяются к синхронным методам.
Если нужен пошаговый разбор перед справочным описанием, смотрите Кэш и Многоуровневый кэш.
Caffeine¶
Реализация на основе библиотеки Caffeine для кэша приложения в оперативной памяти.
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Конфигурация¶
Пример полной конфигурации кэша по пути mycache.config; параметры описаны в классе CaffeineCacheConfig (приведены примерные значения или значения по умолчанию):
mycache {
config {
enabled = true //(1)!
expireAfterWrite = "10s" //(2)!
expireAfterAccess = "10s" //(3)!
initialSize = 10 //(4)!
maximumSize = 100000 //(5)!
telemetry {
logging {
enabled = false //(6)!
}
metrics {
enabled = false //(7)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(8)!
tags = { //(9)!
"key1" = "value1"
}
}
tracing {
enabled = true //(10)!
attributes = { //(11)!
"key1" = "value1"
}
}
}
}
}
- Включает кэш; при значении
falseвсе операции кэша превращаются в пустые, аcomputeIfAbsentвсегда вызывает загрузчик (по умолчанию:true) - Время, после которого значение удаляется из кэша; отсчитывается после записи значения (по умолчанию не указано, опционально)
- Время, после которого значение удаляется из кэша; отсчитывается после чтения значения (по умолчанию не указано, опционально)
- Начальный размер кэша, помогает избежать расширения при быстром росте количества значений (по умолчанию не указано, опционально)
- Максимальный размер кэша; при достижении границы или чуть раньше вытесняются наименее актуальные значения (по умолчанию:
100000) - Включает логирование кэша (по умолчанию:
false) - Включает метрики кэша; также определяет, регистрируются ли стандартные метрики
MicrometerдляCaffeine(по умолчанию:false) - Настройка SLO для метрик, значения являются длительностями, а голые числа означают миллисекунды (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов для метрик (по умолчанию:
{}) - Включает трассировку кэша (по умолчанию:
true) - Настройка атрибутов для трассировки (по умолчанию:
{})
mycache:
config:
enabled: true #(1)!
expireAfterWrite: "10s" #(2)!
expireAfterAccess: "10s" #(3)!
initialSize: 10 #(4)!
maximumSize: 100000 #(5)!
telemetry:
logging:
enabled: false #(6)!
metrics:
enabled: false #(7)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(8)!
tags: #(9)!
key1: value1
tracing:
enabled: true #(10)!
attributes: #(11)!
key1: value1
- Включает кэш; при значении
falseвсе операции кэша превращаются в пустые, аcomputeIfAbsentвсегда вызывает загрузчик (по умолчанию:true) - Время, после которого значение удаляется из кэша; отсчитывается после записи значения (по умолчанию не указано, опционально)
- Время, после которого значение удаляется из кэша; отсчитывается после чтения значения (по умолчанию не указано, опционально)
- Начальный размер кэша, помогает избежать расширения при быстром росте количества значений (по умолчанию не указано, опционально)
- Максимальный размер кэша; при достижении границы или чуть раньше вытесняются наименее актуальные значения (по умолчанию:
100000) - Включает логирование кэша (по умолчанию:
false) - Включает метрики кэша; также определяет, регистрируются ли стандартные метрики
MicrometerдляCaffeine(по умолчанию:false) - Настройка SLO для метрик, значения являются длительностями, а голые числа означают миллисекунды (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов для метрик (по умолчанию:
{}) - Включает трассировку кэша (по умолчанию:
true) - Настройка атрибутов для трассировки (по умолчанию:
{})
Нижележащий кэш Caffeine создаётся фабрикой CaffeineCacheFactory, которая поставляется как @DefaultComponent.
Если требуется настройка сверх перечисленных параметров конфигурации (например, собственная политика вытеснения, слабые ссылки на ключи или свой весовщик),
зарегистрируйте собственный компонент CaffeineCacheFactory — он переопределит реализацию по умолчанию и позволит настроить билдер Caffeine напрямую.
@Component
public final class MyCaffeineCacheFactory implements CaffeineCacheFactory {
@Override
public <K, V> Cache<K, V> build(String name, CaffeineCacheConfig config) {
var builder = Caffeine.newBuilder().weakKeys();
if (config.expireAfterWrite() != null) {
builder.expireAfterWrite(config.expireAfterWrite());
}
builder.maximumSize(config.maximumSize());
return builder.build();
}
}
@Component
class MyCaffeineCacheFactory : CaffeineCacheFactory {
override fun <K, V> build(name: String, config: CaffeineCacheConfig): Cache<K, V> {
val builder = Caffeine.newBuilder().weakKeys()
config.expireAfterWrite()?.let { builder.expireAfterWrite(it) }
builder.maximumSize(config.maximumSize())
return builder.build()
}
}
Переопределение фабрики заменяет и регистрацию метрик по умолчанию, поэтому стандартные метрики Micrometer для Caffeine
придётся подключить вручную, если они по-прежнему нужны.
Redis¶
Реализация на основе базы данных в оперативной памяти Redis и драйвера подключения Lettuce.
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
LettuceRedisCacheModule — точка входа для кэша на Redis: он расширяет RedisCacheModule и LettuceModule
и предоставляет RedisCacheClient поверх общего подключения Lettuce.
Модуль RedisCacheModule из артефакта cache-redis-common не привязан к транспорту: он поставляет фабрику телеметрии кэша,
мапперы ключей и мапперы значений, но не предоставляет RedisCacheClient. Он нужен только тогда, когда используется
другой транспорт Redis и собственная реализация RedisCacheClient.
Конфигурация¶
Для подключения к Redis требуется отдельно настроить драйвер Lettuce.
Для всех кэшей на Redis используется одно подключение.
Основные параметры конфигурации Lettuce:
URIдля подключения кRedis(обязательный, без значения по умолчанию)- Таймаут выполнения команды (по умолчанию:
30s)
Полная конфигурация
Пример полной конфигурации драйвера Lettuce; параметры описаны в классе LettuceConfig (приведены примерные значения или значения по умолчанию):
lettuce {
uri = "redis://localhost:6379" //(1)!
user = "admin" //(2)!
password = "12345" //(3)!
database = 0 //(4)!
protocol = "RESP3" //(5)!
socketTimeout = "10s" //(6)!
commandTimeout = "30s" //(7)!
forceClusterClient = false //(8)!
ssl {
ciphers = [ "TLS_CHACHA20_POLY1305_SHA256" ] //(9)!
handshakeTimeout = "10s" //(10)!
}
telemetry {
logging {
enabled = false //(11)!
}
metrics {
enabled = false //(12)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(13)!
tags = { //(14)!
"key1" = "value1"
"key2" = "value2"
}
}
}
}
URIдля подключения кRedis(обязательный, без значения по умолчанию). Подключение к одному серверу:redis://localhost:6379. Подключение к нескольким серверам:redis://localhost:6379,localhost:6380. Подключение поSSL/TLS:rediss://localhost:6380.- Имя пользователя для подключения (по умолчанию не указано, опционально)
- Пароль пользователя для подключения (по умолчанию не указано, опционально)
- Номер базы данных для подключения (по умолчанию не указано, опционально)
- Протокол подключения, допустимы
RESP2илиRESP3(по умолчанию:RESP3) - Таймаут подключения сокета (по умолчанию:
10s) - Таймаут выполнения команды (по умолчанию:
30s) - Создавать кластерный клиент даже при единственном
URIподключения (по умолчанию:false) - Алгоритмы шифрования для защищённого соединения между клиентом и сервером (по умолчанию:
[]) - Таймаут установки защищённого соединения с сервером (по умолчанию:
10s) - Включает логирование драйвера (по умолчанию:
false) - Включает метрики драйвера (по умолчанию:
false) - Настройка SLO для метрик (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов для метрик (по умолчанию:
{})
lettuce:
uri: "redis://localhost:6379" #(1)!
user: "admin" #(2)!
password: "12345" #(3)!
database: 0 #(4)!
protocol: "RESP3" #(5)!
socketTimeout: "10s" #(6)!
commandTimeout: "30s" #(7)!
forceClusterClient: false #(8)!
ssl:
ciphers:
- "TLS_CHACHA20_POLY1305_SHA256" #(9)!
handshakeTimeout: "10s" #(10)!
telemetry:
logging:
enabled: false #(11)!
metrics:
enabled: false #(12)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(13)!
tags: #(14)!
key1: value1
key2: value2
URIдля подключения кRedis(обязательный, без значения по умолчанию). Подключение к одному серверу:redis://localhost:6379. Подключение к нескольким серверам:redis://localhost:6379,localhost:6380. Подключение поSSL/TLS:rediss://localhost:6380.- Имя пользователя для подключения (по умолчанию не указано, опционально)
- Пароль пользователя для подключения (по умолчанию не указано, опционально)
- Номер базы данных для подключения (по умолчанию не указано, опционально)
- Протокол подключения, допустимы
RESP2илиRESP3(по умолчанию:RESP3) - Таймаут подключения сокета (по умолчанию:
10s) - Таймаут выполнения команды (по умолчанию:
30s) - Создавать кластерный клиент даже при единственном
URIподключения (по умолчанию:false) - Алгоритмы шифрования для защищённого соединения между клиентом и сервером (по умолчанию:
[]) - Таймаут установки защищённого соединения с сервером (по умолчанию:
10s) - Включает логирование драйвера (по умолчанию:
false) - Включает метрики драйвера (по умолчанию:
false) - Настройка SLO для метрик (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов для метрик (по умолчанию:
{})
Если указан один URI и forceClusterClient равен false, создаётся обычный клиент RedisClient;
в остальных случаях для списка URI создаётся кластерный клиент RedisClusterClient.
Конфигурация кэша на Redis определяет поведение конкретного кэша.
Пример полной конфигурации кэша по пути mycache.config; параметры описаны в классе RedisCacheConfig (приведены примерные значения):
mycache {
config {
enabled = true //(1)!
keyPrefix = "mykey" //(2)!
expireAfterWrite = "10s" //(3)!
expireAfterAccess = "10s" //(4)!
telemetry {
logging {
enabled = false //(5)!
}
metrics {
enabled = false //(6)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(7)!
tags = { //(8)!
"key1" = "value1"
}
}
tracing {
enabled = true //(9)!
attributes = { //(10)!
"key1" = "value1"
}
}
}
}
}
- Включает кэш; при значении
falseвсе операции кэша превращаются в пустые, аcomputeIfAbsentвсегда вызывает загрузчик (по умолчанию:true) - Префикс ключа для конкретного кэша, используется во избежание коллизий ключей в одной базе данных
Redis; может быть пустой строкой, тогда ключи будут без префикса (обязательный, без значения по умолчанию) - Устанавливает время истечения значения при записи (по умолчанию не указано, опционально)
- Устанавливает время истечения значения при чтении (по умолчанию не указано, опционально)
- Включает логирование кэша (по умолчанию:
false) - Включает метрики кэша (по умолчанию:
false) - Настройка SLO для метрик (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов для метрик (по умолчанию:
{}) - Включает трассировку кэша (по умолчанию:
true) - Настройка атрибутов для трассировки (по умолчанию:
{})
mycache:
config:
enabled: true #(1)!
keyPrefix: "mykey" #(2)!
expireAfterWrite: "10s" #(3)!
expireAfterAccess: "10s" #(4)!
telemetry:
logging:
enabled: false #(5)!
metrics:
enabled: false #(6)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(7)!
tags: #(8)!
key1: value1
tracing:
enabled: true #(9)!
attributes: #(10)!
key1: value1
- Включает кэш; при значении
falseвсе операции кэша превращаются в пустые, аcomputeIfAbsentвсегда вызывает загрузчик (по умолчанию:true) - Префикс ключа для конкретного кэша, используется во избежание коллизий ключей в одной базе данных
Redis; может быть пустой строкой, тогда ключи будут без префикса (обязательный, без значения по умолчанию) - Устанавливает время истечения значения при записи (по умолчанию не указано, опционально)
- Устанавливает время истечения значения при чтении (по умолчанию не указано, опционально)
- Включает логирование кэша (по умолчанию:
false) - Включает метрики кэша (по умолчанию:
false) - Настройка SLO для метрик (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов для метрик (по умолчанию:
{}) - Включает трассировку кэша (по умолчанию:
true) - Настройка атрибутов для трассировки (по умолчанию:
{})
Параметр keyPrefix обязателен, но может быть пустой строкой. О пустом префиксе при старте выводится предупреждение,
поскольку в этом случае invalidateAll() не может просканировать префикс и выполняет FLUSHALL, очищая всю базу данных Redis.
Метрики модуля описаны в разделе Справочник метрик, а метрики драйвера —
в разделе Redis / Lettuce.
Собственная телеметрия кэша подключается регистрацией собственного компонента RedisCacheTelemetryFactory или CaffeineCacheTelemetryFactory,
который переопределяет @DefaultComponent из модуля. Если нужно сохранить поведение по умолчанию и изменить только его часть,
зарегистрируйте наследника DefaultRedisCacheLoggerFactory / DefaultRedisCacheMetricsFactory
(или их аналогов для Caffeine) — фабрика телеметрии по умолчанию подхватывает такие компоненты как опциональные зависимости.
Мапперы ключей и значений¶
Redis хранит ключи и значения как массивы байт, поэтому RedisCache использует два вида мапперов:
RedisCacheKeyMapper<K>превращает ключ кэша вbyte[].RedisCacheValueMapper<V>записывает значение кэша вbyte[]и читает его обратно.
Обычные ключи строятся через RedisCacheKeyMapper для типа ключа. Встроенные мапперы доступны для String, byte[],
чисел, BigInteger, BigDecimal, UUID, Boolean, Character, Instant, LocalDateTime, LocalDate, ZonedDateTime,
Duration, Period, Enum и Collection<T>, если маппер для T также доступен.
Для Enum используется toString(), поэтому его можно переопределить, если нужен другой формат ключа.
Для значений встроенные реализации RedisCacheValueMapper доступны для тех же простых типов, типов даты/времени, Enum и byte[].
Если требуется другое представление, зарегистрируйте собственный компонент RedisCacheValueMapper<V> или RedisCacheKeyMapper<K>.
Оба контракта являются компонентами графа, поэтому собственный маппер должен быть помечен @Component.
Самый частый случай — хранение объекта в виде JSON. Пометьте тип значения аннотацией @Json, чтобы Kora сгенерировала для него
JsonWriter и JsonReader, и укажите @Json также на аргументе типа значения в контракте кэша: встроенный маппер значений JSON
зарегистрирован под тегом @Json, поэтому внедряется только туда, где аргумент типа значения несёт этот тег.
Чтобы использовать для такого типа другое представление, уберите тег @Json с аргумента типа и зарегистрируйте
собственный компонент RedisCacheValueMapper<V>.
Для составного ключа на основе record или data class Kora генерирует отдельный RedisCacheKeyMapper для всего ключа как @DefaultComponent.
Он получает маппер для каждого поля, преобразует каждое поле в byte[] и объединяет части через RedisCacheKeyMapper.DELIMITER (:).
Порядок частей соответствует порядку компонентов record или свойств data class.
Если тип ключа не является record или data class, маппер не генерируется и RedisCacheKeyMapper для типа ключа нужно предоставить самостоятельно.
Для одиночного ключа встроенные реализации RedisCacheKeyMapper могут кодировать null специальным байтовым значением.
В составном ключе результат отображения каждого поля должен быть не-null: если собственный RedisCacheKeyMapper для поля вернёт null,
создание ключа завершится ошибкой. Для необязательных полей составного ключа собственный маппер должен явно кодировать null
стабильным байтовым значением.
Configurer¶
Клиент Lettuce собирается фабрикой LettuceFactory, и его можно донастроить перед созданием, зарегистрировав компоненты Configurer.
Configurer — это io.koraframework.common.Configurer, контракт с единственным методом T configure(T t).
Донастроить можно три билдера: DefaultClientResources.Builder для общих ресурсов клиента,
ClientOptions.Builder для обычного клиента и ClusterClientOptions.Builder для кластерного клиента.
@Component
public final class MyLettuceResourcesConfigurer implements Configurer<DefaultClientResources.Builder> {
@Override
public DefaultClientResources.Builder configure(DefaultClientResources.Builder builder) {
return builder.commandLatencyRecorder(CommandLatencyRecorder.disabled());
}
}
@Component
public final class MyLettuceOptionsConfigurer implements Configurer<ClientOptions.Builder> {
@Override
public ClientOptions.Builder configure(ClientOptions.Builder builder) {
return builder.autoReconnect(true);
}
}
@Component
class MyLettuceResourcesConfigurer : Configurer<DefaultClientResources.Builder> {
override fun configure(builder: DefaultClientResources.Builder): DefaultClientResources.Builder {
return builder.commandLatencyRecorder(CommandLatencyRecorder.disabled())
}
}
@Component
class MyLettuceOptionsConfigurer : Configurer<ClientOptions.Builder> {
override fun configure(builder: ClientOptions.Builder): ClientOptions.Builder {
return builder.autoReconnect(true)
}
}
Для сценариев за пределами типизированного кэша доступен для внедрения RedisCacheClient — низкоуровневый клиент, работающий с сырыми byte[]
(scan/get/mget/getex/set/mset/psetex/del/flushAll) поверх общего подключения Lettuce; именно на нём построен RedisCache.
Использование¶
Создание кэша потребует регистрации типизированного контракта @Cache.
Интерфейс контракта должен наследовать одну из реализаций Kora: CaffeineCache или RedisCache.
Для такого @Cache генерируется реализация и добавляется в граф, поэтому его можно внедрять как зависимость.
@Cache можно указывать только над интерфейсом, и этот интерфейс должен наследовать ровно один из двух контрактов —
одновременное наследование CaffeineCache и RedisCache приводит к ошибке компиляции.
Аргумент value в @Cache задаёт полный путь до конфигурации конкретного кэша.
Он указывает на объект конфигурации этого кэша, поэтому ключи конфигурации могут находиться как по вложенному пути вида mycache.config { ... },
так и плоско прямо по указанному пути вида my-cache { ... }, как это сделано в примерах проектов. Оба варианта корректны; выберите один и держите ключи конфигурации внутри него.
Путь должен начинаться с буквы, иначе генерация приложения завершится ошибкой.
Опциональные значения¶
Если метод в Java возвращает Optional<T>, аспект кэширования умеет работать с такой сигнатурой напрямую.
Сам тип значения кэша может быть как T, так и Optional<T>:
CaffeineCache<String, String>и методOptional<String> get(String key);CaffeineCache<String, Optional<String>>и методString get(String key);CaffeineCache<String, Optional<String>>и методOptional<String> get(String key).
Для @Cacheable это позволяет отличить отсутствие записи в кэше от результата метода, который тоже означает отсутствие данных.
Для @CachePut результат Optional<T> обрабатывается согласно типу значения кэша: если кэш хранит Optional<T>, сохраняется сам Optional,
а если кэш хранит T, сохраняется только присутствующее значение.
В Kotlin то же различие выражается через нullable-тип возвращаемого значения T?, обёртка Optional не используется.
Императивный подход¶
Кэши доступны для внедрения как зависимости по интерфейсу и могут использоваться совместно с декларативными операциями.
Cache предоставляет get(...), put(...), computeIfAbsent(...), invalidate(...), invalidateAll(),
а также пакетные варианты для коллекции ключей или карты значений.
Методы computeIfAbsent(...) сначала пытаются получить значение из кэша, а при промахе вызывают переданную функцию загрузки и сохраняют результат.
CaffeineCache дополнительно предоставляет getAll(), который возвращает все ключи и значения, находящиеся в памяти.
RedisCache дополнительно предоставляет методы ручного управления временем жизни, описанные ниже.
RedisCache никогда не пробрасывает транспортные ошибки вызывающему коду: неудачная операция фиксируется в телеметрии и деградирует
до промаха при чтении либо до молчаливого пропуска при записи, поэтому недоступность Redis не ломает бизнес-метод.
Композитный кэш через Cache.Builder¶
Если композитный кэш нужен в императивном коде, его можно собрать как фасад через Cache.Builder.
Порядок уровней определяется порядком добавления: обычно первым добавляют быстрый локальный кэш, например Caffeine,
а следом — более общий кэш, например Redis.
get(key)проверяет кэши по порядку и возвращает первое найденное значение.put(...),invalidate(...)иinvalidateAll()выполняются во всех кэшах.computeIfAbsent(...)проверяет кэши по порядку; если значение найдено на нижнем уровне, оно записывается в предыдущие уровни.- Если значения нет ни на одном уровне, вызывается функция загрузки и результат записывается во все кэши.
@Cache("mycache.caffeine.config")
public interface MyCaffeineCache extends CaffeineCache<String, String> { }
@Cache("mycache.redis.config")
public interface MyRedisCache extends RedisCache<String, String> { }
@KoraApp
public interface Application extends CaffeineCacheModule, LettuceRedisCacheModule {
default Cache<String, String> compositeCache(MyCaffeineCache caffeineCache, MyRedisCache redisCache) {
return Cache.builder(caffeineCache)
.addCache(redisCache)
.build();
}
}
@Cache("mycache.caffeine.config")
interface MyCaffeineCache : CaffeineCache<String, String>
@Cache("mycache.redis.config")
interface MyRedisCache : RedisCache<String, String>
@KoraApp
interface Application : CaffeineCacheModule, LettuceRedisCacheModule {
fun compositeCache(
caffeineCache: MyCaffeineCache,
redisCache: MyRedisCache,
): Cache<String, String> {
return Cache.builder(caffeineCache)
.addCache(redisCache)
.build()
}
}
Фасад, собранный через Cache.Builder, не поддерживает прямой get(Collection<K>) и выбрасывает на него UnsupportedOperationException.
Для пакетной загрузки используйте computeIfAbsent(Collection<K>, Function<Set<K>, Map<K, V>>).
Ручное управление временем жизни в Redis¶
Помимо общего набора методов Cache, RedisCache добавляет методы, переопределяющие настроенный expireAfterWrite для отдельной записи.
putExpireAfterWrite(key, value, Duration) и его пакетная перегрузка для Map применяют переданный Duration именно к этой записи
вместо значения из конфигурации. Эти методы доступны только для Redis.
@Cache("mycache.config")
public interface MyCache extends RedisCache<String, String> { }
@Component
public class SomeService {
private final MyCache cache;
public SomeService(MyCache cache) {
this.cache = cache;
}
public void cacheFor(String key, String value) {
cache.putExpireAfterWrite(key, value, Duration.ofMinutes(5));
}
}
Декларативный подход¶
Все примеры аспектов ниже предполагают реализацию кэша, приведённую выше.
Один метод несёт ровно один вид операции кэширования. Совмещение @Cacheable с @CachePut
либо @CacheInvalidate с @CacheInvalidateAll на одном методе приводит к ошибке компиляции.
Получение¶
Чтобы кэшировать и получать значение из кэша для метода get(), укажите над ним аннотацию @Cacheable.
Если значение найдено в кэше, исходный метод не вызывается; если значения нет, метод выполняется и результат сохраняется в кэш.
Ключ кэша строится из аргументов метода, порядок аргументов имеет значение. В данном случае он строится из arg1.
@Cacheable требует хотя бы один аргумент метода для ключа; метод без аргументов не пройдёт компиляцию.
Запись¶
Чтобы добавлять значения в кэш методом put(), укажите над ним аннотацию @CachePut.
Метод с @CachePut вызывается всегда, а его результат помещается в кэш, указанный в value.
Ключ кэша строится из аргументов метода, порядок аргументов имеет значение. В данном случае он строится из arg1.
Удаление¶
Чтобы удалить значение из кэша по ключу методом evict(), укажите над ним аннотацию @CacheInvalidate.
Метод с @CacheInvalidate вызывается, а затем значение удаляется по ключу из кэша, указанного в value.
Ключ кэша строится из аргументов метода, порядок аргументов имеет значение. В данном случае он строится из arg1.
Удаление всего¶
Чтобы удалить все значения из кэша методом evictAll(), укажите над ним аннотацию @CacheInvalidateAll.
Метод с @CacheInvalidateAll вызывается, а затем из кэша, указанного в value, удаляются все значения.
Ключ кэша при этом не строится, поэтому метод может принимать любые аргументы или не принимать их вовсе.
Для кэша на Redis метод invalidateAll() сканирует ключи с настроенным keyPrefix и удаляет их.
Если keyPrefix — пустая строка, вместо этого выполняется FLUSHALL для всей базы данных.
Режим выполнения¶
У каждой аннотации кэширования есть атрибут mode типа CacheMode с двумя значениями:
CacheMode.SYNC(по умолчанию) — запись в кэш происходит в вызывающем потоке до возврата из метода.CacheMode.ASYNC— запись в кэш отправляется в отдельныйExecutor, и метод возвращается, не дожидаясь её завершения.
ASYNC влияет только на записывающую часть операции: put для @Cacheable и @CachePut,
invalidate для @CacheInvalidate и invalidateAll для @CacheInvalidateAll.
Чтение кэша в @Cacheable остаётся синхронным, поскольку именно его результат определяет, будет ли вызван исходный метод.
Асинхронная операция выполняется на Executor, связанном тегом @Tag(CacheMode.class).
CacheCommonModule поставляет его как @DefaultComponent, который запускает виртуальный поток на каждую операцию и логирует неудачную операцию на уровне WARN.
Собственный компонент @Tag(CacheMode.class) Executor переопределяет реализацию по умолчанию.
Для CaffeineCache режим ASYNC игнорируется, поскольку запись в память не имеет смысла выносить в другой поток; процессор сообщает об этом предупреждением компиляции.
Композитный кэш¶
Если требуется использовать несколько кэшей, подключите нужные модули и укажите несколько аннотаций над методом.
Например, так можно объединить быстрый локальный уровень на Caffeine и общий уровень на Redis.
И сам аннотируемый класс:
Порядок обращения соответствует порядку аннотаций над методом сверху вниз.
Для @Cacheable это значит, что сначала проверяется верхний кэш; при промахе проверяется следующий,
а после того как значение найдено на нижнем уровне, оно записывается обратно во все ранее проверенные уровни.
Если значения нет ни на одном уровне, вызывается исходный метод и результат записывается в каждый перечисленный кэш.
Та же модель композиции работает для повторяемых @CachePut, @CacheInvalidate и @CacheInvalidateAll: метод вызывается один раз,
а затем результат записывается во все перечисленные кэши либо во всех перечисленных кэшах выполняется удаление.
Контейнерные аннотации @Cacheables, @CachePuts, @CacheInvalidates и @CacheInvalidateAlls также можно использовать, если такая форма удобнее.
Все повторяющиеся аннотации над одним методом должны объявлять одинаковый список args; разные списки аргументов ключа на одном методе приводят к ошибке компиляции.
Ключ¶
Если ключ кэша состоит из одного аргумента, зарегистрируйте Cache с сигнатурой, соответствующей типам ключа и значения.
Преобразование¶
Если аргумент нельзя использовать напрямую как ключ кэша, реализации потребуется маппер
с интерфейсом CacheKeyMapper. Если для ключа используются два аргумента, потребуется CacheKeyMapper2, если три — CacheKeyMapper3, и так далее до CacheKeyMapper9.
Более девяти аргументов ключа не поддерживается.
Такой маппер можно указать вручную через @Mapping. Маппер внедряется в сгенерированный аспект из графа зависимостей,
поэтому его класс должен быть зарегистрирован как компонент через @Component — включая вложенные классы.
Пример преобразования сложного объекта в простой ключ кэша:
@Component
public class SomeService {
public record UserContext(String userId, String traceId) { }
@Component
public static final class UserContextMapping implements CacheKeyMapper<String, UserContext> {
@Override
public String map(UserContext arg) {
return arg.userId();
}
}
@Mapping(UserContextMapping.class)
@Cacheable(MyCache.class)
public String get(UserContext context) {
// do something
}
}
@Component
open class SomeService {
data class UserContext(val userId: String, val traceId: String)
@Component
class UserContextMapping : CacheKeyMapper<String, UserContext> {
override fun map(arg: UserContext): String {
return arg.userId
}
}
@Mapping(UserContextMapping::class)
@Cacheable(MyCache::class)
open fun get(context: UserContext): String {
// do something
}
}
Если разным методам нужны разные мапперы с одинаковой сигнатурой, компоненты мапперов можно различить,
добавив @Tag рядом с @Mapping над методом и над самим компонентом маппера.
Составной ключ¶
Если ключ кэша состоит из нескольких аргументов, зарегистрируйте Cache с собственным классом,
описывающим этот ключ.
Пример для Cache, где составной ключ состоит из двух элементов:
Создайте собственный record, описывающий составной ключ.
Ключ строится вызовом публичного конструктора типа ключа, типы параметров которого по порядку соответствуют аргументам метода.
Если используется RedisCache, для составного ключа генерируется RedisCacheKeyMapper.
Он использует маппер для каждого поля ключа и ожидает, что результат отображения каждого поля будет не-null.
Встроенные мапперы могут кодировать null специальным значением, а собственные мапперы должны делать это явно.
Порядок аргументов¶
Если метод принимает аргументы, которые нужно исключить из составного ключа, либо порядок аргументов не совпадает
с порядком аргументов конструктора составного ключа, используйте атрибут args и укажите,
какие аргументы метода использовать и в каком порядке.
args задаёт полный набор аргументов метода, используемых для построения ключа. Каждое имя должно совпадать с именем аргумента метода,
а порядок должен соответствовать типу ключа: для одного аргумента — типу ключа Cache<K, V>; для составного ключа —
порядку аргументов конструктора record или data class.
Если имя не совпадает ни с одним аргументом метода, генерация приложения завершается ошибкой.
Если перечисленные аргументы не подходят ни под один конструктор типа ключа — отличается порядок или типы —
аспект переключается на CacheKeyMapperN для этого набора аргументов и ожидает его в графе зависимостей.
Сборка тогда упадёт на разрешении графа, пока такой маппер не зарегистрирован, поэтому либо исправьте порядок аргументов, либо предоставьте маппер через @Mapping.
Loadable Cache¶
Библиотека предоставляет компонент LoadableCache, который объединяет операции get и put без использования аспектов.
Он полезен, когда загрузкой значения нужно управлять вручную, сохранив стандартную логику: сначала проверить кэш,
а при промахе загрузить данные и сохранить их.
Cache.asLoadable(Function<Collection<K>, Map<K, V>>) создаёт LoadableCache вокруг пакетного загрузчика, а
Cache.asLoadableSimple(Function<K, V>) — вокруг загрузчика одного ключа.
LoadableCache предоставляет get(K) и get(Collection<K>).
@Cache("mycache.config")
public interface MyCache extends CaffeineCache<String, String> { }
@KoraApp
public interface Application extends CaffeineCacheModule {
default LoadableCache<String, String> loadableCache(MyCache cache, SomeService someService) {
return cache.asLoadable(someService::loadEntities);
}
}
То же самое применимо к RedisCache, поскольку оба контракта кэша наследуют общий интерфейс Cache.
Сигнатуры¶
Доступные сигнатуры для методов, поддерживаемые аннотациями:
Класс не должен быть final, чтобы работали аспекты.
Под T подразумевается тип возвращаемого значения.
T myMethod()Optional<T> myMethod()
@Cacheable и @CachePut требуют возвращаемого значения и не могут применяться к void.
@CacheInvalidate и @CacheInvalidateAll могут применяться к методам без результата.
Асинхронные и реактивные типы возвращаемого значения аспектами кэширования не поддерживаются:
CompletionStage<T>, Future<T>, Publisher<T>, Mono<T> и Flux<T> отклоняются на этапе компиляции.
Класс должен быть open, чтобы работали аспекты.
Под T подразумевается тип возвращаемого значения, либо T, либо T?, либо Unit.
myMethod(): T
@Cacheable и @CachePut требуют возвращаемого значения и не могут применяться к Unit.
@CacheInvalidate и @CacheInvalidateAll могут применяться к методам без результата.
Асинхронные и реактивные типы возвращаемого значения аспектами кэширования не поддерживаются:
CompletionStage<T>, Future<T> и Publisher<T> отклоняются на этапе компиляции.