JavaRush /Курси /Docker for Spring /Redis після стабільного a...

Redis після стабільного app + postgres

Docker for Spring
Рівень 19, Лекція 0
Відкрита

1. Стабільний фундамент app+postgres

Коли ви додаєте нову інфраструктурну залежність (Redis, брокер повідомлень, що завгодно), ви не просто «ставите ще один контейнер». Ви збільшуєте кількість місць, де щось може піти не так: мережа, конфігурація, профілі, готовність сервісу, порти, версії образів, порядок запуску. Якщо базовий стек ще хитається, нова залежність перетворює діагностику на детектив без фіналу: докази є, а винуватець невідомий.

У Docker/Compose є неприємна математика: чим більше сервісів, тим більше комбінацій проблем. Поки у нас був один контейнер app, усе було відносно лінійним: «запускається / не запускається». Коли зʼявився postgres, стало цікавіше: «застосунок не запускається через профіль? через хост? через пароль? через готовність сервісу? через міграцію?». Це вже нормально, але потребує дисципліни.

Якби ми додали Redis раніше, коли app + postgres ще не доведено до стану «стабільно запускається», ви б отримали ідеальну навчальну катастрофу. Застосунок падає, а ви не розумієте, що саме: не підʼєднався до бази, не пройшли міграції, Redis недоступний або ви просто випадково написали localhost. Такий режим навчання зазвичай закінчується тим, що студент «перемагає» помилку випадковим набором правок, але не може пояснити, що саме він зробив. А ми тут якраз тренуємо протилежну навичку: пояснюваність і відтворюваність.

Можна уявити це як ремонт квартири. Спочатку ви робите стіни рівними та проводку безпечною, а вже потім купуєте розумний дім і підсвітку, яка змінює колір під настрій. Якщо почати з підсвітки, вона дуже красиво миготітиме на тлі кривих стін — але жити в цьому все одно важко.

Що означає «стабільний app + postgres»

«Стабільно» — це не «один раз запустилося після трьох годин болю, і я боюся чіпати компʼютер». У контексті курсу стабільність — це здатність повторити запуск і отримати той самий результат, а у разі поломки — швидко зрозуміти клас проблеми. Це те, що робить локальне середовище інструментом розробки, а не крихким особистим артефактом на одному ноутбуці.

Нам важливо, щоб зв’язка app + postgres поводилася передбачувано у трьох площинах: запуск, дані, спостережуваність. Запуск означає, що застосунок не «обганяє» базу і не падає під час спроби підʼєднатися. Дані означають, що міграції застосовуються, схема має очікуваний вигляд, а reset працює так, як ви це розумієте (а не «щось видалилося, але я не знаю що»). Спостережуваність означає, що за логами й health-ендпоінтами видно, що відбувається, і вам не доводиться діагностувати систему за виразом обличчя.

Щоб це було трохи більш відчутно, давайте зафіксуємо «як виглядає здоровий стек» не списком заклинань, а таблицею спостережуваних сигналів.

Що ми перевіряємо Де це видно Як виглядає «гаразд»
PostgreSQL справді готова docker compose ps, health status, логи postgres статус
healthy
, у логах немає нескінченних рестартів
Застосунок запускається після готовності БД логи app немає нескінченних Connection refused, видно успішний запуск
Flyway застосував міграції логи app видно, що міграції застосовані, застосунок не падає на Validate failed
API відповідає
GET /api/catalog/items
GET /actuator/health
HTTP 200, health зелений
Зрозуміле скидання середовища docker compose down vs down -v ви розумієте, коли дані зберігаються, а коли ви їх свідомо стираєте

Зверніть увагу: тут немає магічних очікувань на кшталт «нехай усе буде швидко» або «нехай усе буде красиво». Стабільність — це про те, що ви можете підняти середовище завтра, на іншій машині, в іншій ОС і отримати ті самі властивості.

І лише після цього фундаменту має сенс додати Redis. Тому що Redis — це шар поверх читань, а читання вже мають уміти доходити до джерела правди. Кешувати те, що не вміє стабільно читатися, — це як клеїти скотч на діряву трубу й радіти, що вода почала текти «трохи по-іншому».

2. Redis як шар кешу

Коли новачок уперше чує «Redis», у голові часто виникає образ дуже швидкої бази даних, яка замінить PostgreSQL і зробить усе блискавичним. Це не зовсім неправда, але в межах нашого курсу такий погляд надто широкий, і він заважає. Сьогодні Redis потрібен не як новий світ, а як інфраструктурне розширення: ми додаємо його до Compose-стеку й використовуємо як кеш для повторних читань.

