Автор статьи: Сергей Прощаев @sproshchaev
Руководитель направления Java‑разработки в FinTech
Введение
В современных микросервисных архитектурах кэширование играет ключевую роль в обеспечении высокой производительности, масштабируемости и отказоустойчивости систем. Правильное применение паттернов кэширования позволяет значительно снизить нагрузку на базы данных, уменьшить время отклика и повысить общую пропускную способность системы.
Что представляют из себя паттерны кэширования?
Давайте рассмотрим пять основных паттернов кэширования, которые применяются в разработке.
Cache‑Aside — это самый интуитивный и часто используемый паттерн. Его можно встретить под названием Паттерн ленивой загрузки. Приложение выступает в роли «контроллера», самостоятельно управляя процессом кэширования. Когда требуется получить данные, приложение сначала обращается к кэшу. Если данные найдены — они возвращаются немедленно. В противном случае, запускается запрос к основному хранилищу, полученные данные сохраняются в кэш, а затем возвращаются пользователю. Этот подход особенно эффективен, когда не все данные в системе требуют кэширования — кэш заполняется только по мере необходимости. Однако он не защищает от ситуаций, когда одновременно множество пользователей запрашивают одни и те же данные, которых еще нет в кэше — это приводит к эффекту «thundering herd».
Read‑Through Cache предлагает более абстрактный подход к работе с данными. В отличие от Cache‑Aside, приложение взаимодействует исключительно с кэшем, который сам отвечает за загрузку данных из источника при их отсутствии. Это упрощает логику приложения, поскольку оно больше не заботится о том, есть ли данные в кэше или нет. Такой паттерн особенно полезен в системах, где важно обеспечить единообразный интерфейс доступа к данным. Однако он требует более тщательной настройки политик вытеснения и может быть менее гибким в сценариях, где часть данных не нуждается в кэшировании.
Write‑Through Cache гарантирует согласованность данных между кэшем и основным хранилищем. При каждой операции записи данные одновременно сохраняются и в кэше, и в базе данных. Это обеспечивает то, что при последующем чтении данные будут доступны немедленно из кэша. Такой подход идеально подходит для систем, где критически важна актуальность данных. Однако он может стать узким местом при высокой нагрузке на запись, поскольку каждая операция требует обновления двух систем. Кроме того, данные, которые редко запрашиваются, все равно будут занимать место в кэше.
Write‑Around Cache предлагает противоположный подход — данные записываются только в основное хранилище, минуя кэш. Это предотвращает «загрязнение» кэша данными, которые могут никогда не понадобиться для чтения. Такой паттерн экономит память кэша для действительно востребованных данных. Этот подход особенно эффективен в сценариях, где запись происходит часто, а чтение — редко. Однако он может привести к увеличенному времени отклика при первом чтении записанных данных, поскольку они должны быть загружены из основного хранилища и помещены в кэш.
Write‑Back Cache обеспечивает максимальную производительность при операциях записи. Данные сначала записываются в кэш, а затем асинхронно передаются в основное хранилище. Это позволяет значительно ускорить операции записи и даже группировать несколько операций для оптимизации. Такой паттерн идеально подходит для write‑heavy систем, где важна высокая пропускная способность. Однако он несет риск потери данных в случае сбоя кэша до того, как данные будут синхронизированы с основным хранилищем. Кроме того, поддержание согласованности данных в распределенных системах становится значительно сложнее.
Каждый из этих паттернов имеет свои сценарии применения. Cache‑Aside подходит для большинства случаев, Read‑Through упрощает архитектуру, Write‑Through гарантирует согласованность, Write‑Around экономит ресурсы кэша, а Write‑Back обеспечивает максимальную производительность.
При выборе паттерна необходимо учитывать характер нагрузки, а также требования к согласованности данных и допустимый уровня сложности реализации.
Пример реализации Cache-Aside
С учетом того, что паттерн кэширования Cache‑Aside является наиболее универсальным, мы рассмотрим его реализацию более подробно.

