Превратности
кэша
Как Redis спасает latency и ломает корректность в сервисах
Евгений Сулейманов
СТО | ProzyTech
1
2
Дисклеймер
Доклад не связан с компаниями, с которыми автор сотрудничает.
Все примеры кода, метрики, графики и аварийные сценарии - учебная модель и не предназначена для промышленной эксплуатации.
3
План расследования
4
План расследования
Система без кэша: честно, но медленно
5
План расследования
Redis как спасение: p95 падает
Система без кэша: честно, но медленно
6
План расследования
Redis как спасение: p95 падает
Неочевидные проблемы кэширования
Система без кэша: честно, но медленно
7
План расследования
Redis как спасение: p95 падает
Неочевидные проблемы кэширования
Кэш скрывает деградацию БД
Система без кэша: честно, но медленно
8
План расследования
Redis как спасение: p95 падает
Неочевидные проблемы кэширования
Кэш скрывает деградацию БД
Как кэшировать безопаснее
Система без кэша: честно, но медленно
9
План расследования
Redis как спасение: p95 падает
Неочевидные проблемы кэширования
Кэш скрывает деградацию БД
Как кэшировать безопаснее
Система без кэша: честно, но медленно
Сделаем выводы
10
11
Как устроен доклад?
12
Как устроен доклад?
видим боль в latency и БД
13
Как устроен доклад?
видим боль в latency и БД
добавляем Redis и радуемся
14
Как устроен доклад?
видим боль в latency и БД
добавляем Redis и радуемся
ловим новую проблему
15
Как устроен доклад?
видим боль в latency и БД
добавляем Redis и радуемся
ловим новую проблему
разбираем код и метрики
16
Как устроен доклад?
видим боль в latency и БД
добавляем Redis и радуемся
ловим новую проблему
разбираем код и метрики
голосуем: что делать?
17
Как устроен доклад?
видим боль в latency и БД
добавляем Redis и радуемся
ловим новую проблему
разбираем код и метрики
голосуем: что делать?
фиксируем компромиссы
18
Как устроен доклад?
Не лекция про Redis, а инцидент, где каждое “исправление” порождает новый вопрос.
видим боль в latency и БД
добавляем Redis и радуемся
ловим новую проблему
разбираем код и метрики
голосуем: что делать?
фиксируем компромиссы
Откройте
голосование
19
ГОЛОСОВАНИЕ
Дополнительные
ссылки
20
Дополнительные
ссылки
21
GitHub repo
Дополнительные
ссылки
22
Презентация
GitHub repo
Дополнительные
ссылки
23
Software Engineering
Презентация
GitHub repo
О себе
24
О себе
25
~15 лет опыта в разработке
О себе
26
~15 лет опыта в разработке
Прошу других писать код
О себе
27
~15 лет опыта в разработке
Прошу других писать код
Говорю слова
Акт 1. Без кэша все честно, но становится медленно
28
Один ендпоинт, одна БД, один понятный источник истины.
29
Типовой Spring-сервис до кэша
Client
30
Типовой Spring-сервис до кэша
Client
Spring MVC Controller
31
Типовой Spring-сервис до кэша
Client
Spring MVC Controller
Service
32
Типовой Spring-сервис до кэша
Client
Spring MVC Controller
Service
PostgreSQL�SourceOfTruth
33
Типовой Spring-сервис до кэша
Client
Spring MVC Controller
Service
HikariCP
PostgreSQL�SourceOfTruth
34
Типовой Spring-сервис до кэша
Источник истины один. Проблема: дорогие чтения и высокий p95/p99.
Client
Spring MVC Controller
Service
HikariCP
PostgreSQL�SourceOfTruth
35
@GetMapping("/api/products/{id}/card")
public ProductCardDto getProductCard(@PathVariable UUID id) {
return productCardService.getProductCard(id);
}
public ProductCardDto getProductCard(UUID id) {
return productRepository.loadProductCard(id)
.orElseThrow(ProductNotFoundException::new);
}
Код, который выглядит нормально
38
Инцидент начинается скучно
39
Инцидент начинается скучно
40
Инцидент начинается скучно
41
Инцидент начинается скучно
42
Инцидент начинается скучно
43
Инцидент начинается скучно
Интерактив #1: Что вы сделаете первым с медленным ендпоинтом для чтения?
44
A. Уводим чтение на реплику
C. Сначала фиксируем профиль чтения и допустимое устаревание
B. Кэшируем горячие ключи в Redis
D. Строим отдельную модель для чтения
45
Первое решение должно начинаться не с Redis
46
Первое решение должно начинаться не с Redis
Кэш можно добавить, но сначала нужно понять: что именно и как мы кэшируем
Практичный ответ
47
Первое решение должно начинаться не с Redis
Кэш можно добавить, но сначала нужно понять: что именно и как мы кэшируем
Практичный ответ
Почему
Redis отлично снижает latency. Но существует:
Что мы имеем?
48
Что получили
Система показывает фактическую стоимость чтения: DB QPS, Hikari, p95/p99.
Что мы имеем?
49
Что получили
Система показывает фактическую стоимость чтения: DB QPS, Hikari, p95/p99.
Что решили
Поняли, почему команда тянется к кэшу: latency действительно болит.
Что мы имеем?
50
Что получили
Система показывает фактическую стоимость чтения: DB QPS, Hikari, p95/p99.
Что решили
Что осталось
Поняли, почему команда тянется к кэшу: latency действительно болит.
Добавить Redis и увидеть, почему самая опасная фаза - когда он помог.
Акт 2. Redis как спасение
51
Сначала кэш действительно делает систему лучше.
52
Почему не просто добавить много реплик? | | декабрь |
Критерий | Реплика на чтение | Redis кэш |
Запрос на каждый вызов | да, просто на другой БД | нет при cache hit |
Устаревание | лаг репликации | TTL / инвалидация / политика устаревания |
Лучший сценарий | разнообразные SQL‑чтения | повторяющиеся “горячие” чтения |
Главный риск | лаг, конфликты, стоимость БД | устаревшие данные, холодный кэш, инвалидация |
53
@GetMapping("/api/products/{id}/card")
public ProductCardDto getProductCard(@PathVariable UUID id) {
return productCardService.getProductCard(id);
}
Изначальный код
54
@GetMapping("/api/products/{id}/card")
public ProductCardDto getProductCard(@PathVariable UUID id) {
return productCardService.getProductCard(id);
}
public ProductCardDto getProductCard(UUID id) {
return productRepository.loadProductCard(id)
.orElseThrow(ProductNotFoundException::new);
}
Изначальный код
55
@GetMapping("/api/products/{id}/card")
public ProductCardDto getProductCard(@PathVariable UUID id) {
return productCardService.getProductCard(id);
}
@Cacheable(cacheNames = "product-card", key = "#productId")
public ProductCardDto getProductCard(UUID id) {
return productRepository.loadProductCard(id)
.orElseThrow(ProductNotFoundException::new);
}
Изначальный код
Внедряем кэширование
56
@Cacheable
57
С этого момента появляется вторая модель данных
58
С этого момента появляется вторая модель данных
Client
59
С этого момента появляется вторая модель данных
Client
Spring MVC Controller
60
С этого момента появляется вторая модель данных
Client
Spring MVC Controller
Service
61
С этого момента появляется вторая модель данных
Client
Spring MVC Controller
Service
PostgreSQL�SoT
62
С этого момента появляется вторая модель данных
Client
Spring MVC Controller
Service
Redis
cache
PostgreSQL�SoT
63
С этого момента появляется вторая модель данных
Кэш - не деталь реализации. Это изменение модели консистентности.
Client
Spring MVC Controller
Service
Redis
cache
PostgreSQL�SoT
65
66
Самая опасная фаза: �когда все стало быстрее
Что мы имеем?
67
Что получили
Увидели фактическую пользу Redis: latency падает, БД дышит легче.
Что мы имеем?
68
Что получили
Увидели фактическую пользу Redis: latency падает, БД дышит легче.
Что решили
Зафиксировали: кэш - не враг, а мощный инструмент.
Что мы имеем?
69
Что получили
Увидели фактическую пользу Redis: latency падает, БД дышит легче.
Что решили
Что осталось
Зафиксировали: кэш - не враг, а мощный инструмент.
Показать, где он начинает врать и почему hit ratio не спасает корректность.
Акт 3. Неочевидные проблемы кэширования
70
Про что мы можем “забыть” при кэшировании
Основные классы проблем
71
Основные классы проблем
Устаревшие данные
72
Основные классы проблем
Проблемы негативного кэширования
Устаревшие данные
73
Основные классы проблем
Проблемы негативного кэширования
Права доступа
Устаревшие данные
74
Основные классы проблем
Проблемы негативного кэширования
Права доступа
Изменение нагрузочного профиля
Устаревшие данные
75
Основные классы проблем
Проблемы негативного кэширования
Права доступа
Изменение нагрузочного профиля
Кэш усложняет релизы
Устаревшие данные
76
Основные классы проблем
Проблемы негативного кэширования
Права доступа
Изменение нагрузочного профиля
Кэш усложняет релизы
Устаревшие данные
Local cache + Redis
77
Устаревшие данные
78
79
Устаревшие данные:
профиль изменился, кэш старый
12:00 read card Redis version=1
80
Устаревшие данные:
профиль изменился, кэш старый
12:00 read card Redis version=1
12:01 DB update version=2
81
Устаревшие данные:
профиль изменился, кэш старый
12:00 read card Redis version=1
12:01 DB update version=2
12:01 Redis old value version=1
82
Устаревшие данные:
профиль изменился, кэш старый
12:00 read card Redis version=1
12:01 DB update version=2
12:01 Redis old value version=1
12:02 read old card version=1
83
Устаревшие данные:
профиль изменился, кэш старый
Источников истины два. Проблема: в кэше устаревшие данные
12:00 read card Redis version=1
12:01 DB update version=2
12:01 Redis old value version=1
12:02 read old card version=1
84
@Transactional
public void updateProduct(UUID productId, UpdateProductRequest request) {
productRepository.updateProduct(productId, request);
}
UPDATE без инвалидации
85
@Transactional
public void updateProduct(UUID productId, UpdateProductRequest request) {
productRepository.updateProduct(productId, request);
}
// Redis все еще содержит product-card::{productId}
// с предыдущей версией DTO
UPDATE без инвалидации
86
@Transactional
public void updateProduct(UUID productId, UpdateProductRequest request) {
productRepository.updateProduct(productId, request);
}
// Redis все еще содержит product-card::{productId}
// с предыдущей версией DTO
UPDATE без инвалидации
Запись прошла.
Кэш не знает, что мир изменился.
Интерактив #2: после PATCH приходит старый DTO. Как чинить путь записи?
87
A. `@CachePut` после апдейта
С. Проверка версии перед чтением из кэша
B. Evict после коммита + доставка инвалидации
D. Двойное удаление (из БД и кэша) с задержкой
88
@Transactional
public void updateProduct(
UUID productId,
UpdateProductRequest request
) {
productRepository.updateProduct(productId, request);
}
Добавим @CacheEvict - и это еще не конец
89
@CacheEvict(
cacheNames = "product-card",
key = "#productId",
beforeInvocation = false
)
@Transactional
public void updateProduct(
UUID productId,
UpdateProductRequest request
) {
productRepository.updateProduct(productId, request);
}
Добавим @CacheEvict - и это еще не конец
90
@CacheEvict(
cacheNames = "product-card",
key = "#productId",
beforeInvocation = false
)
@Transactional
public void updateProduct(
UUID productId,
UpdateProductRequest request
) {
productRepository.updateProduct(productId, request);
}
Добавим @CacheEvict - и это еще не конец
Теперь вопрос не “есть eviction?”, а “когда и кто гарантирует его корректность?”.
91
Cache-aside ломается не в “happy path”
Надежность cache-aside определяется не чтением из Redis, а тем, как система переживает update, commit, eviction и события инвалидации.
PATCH update
DB commit OK
Redis evict timeout
HTTP 200 OK
next GET returns old DTO
92
@Bean
@Primary
CacheManager cacheManager(
@Qualifier("redisCacheManagerDelegate")
RedisCacheManager delegate
) {
return new TransactionAwareCacheManagerProxy(delegate);
}
Transaction-aware cache manager
93
@Transactional
public void updateProduct(
UUID productId,
UpdateProductRequest request
) {
long newVersion =
productRepository.updateProduct(productId, request);
cacheInvalidationOutboxRepository.save(
CacheInvalidationEvent.productCard(
productId,
newVersion
)
);
}
@Transactional outbox для гарантии
Негативное кэширование
94
95
Негативное кэширование
12:00
GET /products/ABC
Redis MISS
96
Негативное кэширование
12:00
GET /products/ABC
Redis MISS
DB: NOT_FOUND
Redis SET
ABC = NOT_FOUND
97
Негативное кэширование
12:00
GET /products/ABC
Redis MISS
DB: NOT_FOUND
Redis SET
ABC = NOT_FOUND
12:02
Redis HIT
ABC = NOT_FOUND
98
Негативное кэширование
12:00
GET /products/ABC
Redis MISS
DB: NOT_FOUND
Redis SET
ABC = NOT_FOUND
12:02
Redis HIT
ABC = NOT_FOUND
99
Негативное кэширование
12:00
GET /products/ABC
Redis MISS
DB: NOT_FOUND
Redis SET
ABC = NOT_FOUND
12:02
Redis HIT
ABC = NOT_FOUND
100
Негативное кэширование
12:00
GET /products/ABC
Redis MISS
DB: NOT_FOUND
Redis SET
ABC = NOT_FOUND
12:02
Redis HIT
ABC = NOT_FOUND
101
Негативное кэширование: отсутствие данных устаревает
12:00
GET /products/ABC
Redis MISS
102
Негативное кэширование: отсутствие данных устаревает
12:00
GET /products/ABC
Redis MISS
DB: NOT_FOUND
Redis SET
ABC = NOT_FOUND
103
Негативное кэширование: отсутствие данных устаревает
12:00
GET /products/ABC
Redis MISS
DB: NOT_FOUND
Redis SET
ABC = NOT_FOUND
12:01
POST /products/ABC
DB: created
104
Негативное кэширование: отсутствие данных устаревает
12:00
GET /products/ABC
Redis MISS
DB: NOT_FOUND
Redis SET
ABC = NOT_FOUND
12:01
POST /products/ABC
DB: created
12:02
Redis HIT
ABC = NOT_FOUND
105
Негативное кэширование: отсутствие данных устаревает
12:00
GET /products/ABC
Redis MISS
DB: NOT_FOUND
Redis SET
ABC = NOT_FOUND
12:01
POST /products/ABC
DB: created
12:02
Redis HIT
ABC = NOT_FOUND
106
Что делать с NOT_FOUND? | | декабрь |
Вариант | Что получаем | Где платим |
Не кэшировать отсутствие | Всегда актуальный ответ | Каждый MISS идет в БД |
Короткий TTL | Снижает нагрузку и ограничивает stale-окно | После создания еще возможен временный 404 |
Инвалидировать при создании | Новый объект становится видим почти сразу | Логика записи должна знать ключ негативного кэширования, возможны ошибки при инвалидации |
TTL + инвалидация | Защита от повторных MISS + быстрое появление объекта | Больше логики и сценариев отказа |
Кэширование прав доступа
107
108
Крайне неприятный баг: права доступа
109
Крайне неприятный баг: права доступа
110
public AccountDto getAccount(UUID accountId) {
authorizationService.checkAccess(accountId);
return accountRepository.findAccount(accountId);
}
Ключ кэширования не учитывает контекст доступа
111
@Cacheable(cacheNames = "account-view", key = "#accountId")
public AccountDto getAccount(UUID accountId) {
authorizationService.checkAccess(accountId);
return accountRepository.findAccount(accountId);
}
Ключ кэширования не учитывает контекст доступа
112
@Cacheable(cacheNames = "account-view", key = "#accountId")
public AccountDto getAccount(UUID userId, UUID tenantId, UUID accountId) {
authorizationService.checkAccess(userId, tenantId, accountId);
return accountRepository.findAccount(accountId);
}
Ключ кэширования не учитывает контекст доступа
113
@Cacheable(cacheNames = "account-view", key = "#accountId")
public AccountDto getAccount(UUID userId, UUID tenantId, UUID accountId) {
authorizationService.checkAccess(userId, tenantId, accountId);
return accountRepository.findAccount(accountId);
}
Ключ кэширования не учитывает контекст доступа
Если Redis вернул HIT, метод не вызван. Значит checkAccess(...) тоже не вызван.
Интерактив #3: права доступа. Где должна проходить граница безопасности?
114
C. Кэшировать разрешения отдельно
D. Не кэшировать защищенные данные совсем.
A. Добавить user/tenant в ключ кэширования
B. Проверять доступ до входа в кэшированный метод
115
@Cacheable(
cacheNames = "account-view",
key = "#tenantId + ':' + #userId + ':' + #accountId"
)
public AccountDto getAccount(UUID userId, UUID tenantId, UUID accountId) { ... }
Безопасный ключ дороже по кардинальности
116
@Cacheable(
cacheNames = "account-view",
key = "#tenantId + ':' + #userId + ':' + #accountId"
)
public AccountDto getAccount(UUID userId, UUID tenantId, UUID accountId) { ... }
Безопасный ключ дороже по кардинальности
Иногда лучше кэшировать данные отдельно, а авторизацию проверять вне метода.
117
Безопасный ключ дороже по кардинальности
Плюс
118
Безопасный ключ дороже по кардинальности
Плюс
Минус
Что мы имеем?
119
Что получили
Поняли: быстрый кэшированный ответ может быть неправильным.
Что мы имеем?
120
Что получили
Поняли: быстрый кэшированный ответ может быть неправильным.
Что решили
Разобрали устаревшие данные, negative caching и ошибки ключей для прав доступа.
Что мы имеем?
121
Что получили
Поняли: быстрый кэшированный ответ может быть неправильным.
Что решили
Что осталось
Разобрали устаревшие данные, negative caching и ошибки ключей для прав доступа.
Показать, как кэш меняет не только данные, но и нагрузку на систему.
Кэш меняет нагрузочный профиль
122
TTL, miss storm и отказ Redis способны ударить по БД сильнее обычного трафика.
123
TTL - это не просто срок жизни ключа
Один популярный ключ TTL = 5 минут
124
TTL - это не просто срок жизни ключа
Один популярный ключ TTL = 5 минут
Через 5 минут 1000 cache miss
125
TTL - это не просто срок жизни ключа
Один популярный ключ TTL = 5 минут
Через 5 минут 1000 cache miss
1000 одинаковых запросов в БД
126
TTL - это не просто срок жизни ключа
TTL задает момент, когда система может синхронно переложить чтение с Redis обратно на PostgreSQL.
Один популярный ключ TTL = 5 минут
Через 5 минут 1000 cache miss
1000 одинаковых запросов в БД
128
129
public CatalogPageDto getCatalogPage(int page) {
return catalogRepository.loadPage(page);
}
Примитивный TTL для горячего ключа
130
@Cacheable(cacheNames = "catalog-page", key = "#page")
public CatalogPageDto getCatalogPage(int page) {
return catalogRepository.loadPage(page);
}
Примитивный TTL для горячего ключа
131
@Cacheable(cacheNames = "catalog-page", key = "#page")
public CatalogPageDto getCatalogPage(int page) {
return catalogRepository.loadPage(page);
}
// catalog-page TTL = 30 seconds
// один популярный ключ истекает
// сразу для всех
Примитивный TTL для горячего ключа
132
@Cacheable(cacheNames = "catalog-page", key = "#page")
public CatalogPageDto getCatalogPage(int page) {
return catalogRepository.loadPage(page);
}
// catalog-page TTL = 30 seconds
// один популярный ключ истекает
// сразу для всех
Примитивный TTL для горячего ключа
Проблема не в самом TTL.
Проблема в синхронном восстановлении популярного значения.
Интерактив #4: У нас cache stampede. Что первым уменьшит лавину одинаковых MISS?
133
A. Увеличить размер Hikari pool
C. Single-flight + лимит загрузчиков кэша
B. Добавить TTL jitter
D. Фоновое обновление + прогрев кэша
134
135
136
137
138
139
140
141
Что мы имеем?
142
Что получили
Кэш меняет не только latency, но и профиль нагрузки на PostgreSQL.
Что мы имеем?
143
Что получили
Кэш меняет не только latency, но и профиль нагрузки на PostgreSQL.
Что решили
Разобрали TTL, stampede, cold cache и защиту от miss storm.
Что мы имеем?
144
Что получили
Кэш меняет не только latency, но и профиль нагрузки на PostgreSQL.
Что решили
Что осталось
Разобрали TTL, stampede, cold cache и защиту от miss storm.
Показать, что кэшированные данные становятся частью релиза.
Кэш усложняет релизы
145
Формат значения кэша - это прод-контракт.
146
// v1
public record ProductCardCacheDto(
UUID id,
String name,
BigDecimal price
) {}
Кэш хранит не объект. Кэш хранит байты.
147
// v1
public record ProductCardCacheDto(
UUID id,
String name,
BigDecimal price
) {}
// v2
public record ProductCardCacheDto(
UUID id,
String name,
Money price,
ProductStatus status
) {}
Кэш хранит не объект. Кэш хранит байты.
148
// v1
public record ProductCardCacheDto(
UUID id,
String name,
BigDecimal price
) {}
// v2
public record ProductCardCacheDto(
UUID id,
String name,
Money price,
ProductStatus status
) {}
Кэш хранит не объект. Кэш хранит байты.
Redis все еще содержит значения v1. Новый код уже читает как v2.
149
150
cache-lab::v1::product-card::{id}
cache-lab::v2::product-card::{id}
ProductCardCacheDto {
UUID id;
String name;
long schemaVersion;
}
Версионирование кэша: скучно, но спасает релизы
151
Версионирование кэша:
скучно, но спасает релизы
152
Версионирование кэша:
скучно, но спасает релизы
153
Версионирование кэша:
скучно, но спасает релизы
Что мы имеем?
154
Что получили
Формат значения кэша оказался частью релизного контракта.
Что мы имеем?
155
Что получили
Формат значения кэша оказался частью релизного контракта.
Что решили
Разобрали эволюцию DTO, blue-green и БД fallback шторм.
Что мы имеем?
156
Что получили
Формат cache value оказался частью релизного контракта.
Что решили
Что осталось
Разобрали эволюцию DTO, blue-green и БД fallback шторм.
Показать, почему local cache + Redis усложняет получение свежих данных еще сильнее.
Локальный кэш + Redis
157
Два уровня скорости - несколько уровней рассинхронизации.
158
Caffeine перед Redis: быстрее, но не проще
PostgreSQL
version=2
159
Caffeine перед Redis: быстрее, но не проще
Redis
version=2
PostgreSQL
version=2
160
Caffeine перед Redis: быстрее, но не проще
app-b
Caffeine: v2
Redis
version=2
PostgreSQL
version=2
161
Caffeine перед Redis: быстрее, но не проще
Client
app-b
Caffeine: v2
Redis
version=2
PostgreSQL
version=2
162
Caffeine перед Redis: быстрее, но не проще
Client
app-a
Caffeine: v1
app-b
Caffeine: v2
Redis
version=2
PostgreSQL
version=2
163
Caffeine перед Redis: быстрее, но не проще
Client
app-a
Caffeine: v1
app-b
Caffeine: v2
Redis
version=2
PostgreSQL
version=2
Один Redis и одна БД, но несколько локальных состояний. После операции UPDATE один под может продолжать отдавать старое значение.
164
Что делать с многоуровневым кэшем
Многоуровневый кэш ускоряет чтение, но свежесть становится распределенной проблемой.
Короткий локальный TTL
снижает окно устаревания
Pub/Sub инвалидация
полезно, но не бесплатно
Версионированные значения
помогают обнаружить старое
Никакого локального кэша
для чувствительных данных
Иммутабельные данные
лучший кандидат
Метрики на каждый слой
local hit ≠ Redis hit
165
Что делать с многоуровневым кэшем
Многоуровневый кэш ускоряет чтение, но свежесть становится распределенной проблемой.
Короткий локальный TTL
снижает окно устаревания
Pub/Sub инвалидация
полезно, но не бесплатно
Версионированные значения
помогают обнаружить старое
Никакого локального кэша
для чувствительных данных
Иммутабельные данные
лучший кандидат
Метрики на каждый слой
local hit ≠ Redis hit
166
Что делать с многоуровневым кэшем
Многоуровневый кэш ускоряет чтение, но свежесть становится распределенной проблемой.
Короткий локальный TTL
снижает окно устаревания
Pub/Sub инвалидация
полезно, но не бесплатно
Версионированные значения
помогают обнаружить старое
Никакого локального кэша
для чувствительных данных
Иммутабельные данные
лучший кандидат
Метрики на каждый слой
local hit ≠ Redis hit
167
Что делать с многоуровневым кэшем
Многоуровневый кэш ускоряет чтение, но свежесть становится распределенной проблемой.
Короткий локальный TTL
снижает окно устаревания
Pub/Sub инвалидация
полезно, но не бесплатно
Версионированные значения
помогают обнаружить старое
Никакого локального кэша
для чувствительных данных
Иммутабельные данные
лучший кандидат
Метрики на каждый слой
local hit ≠ Redis hit
168
Что делать с многоуровневым кэшем
Многоуровневый кэш ускоряет чтение, но свежесть становится распределенной проблемой.
Короткий локальный TTL
снижает окно устаревания
Pub/Sub инвалидация
полезно, но не бесплатно
Версионированные значения
помогают обнаружить старое
Никакого локального кэша
для чувствительных данных
Иммутабельные данные
лучший кандидат
Метрики на каждый слой
local hit ≠ Redis hit
169
Что делать с многоуровневым кэшем
Многоуровневый кэш ускоряет чтение, но свежесть становится распределенной проблемой.
Короткий локальный TTL
снижает окно устаревания
Pub/Sub инвалидация
полезно, но не бесплатно
Версионированные значения
помогают обнаружить старое
Никакого локального кэша
для чувствительных данных
Иммутабельные данные
лучший кандидат
Метрики на каждый слой
local hit ≠ Redis hit
Что мы имеем?
170
Что получили
Поняли, что Redis может быть актуален, а локальный кэш - нет.
Что мы имеем?
171
Что получили
Поняли, что Redis может быть актуален, а локальный кэш - нет.
Что решили
Определили: не все данные можно держать в нескольких слоях.
Что мы имеем?
172
Что получили
Поняли, что Redis может быть актуален, а локальный кэш - нет.
Что решили
Что осталось
Определили: не все данные можно держать в нескольких слоях.
Показать кульминацию: кэш долго скрывал деградацию базы.
Акт 4. Кэш скрывает деградацию БД
173
Пока hit ratio высокий, база может болеть почти незаметно.
175
177
Интерактив #5: Redis очистили, БД легла. Что сломано архитектурно?
178
C. Нет управляемого режима холодного кэша.
D. Redis стал единой точкой отказа
A. БД должна держать весь трафик на чтение.
B. Не было прогрева кэша
179
Холодный кэш - это отдельный режим работы системы
Правильный разбор начинается с вопроса: выдерживает ли PostgreSQL трафик при падении hit ratio и есть ли лимиты на cache loader.
Практичный ответ
180
Холодный кэш - это отдельный режим работы системы
Правильный разбор начинается с вопроса: выдерживает ли PostgreSQL трафик при падении hit ratio и есть ли лимиты на cache loader.
Практичный ответ
Почему
Высокий hit ratio может скрывать деградацию БД. Планирование пропускной способности должен включать:
Что мы имеем?
181
Что получили
Увидели, как кэш маскирует проблемы БД до момента резкого провала hit ratio.
Что мы имеем?
182
Что получили
Увидели, как кэш маскирует проблемы БД до момента резкого провала hit ratio.
Что решили
Зафиксировали: Redis outage - это сценарий нагрузки на БД, а не только проблема Redis.
Что мы имеем?
183
Что получили
Увидели, как кэш маскирует проблемы БД до момента резкого провала hit ratio.
Что решили
Что осталось
Зафиксировали: Redis outage - это сценарий нагрузки на БД, а не только проблема Redis.
Собрать практический чеклист безопасного кэширования.
Акт 5. Как кэшировать безопаснее
184
Не “не используйте кэш”, а проектируйте его как часть архитектуры.
185
Чеклист перед @Cacheable
Redis не лечит БД. Он уменьшает количество случаев, когда ее болезнь видна пользователю.
Что кэшируем?
DTO, не Entity
Свежесть:
сколько старого можно показать?
Инвалидация
кто и когда удаляет?
Авария
что при отказе Redis?
Холодный кэш
выдержит ли БД?
Безопасность
есть ли user/tenant/role?
Релиз
формат совместим?
Метрики/тесты
как поймем, что сломали?
186
Чеклист перед @Cacheable
Redis не лечит БД. Он уменьшает количество случаев, когда ее болезнь видна пользователю.
Что кэшируем?
DTO, не Entity
Свежесть:
сколько старого можно показать?
Инвалидация
кто и когда удаляет?
Авария
что при отказе Redis?
Холодный кэш
выдержит ли БД?
Безопасность
есть ли user/tenant/role?
Релиз
формат совместим?
Метрики/тесты
как поймем, что сломали?
187
Чеклист перед @Cacheable
Redis не лечит БД. Он уменьшает количество случаев, когда ее болезнь видна пользователю.
Что кэшируем?
DTO, не Entity
Свежесть:
сколько старого можно показать?
Инвалидация
кто и когда удаляет?
Авария
что при отказе Redis?
Холодный кэш
выдержит ли БД?
Безопасность
есть ли user/tenant/role?
Релиз
формат совместим?
Метрики/тесты
как поймем, что сломали?
188
Чеклист перед @Cacheable
Redis не лечит БД. Он уменьшает количество случаев, когда ее болезнь видна пользователю.
Что кэшируем?
DTO, не Entity
Свежесть:
сколько старого можно показать?
Инвалидация
кто и когда удаляет?
Авария
что при отказе Redis?
Холодный кэш
выдержит ли БД?
Безопасность
есть ли user/tenant/role?
Релиз
формат совместим?
Метрики/тесты
как поймем, что сломали?
189
Чеклист перед @Cacheable
Redis не лечит БД. Он уменьшает количество случаев, когда ее болезнь видна пользователю.
Что кэшируем?
DTO, не Entity
Свежесть:
сколько старого можно показать?
Инвалидация
кто и когда удаляет?
Авария
что при отказе Redis?
Холодный кэш
выдержит ли БД?
Безопасность
есть ли user/tenant/role?
Релиз
формат совместим?
Метрики/тесты
как поймем, что сломали?
190
Чеклист перед @Cacheable
Redis не лечит БД. Он уменьшает количество случаев, когда ее болезнь видна пользователю.
Что кэшируем?
DTO, не Entity
Свежесть:
сколько старого можно показать?
Инвалидация
кто и когда удаляет?
Авария
что при отказе Redis?
Холодный кэш
выдержит ли БД?
Безопасность
есть ли user/tenant/role?
Релиз
формат совместим?
Метрики/тесты
как поймем, что сломали?
191
Чеклист перед @Cacheable
Redis не лечит БД. Он уменьшает количество случаев, когда ее болезнь видна пользователю.
Что кэшируем?
DTO, не Entity
Свежесть:
сколько старого можно показать?
Инвалидация
кто и когда удаляет?
Авария
что при отказе Redis?
Холодный кэш
выдержит ли БД?
Безопасность
есть ли user/tenant/role?
Релиз
формат совместим?
Метрики/тесты
как поймем, что сломали?
192
Чеклист перед @Cacheable
Redis не лечит БД. Он уменьшает количество случаев, когда ее болезнь видна пользователю.
Что кэшируем?
DTO, не Entity
Свежесть:
сколько старого можно показать?
Инвалидация
кто и когда удаляет?
Авария
что при отказе Redis?
Холодный кэш
выдержит ли БД?
Безопасность
есть ли user/tenant/role?
Релиз
формат совместим?
Метрики/тесты
как поймем, что сломали?
193
cache.hit / cache.miss / cache.put / cache.eviction
cache.load.duration / cache.load.failures
cache.stale.served / cache.refresh.failures
cache.deserialization.errors
cache.db.fallback
hikaricp.connections.pending
http.server.requests p95 / p99
redis evicted_keys / expired_keys
Метрики и тесты: hit/miss недостаточно
194
Метрики и тесты: hit/miss недостаточно
196
Выводы
197
Выводы
198
Выводы
199
Исходный код проекта
Превратности
кэша
200
Евгений Сулейманов
СТО | ProzyTech
Software Engineering
Презентация
Github repo