Кеш — це збережений результат читання, який можна повернути без повторного походу до основного сховища. У нашому проєкті основне сховище в режимі postgres — це PostgreSQL. Вона залишається джерелом правди: якщо завтра Redis зникне, бізнес-дані не повинні зникнути разом із ним. Це важлива точка: Redis зберігає похідні дані, а не первинні.

Щоб не плутатися, корисно тримати в голові різницю ролей, особливо в навчальному проєкті:

Питання PostgreSQL (у режимі postgres) Redis (у режимі cache)
Що зберігає первинні бізнес-дані каталогу результати читань (кеш)
Що буде, якщо видалити ви втратите дані (і це боляче) кеш зникне, але відновиться повторними читаннями
Навіщо потрібен коректність і консистентність даних прискорення повторних читань і розвантаження основного сховища
Як «вмикається» postgres профіль, datasource config, міграції cache профіль, Redis host/port, кешування на read-методах

Це допомагає не переускладнювати архітектуру. Ми не будуємо «Redis-центричну архітектуру» й не перетворюємо Container-Ready Catalog Service на підручник з Redis. Ми додаємо Redis так, як він часто зʼявляється в реальних командах: після бази, як наступний інфраструктурний шар, причому вмикаємо його профілем, щоб не ламати основний шлях.

3. Cache hit / cache miss

Кешування звучить як магія: «зберегли — і стало швидко». Але воно стає зрозумілим рівно в той момент, коли ви навчилися вимовляти два слова: cache miss і cache hit. Miss — це коли в кеші ще немає потрібного результату, і ми змушені сходити до основного сховища. Hit — коли результат уже лежить у кеші, і ми повертаємо його без походу до бази.

Ключова деталь — cache key, тобто значення, за яким ми шукаємо запис у кеші. Для читання елемента за id це майже завжди сам id. Для читання «списку всього» ключ фактично один і той самий, бо запит один і той самий. Зараз важлива сама механіка: однаковий запит → однаковий key → шанс на hit.

Давайте подивимося на мінімальну модель на нашому домені, ще без Spring-анотацій і без Redis — просто щоб мозок побачив «механіку», а не «магічні слова».

import com.example.catalog.catalog.model.CatalogItem;

// Два однакових читання підряд: з погляду сховища це два окремі запити
CatalogItem first = storage.findById(10L);
CatalogItem second = storage.findById(10L); // вдруге знову звертаємося до основного сховища

// Дані однакові, але робота виконана двічі
System.out.println(first.getSku());   // наприклад: "SKU-10"
System.out.println(second.getSku());  // наприклад: "SKU-10"

Тут обидва читання однакові за змістом, але з погляду застосунку це два незалежні походи до сховища. Якщо storage під капотом — PostgreSQL, отже ми двічі просимо БД виконати роботу. Кеш зʼявляється саме там, де виникає питання: «а можна вдруге не ходити?».

Тепер спрощена «ручна» модель кешу (так, це не production-код, зате чесна механіка):

import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

class SimpleItemCache {
    // Локальний кеш у памʼяті процесу: ключ — id елемента, значення — сам елемент
    private final Map<Long, CatalogItem> cache = new HashMap<>();

    CatalogItem get(Long id, Supplier<CatalogItem> loader) {
        // Якщо запису немає (miss) — викликаємо loader (звернення до основного сховища) і кладемо його в кеш
        // Якщо запис є (hit) — повертаємо його без виклику loader
        return cache.computeIfAbsent(id, key -> loader.get());
    }
}

У цій моделі id — це cache key. Якщо id ще не траплявся, ми викликаємо loader, отримуємо дані з основного сховища й кладемо їх у кеш. Якщо траплявся — повертаємо їх із Map. Це і є «hit/miss» у чистому вигляді, без привʼязки до Redis і Spring.

Ту саму думку можна візуалізувати як простий потік:

flowchart TD
    A[Запит: item id=10] --> B{Є в кеші?}
    B -- ні --> C[Читаємо з PostgreSQL]
    C --> D[Кладемо результат у кеш]
    D --> E[Повертаємо відповідь]
    B -- так --> E[Повертаємо відповідь із кешу]

І тут зʼявляється важливе антиочікування, яке в новачків ламає картину: перший запит майже завжди miss. Прискорення зазвичай видно на повторних читаннях. Тому в навчальний день ми й беремо повторювані read-сценарії: щоб hit/miss було видно не десь у теорії, а в поведінці.