На рис. 1 изображена схема работы шаблона Cache‑Aside. Приложение сначала проверяет кэш. Если данные найдены в кэше, это называется попадание в кэш (cache hit), и данные сразу считываются и возвращаются клиенту.
Однако, если данные отсутствуют в кэше, происходит промах кэша (cache miss), и тогда приложению приходится выполнить дополнительную работу: запросить данные из базы данных, вернуть их клиенту и сохранить в кэше для обеспечения попадания в кэш при последующих обращениях к этим же данным.
Давайте теперь рассмотрим его простейшую реализацию для понимания того как все работает. И в этом нам поможет Java и Spring Framework.
Для начала подключим зависимости в build.gradle:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
}
И опишем нашу модель данных, которую будем использовать в запросах нашего приложения:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String name;
private String email;
}
И реализуем сервис, который будет имитировать базу данных и выполнять всю логику паттерна Cache‑Aside.
Для имитации базы данных будем использовать класс ConcurrentHashMap, в который через аннотацию @PostConstruct
добавим набор сущностей:
private final Map<Long, User> database = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
database.put(1L, new User(1L, "Alice", "alice@example.com"));
database.put(2L, new User(2L, "Bob", "bob@example.com"));
database.put(3L, new User(3L, "Charlie", "charlie@example.com"));
}
Для реализации хэша будем использовать тот же класс ConcurrentHashMap
, который будет потокобезопасен и будет сохранять кэшируемые данные:
private final Map<Long, User> cache = new ConcurrentHashMap<>();
Теперь рассмотрим реализацию getUserById(Long id) на основе паттерна Cache‑Aside. Для этого реализуем логику из трех шагов:
ШАГ 1: Проверяем кэш и возвращаем пользователя если он там есть:
User cachedUser = cache.get(id);
if (cachedUser != null) {
log.info("CACHE HIT: Пользователь найден в кэше!");
return cachedUser;
}
ШАГ 2: Кэш-промах - если пользователя не нашли в кэше, то загружаем его из базы данных:
User userFromDatabase = database.get(id);
if (userFromDatabase == null) {
log.warn("Пользователь не найден в базе данных! ID: {}", id);
return null;
}
ШАГ 3: Сохраняем найденного пользователя из базы данных в кэш для будущих запросов:
cache.put(id, userFromDatabase);
И осталось добавить простейший REST‑контроллер для возможности формирования вызовов:
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
log.info("Получен HTTP GET запрос для пользователя ID: {}", id);
User user = userService.getUserById(id);
if (user != null) {
log.info("Успешно возвращен пользователь: {} ({})",
user.getName(), user.getId());
} else {
log.warn("Пользователь с ID {} не найден", id);
}
return user;
}
Полный пример этого учебного проекта находится в репозитории GitHub и доступен по ссылке https://github.com/sproshchaev/cache‑patterns‑examples
Заключение
Правильный выбор и реализация паттернов кэширования в микросервисной архитектуре позволяет достичь значительного улучшения производительности и масштабируемости системы. Каждый паттерн имеет свои сценарии применения, и выбор зависит от конкретных требований к системе, характера нагрузки и требований к согласованности данных.
Кэширование — это лишь один из инструментов, позволяющих повысить производительность и масштабируемость микросервисной архитектуры. Но чтобы уверенно применять подобные подходы, важно системно разбираться в устройстве современных серверных приложений и понимать архитектурные паттерны.
Если вы хотите углубить знания и увидеть больше практических примеров, приглашаем вас на курс «Microservice Architecture» — на нем вы научитесь работе с лучшими инструментами по разработке микросервисной архитектуры.
А чтобы узнать, подойдет ли вам программа курса, пройдите вступительный тест.
Комментарии (8)
akod67
10.09.2025 09:58Вопросы консистентности и прогрева кеша при разных паттернах еще бы затронуть.
remindscope
10.09.2025 09:58Спасибо за статью. Про паттерны здорово, но вот примеры кода... Не помешало бы сделать оговорку о том, как реализуется кеш силами Spring в реальном приложении (явно же не так)
onyxmaster
Cash? Серьёзно?
Dair_Targ
Бизнесу-поди cash service куда полезнее, чем какой-то cache service.
onyxmaster
Да и я от cash не отказываюсь, кушать-то что-то надо! =)
sproshchaev
Коллеги, спасибо - опечатку поправили. Сорри :)
MaxRokatansky Автор
Да, забавная очепятка случилась. Исправили)