Пошук уроків, статей та іншого контенту
Навчитеся читати дані через кеш, завантажувати промахи з джерела та самостійно оновлювати кеш.
Cache-Aside — це патерн роботи з кешем, за якого застосунок самостійно:
перевіряє кеш;
якщо дані знайдено — повертає їх;
якщо даних немає — читає їх із основного джерела;
записує отримані дані в кеш;
повертає результат клієнту.
Кеш не завантажує дані автоматично. Саме застосунок вирішує, коли читати кеш, коли звертатися до джерела та коли оновлювати або видаляти кешовані значення.
Основне джерело даних може бути базою даних, зовнішнім API або файловим сховищем. Кеш зазвичай є швидшим, але тимчасовим сховищем.
Типовий алгоритм читання за Cache-Aside:
запит до застосунку
│
▼
перевірка кешу
┌────┴────┐
│ │
знайдено не знайдено
│ │
▼ ▼
повернути прочитати з джерела
│
▼
записати в кеш
│
▼
повернути даніЯкщо значення є в кеші, це називається cache hit — влучання в кеш.
Якщо значення відсутнє, це називається cache miss — промах кешу.
Користувач запитує профіль із ідентифікатором 42.
Застосунок перевіряє ключ user:42 у кеші.
Кеш повертає профіль.
Застосунок одразу відповідає користувачу.
У цьому випадку база даних не використовується.
Користувач запитує профіль із ідентифікатором 42.
У кеші немає ключа user:42.
Застосунок читає профіль із бази даних.
Застосунок записує профіль у кеш.
Застосунок відповідає користувачу.
Наступний такий самий запит, імовірно, буде оброблено з кешу.
Нижче наведено спрощений приклад із двома сховищами:
database імітує основне джерело даних;
cache імітує кеш;
getUser реалізує читання за Cache-Aside.
const database = new Map([
[1, { id: 1, name: "Олена", email: "olena@example.com" }],
[2, { id: 2, name: "Андрій", email: "andrii@example.com" }],
]);
const cache = new Map();
function readUserFromDatabase(userId) {
console.log(`Читання користувача ${userId} з бази даних`);
return database.get(userId) ?? null;
}
function getUser(userId) {
const cacheKey = `user:${userId}`;
// Спочатку перевіряємо кеш.
if (cache.has(cacheKey)) {
console.log(`Cache hit: ${cacheKey}`);
return cache.get(cacheKey);
}
console.log(`Cache miss: ${cacheKey}`);
// Якщо в кеші немає даних, читаємо їх із джерела.
const user = readUserFromDatabase(userId);
if (user === null) {
return null;
}
// Зберігаємо отримані дані в кеші.
cache.set(cacheKey, user);
return user;
}
console.log(getUser(1));
console.log(getUser(1));
console.log(getUser(999));Результат буде приблизно таким:
Cache miss: user:1
Читання користувача 1 з бази даних
{ id: 1, name: 'Олена', email: 'olena@example.com' }
Cache hit: user:1
{ id: 1, name: 'Олена', email: 'olena@example.com' }
Cache miss: user:999
Читання користувача 999 з бази даних
nullПерший запит для користувача 1 є промахом кешу. Другий запит використовує вже збережене значення.
Cache-Aside відповідає не лише за читання. Застосунок також має подбати про узгодженість кешу під час зміни даних.
Наприклад, користувач змінює ім’я. Потрібно:
оновити запис в основному джерелі;
оновити відповідне значення в кеші або видалити його.
Один із підходів — спочатку оновити базу даних, а потім записати нове значення в кеш.
const database = new Map([
[1, { id: 1, name: "Олена", email: "olena@example.com" }],
]);
const cache = new Map();
function getUser(userId) {
const cacheKey = `user:${userId}`;
if (cache.has(cacheKey)) {
console.log(`Cache hit: ${cacheKey}`);
return cache.get(cacheKey);
}
console.log(`Cache miss: ${cacheKey}`);
const user = database.get(userId) ?? null;
if (user === null) {
return null;
}
cache.set(cacheKey, user);
return user;
}
function updateUserName(userId, newName) {
const user = database.get(userId);
if (!user) {
return null;
}
// Спочатку оновлюємо основне джерело.
const updatedUser = {
...user,
name: newName,
};
database.set(userId, updatedUser);
// Потім оновлюємо значення в кеші.
const cacheKey = `user:${userId}`;
cache.set(cacheKey, updatedUser);
return updatedUser;
}
console.log(getUser(1));
updateUserName(1, "Олена Коваль");
console.log(getUser(1));Після виклику updateUserName наступне читання поверне нове ім’я і з бази даних, і з кешу.
Інший поширений підхід — після зміни даних видалити старе значення з кешу.
Після цього наступне читання буде промахом кешу, прочитає актуальні дані з основного джерела та запише їх у кеш знову.
function updateUserName(userId, newName) {
const user = database.get(userId);
if (!user) {
return null;
}
// Оновлюємо основне джерело.
const updatedUser = {
...user,
name: newName,
};
database.set(userId, updatedUser);
// Видаляємо застаріле значення з кешу.
const cacheKey = `user:${userId}`;
cache.delete(cacheKey);
return updatedUser;
}Цей спосіб зручний, коли об’єкт має багато кешованих представлень. Застосунок не повинен вручну оновлювати кожну копію — він видаляє застарілі значення, а вони будуть створені під час наступного читання.
Кешовані дані можуть застаріти. Тому кеш часто зберігає значення лише обмежений час. Цей час називають TTL — time to live.
Наприклад:
профіль користувача можна кешувати 5 хвилин;
список категорій — 1 годину;
дані, які часто змінюються, — кілька секунд.
Після завершення TTL запис вважається недійсним. Наступний запит має прочитати дані з основного джерела та знову покласти їх у кеш.
TTL зменшує ризик того, що кеш надовго збереже застарілі дані. Проте TTL не замінює оновлення або інвалідацію кешу: після зміни важливих даних застарілий запис краще оновити або видалити одразу.
Зазвичай кешують дані, які:
часто читаються;
повільно отримуються з основного джерела;
рідко змінюються;
однаково потрібні багатьом запитам.
Наприклад:
профілі користувачів;
налаштування застосунку;
результати дорогих запитів;
дані з повільного зовнішнього сервісу.
Не кожні дані потрібно кешувати. Якщо значення змінюється майже після кожного читання або його легко отримати з джерела, кеш може додати складності без помітної користі.
Зменшується кількість запитів до бази даних.
Зменшується навантаження на основне джерело.
Повторні запити обробляються швидше.
Кеш можна додати до вже наявного коду читання.
Застосунок контролює логіку завантаження та оновлення даних.
Дані в кеші можуть бути застарілими.
Потрібно самостійно обробляти cache hit і cache miss.
Потрібно продумати оновлення та видалення записів.
Перший запит після очищення кешу повільніший.
Під час одночасних промахів кілька запитів можуть одночасно звернутися до основного джерела.
Для одного об’єкта алгоритм можна сформулювати так:
Сформувати стабільний ключ кешу, наприклад user:42.
Прочитати значення з кешу.
Якщо значення знайдено — повернути його.
Якщо значення не знайдено — прочитати його з основного джерела.
Якщо джерело не повернуло дані — повернути відповідь про відсутність.
Зберегти результат у кеші.
Повернути результат.
Оновити дані в основному джерелі.
Оновити відповідний запис у кеші або видалити його.
Повернути оновлені дані.
Важливо, щоб основне джерело залишалося джерелом правди. Кеш допомагає швидше читати дані, але не має бути єдиним місцем, де вони зберігаються.
Кеш може бути порожнім, очищеним або тимчасово недоступним. Застосунок має вміти завантажити дані з основного джерела після промаху.
У такому випадку кеш може продовжити повертати старе значення. Після зміни даних потрібно оновити або видалити відповідний кешований запис.
Для читання та оновлення потрібно формувати однаковий ключ. Наприклад, якщо читання використовує user:42, а оновлення видаляє users:42, старе значення залишиться в кеші.
Якщо запису немає в базі, бездумне повторне звернення до бази для кожного запиту може створити навантаження. Іноді відсутній результат також кешують на короткий час. Водночас такий запис потрібно швидко видалити або оновити, якщо об’єкт згодом буде створено.
Кеш має обмежений час життя або механізм інвалідації. Без цього користувачі можуть довго бачити стару інформацію.
Якщо запис у кеш відбувся успішно, а оновлення основного джерела завершилося помилкою, кеш може містити дані, яких насправді немає в джерелі. Надійнішим є порядок: спочатку оновити основне джерело, потім оновити або видалити кеш.
Cache-Aside означає, що застосунок сам керує читанням і записом у кеш.
Спочатку застосунок перевіряє кеш.
Після cache miss дані завантажуються з основного джерела.
Отримані дані записуються в кеш для наступних запитів.
Під час зміни даних кеш потрібно оновити або інвалідувати.
Основне джерело залишається джерелом правди.
TTL допомагає обмежити час зберігання застарілих даних.