При цьому кеш — не безкоштовний. Він додає запитання: «а коли очищати?», «а що робити зі зміною ціни?», «а що, якщо кеш застаріє?». Сьогодні ми свідомо не перетворюємо це на окремий архітектурний курс. Але одне правило варто запамʼятати вже зараз: кешування найпростіше починати з простих, частих читань, а не з операцій, які активно змінюють дані.

4. Перші read-сценарії для кешу

Дуже хочеться кешувати одразу все: список, елемент, експорт, статистику, погоду в місті автора й курс біткоїна. Це нормальне бажання, воно називається «синдром великого тумблера: увімкнути прискорення». Але в реальному проєкті, особливо в навчальному, так робити не можна, тому що ви втрачаєте розуміння причинно-наслідкового звʼязку. Ми маємо побачити ефект кешу на зрозумілій маленькій ділянці.

У Container-Ready Catalog Service є два природні кандидати, які не потребують перепридумувати домен і легко перевіряються простими HTTP-запитами. Це читання списку елементів каталогу та читання елемента за ідентифікатором. Вони максимально чисті: це read-операції, які можна повторити двічі підряд однаково й чесно побачити: перший раз важко, другий раз легко.

Мінімальна модель повторного читання за id виглядає так (у термінах сервісного шару):

import com.example.catalog.catalog.model.CatalogItem;

// Двічі читаємо один і той самий id: для кешу це один і той самий key
CatalogItem a = queryService.findById(10L);
CatalogItem b = queryService.findById(10L); // key = 10L

// Результат однаковий, але у другому виклику ми очікуємо "hit"
System.out.println(a.getTitle()); // наприклад: "USB-C Cable"
System.out.println(b.getTitle()); // наприклад: "USB-C Cable"

А для списку логіка така сама: якщо метод читання той самий, то й сенс кешування зрозумілий — не перераховувати одне й те саме.

import java.util.List;

// Повторюємо один і той самий read-сценарій: у такого запиту зазвичай один і той самий cache key
List<CatalogItem> first = queryService.findAll();
List<CatalogItem> second = queryService.findAll(); // той самий read-сценарій повторно

Чому ми обмежуємося двома сценаріями? Тому що нам зараз важливі не максимальне охоплення кешування, а контрольоване розширення Compose-стеку й акуратна конфігураційна модель. Якщо ми почнемо кешувати половину застосунку, ми витратимо весь день на обговорення «а чому ось це не оновилося» і випадково зробимо кеш центральною темою. Нам зараз важливіше побачити сам шар кешу на маленькій, повторюваній ділянці.

5. Порядок додавання Redis

На перших кроках в інфраструктурі є дуже поширена пастка: «давайте одразу піднімемо весь зоопарк». Це виглядає як прогрес: багато контейнерів, багато логів, відчуття дорослої розробки. Але фактично це часто просто пришвидшений спосіб отримати хаос. Ми спеціально будуємо стек шарами: кожен шар має бути зрозумілий до того, як зʼявиться наступний.

Якщо app + postgres нестабільний, кешування не дає вам інженерної цінності. По-перше, тому що кешувати нічого, доки читання з джерела правди не працюють передбачувано. По-друге, тому що ви не можете чесно інтерпретувати симптоми. Застосунок упав — це база не готова? міграція зламалася? Redis недоступний? профіль не той? порт не той? І кожен новий сервіс додає щонайменше один новий клас проблем: мережа, конфігурація, готовність.

Є ще один практичний момент: кеш — це не просто «прискорювач». Кеш — це домовленість про те, що результат читання можна повторно використовувати. Якщо у вас ще не вибудувана дисципліна міграцій і reset/recovery, ви легко отримаєте ситуацію, коли частина даних «ніби» залишилася в кеші, а база вже була скинута, і ви починаєте діагностувати привиди. Тому ми спочатку стабілізували життєвий цикл бази, і лише потім додаємо шар, який зберігає похідні результати.

Тут порядок не випадковий. Спочатку потрібні джерело правди та його життєвий цикл, потім кеш як прискорювач повторних читань, і лише після цього має сенс розширювати стек далі.

6. Redis у Compose-стеку

Тепер, коли ідея кешу та причина «чому не раніше» вже склалися, стає зрозуміло, що в Compose на нас чекає не революція, а акуратне розширення: до нашого вже передбачуваного стенду app + postgres додасться третій сервіс — Redis. Найважливіше тут те, що головний принцип курсу не змінюється: один і той самий image застосунку запускається по-різному лише завдяки runtime-конфігурації.

На рівні архітектури стека це виглядає дуже спокійно: клієнт як і раніше бʼє HTTP у app, застосунок як і раніше зберігає дані в PostgreSQL, а Redis зʼявляється як шар, який іноді відповідає швидше, бо результат уже лежить під рукою. Якщо Redis недоступний (наприклад, ви його не підняли), ідеальний навчальний сервіс має поводитися передбачувано: або не вмикати cache-режим, або явно повідомляти, що cache-залежність не задоволена. «Мовчки працювати абияк» — поганий сценарій.

Схематично наш світ стає таким:

flowchart LR
    Client[HTTP-клієнт] --> App[Spring Boot app]
    App -->|джерело правди| PG[(PostgreSQL)]
    App -->|шар кешу| R[(Redis)]

Користь цього кроку для Docker-курсу в тому, що ви тренуєте дуже прикладну навичку: розширювати середовище без руйнування базового шляху. Ми не робимо окремий образ «для Redis», не пишемо «docker-режим коду», не замінюємо половину конфігів. Ми додаємо залежність так, як це потім робитимете в реальному проєкті: сервіс зʼявляється в Compose, адреса надходить через service name, увімкнення — через профіль, спостережуваність — через healthchecks і логи.

Далі вже потрібна не загальна розмова про користь кешу, а цілком прикладний wiring: мінімально увімкнути кешування в Spring і підʼєднати Redis як контейнеризовану залежність. Фундамент під цей крок у нас уже є: стабільний app + postgres, готовність і зрозумілий життєвий цикл середовища.

7. Типові помилки під час старту Redis-дня

Перші спроби додати Redis майже завжди супроводжуються відчуттям «я зробив за інструкцією, а воно не магічно прискорилося». Це нормально: кеш — річ, яку треба спостерігати, а не «вірити в неї». Якщо заздалегідь розуміти, чому Redis додається саме зараз і яку роль він відіграє, більшість граблів можна обійти взагалі без болю (або хоча б наступати на них у мʼяких капцях).

Помилка №1: додавати Redis, коли app + postgres ще нестабільний.
Якщо базовий стек підіймається через раз, то Redis не прискорить нічого, крім появи нових помилок. У такій ситуації ви одночасно лагодитимете готовність бази, міграції та підʼєднання до Redis, а мозок швидко перейде в режим «я нічого не розумію». Правильний порядок — спочатку домогтися повторюваного запуску app + postgres, а вже потім розширюватися.

Помилка №2: думати, що Redis замінює PostgreSQL.
На цьому етапі Redis — кеш, а не джерело правди. Якщо ви почнете ставитися до нього як до «другої бази», ви дуже швидко зіткнетеся з тим, що дані «не такі», «зникли» або «не оновилися». Джерело правди залишається в PostgreSQL, Redis зберігає похідний результат читання, який можна створити знову.

Помилка №3: намагатися кешувати все одразу, щоб «точно відчути ефект».
Чим ширше ви розмазуєте кешування, тим складніше зрозуміти, що саме ви спостерігаєте. У навчальному проєкті це особливо критично: ми хочемо побачити hit/miss на конкретних двох read-сценаріях, а не будувати мінікурс з інвалідації кешу та консистентності даних.

Помилка №4: очікувати cache hit, перевіряючи різні запити.
Якщо ви один раз прочитали id=10, а вдруге — id=11, це два різні ключі, і чесного hit тут не буде. Так само не можна порівнювати «перший раз запросив список без фільтра, вдруге — з фільтром» і дивуватися, що кеш не спрацював. Для smoke-перевірки потрібен однаковий повторний read.

Помилка №5: чекати прискорення на першому запиті.
Перший запит найчастіше — miss: потрібно сходити в PostgreSQL і лише потім зберегти результат. Якщо ви оцінюєте кеш за «відчуттям швидкості» на першому виклику, ви майже гарантовано зробите неправильний висновок. Кеш проявляється на повторних читаннях, і це нормально: він не передбачає майбутнє, він запамʼятовує минуле.

1
Задача
Docker for Spring, 19 рівень, 0 лекція
Недоступна
Повторне читання з PostgreSQL без кешу
Повторне читання з PostgreSQL без кешу
1
Задача
Docker for Spring, 19 рівень, 0 лекція
Недоступна
Два повторювані read-сценарії до появи Redis
Два повторювані read-сценарії до появи Redis
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