Введення
У сучасній архітектурі мікросервісів ефективне управління отриманням та зберіганням даних має вирішальне значення для досягнення високої продуктивності та масштабованості. Однією з широко використовуваних технік для оптимізації доступу до даних є Cache-Aside Pattern (Патерн кешу на стороні). У цій статті розглядається, як працює цей патерн, його переваги та впровадження в середовищі мікросервісів.
Що таке Cache-Aside Pattern?
Cache-Aside Pattern — це підхід до проектування, при якому програма відповідає за управління кешем. Вона явно завантажує дані в кеш за потреби та оновлює або інвалідовує кеш, коли це необхідно. На відміну від інших стратегій кешування, кеш не синхронізується автоматично з базою даних, а діє як швидкий сховище даних для часто запитуваних даних.
Як працює Cache-Aside Pattern?
- Cache Miss: Коли програма запитує кеш для отримання даних, і дані не знайдені (cache miss), вона отримує дані з основного сховища даних (наприклад, з бази даних).
- Заповнення кешу: Програма потім зберігає отримані дані в кеш для подальших запитів.
- Cache Hit: Для наступних запитів програма отримує дані безпосередньо з кешу (cache hit), значно покращуючи час відповіді.
- Інвалідизація кешу: Коли дані в базі даних оновлюються, програма відповідає за інвалідизацію або оновлення відповідного кешу для підтримки узгодженості.
Процес роботи Cache-Aside Pattern можна підсумувати наступним чином:
- Перевірка кешу: Коли клієнт (викликач) потребує доступу до даних, він спочатку перевіряє, чи доступні ці дані в кеші.
- Отримання з кешу: Якщо дані знайдені в кеші, клієнт (викликач) отримує їх і повертає викликачеві.
- Отримання з бази даних, якщо не в кеші: Якщо дані не знайдені в кеші, клієнт (викликач) отримує їх з бази даних, зберігає в кеші для подальшого використання і потім повертає їх клієнту (викликачеві).
Переваги Cache-Aside Pattern
- Покращення продуктивності: Зменшуючи навантаження на базу даних і сервуючи дані з кешу, патерн знижує час відповіді для часто запитуваних даних.
- Ефективність витрат: Уникаючи непотрібних запитів до бази даних, він знижує інфраструктурні витрати, особливо в сценаріях з високим трафіком.
- Гнучкість: Програма має повний контроль над тим, що кешується і на який час, що дозволяє створювати детальні стратегії кешування.
- Простота: Його простіше впровадити порівняно з іншими патернами кешування, такими як write-through чи write-behind кешування.
Впровадження в мікросервісах
У архітектурі мікросервісів кожен сервіс часто управляє своєю власною базою даних і шаром кешування. Ось як можна впровадити Cache-Aside Pattern у типовому середовищі мікросервісів, використовуючи .NET 9, PostgreSQL і Redis як рішення для кешування:
Повний репозиторій проекту на GitHub:
GitHub - Avdunusinghe/Cache-Aside-Pattern: .NET 9 | C# | ASP.NET Web API | Docker | MassTransit | …
Крок 1: Інтеграція кешування та інших бібліотек
// Microsoft.Extensions.Caching.StackExchangeRedis
// Надання послуг кешування для додатків .NET, використовуючи StackExchange.Redis як бекенд для кешування на основі Redis.
// Це використовується для розподіленого кешування, покращуючи продуктивність за рахунок зменшення необхідності повторно запитувати бази даних.
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis
// Marten
// Бібліотека, яка дозволяє зберігати та запитувати документи, використовуючи PostgreSQL як базу даних.
// Це дозволяє здійснювати персистентність на основі документів і підтримує розширені функції, такі як event sourcing і CQRS (Command Query Responsibility Segregation).
Install-Package Marten
// Scrutor
// Бібліотека, що розширює контейнер ін'єкцій залежностей .NET Core для підтримки автоматичної реєстрації сервісів на основі конвенцій.
// Використовується для спрощення і автоматизації процесу реєстрації сервісів, зменшуючи потребу в ручній конфігурації.
Install-Package Scrutor
Крок 2: Налаштування Redis та Бази Даних у Вашому Мікросервісі
У файлі appsettings.json
налаштуйте з'єднання для Redis та бази даних:
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Port=5433;Database=shoppingCartdb;User Id=postgres;Password=postgres;Include Error Detail=true",
"Redis": "localhost:6379"
},
У файлі Program.cs
налаштуйте Redis клієнт і базу даних, використовуючи контейнер ін'єкцій залежностей .NET.
// Налаштування Marten для сховища документів PostgreSQL
builder.Services.AddMarten(opts =>
{
// Встановлюємо рядок з'єднання для бази даних PostgreSQL
opts.Connection(builder.Configuration.GetConnectionString("DefaultConnection")!);
// Конфігуруємо схему Marten для контейнера ShoppingCartContainer, встановлюючи 'UserName' як поле ідентичності
opts.Schema.For().Identity(x => x.UserName);
})
// Увімкнення легких сесій для зменшення накладних витрат при роботі з Marten
.UseLightweightSessions();
// Налаштування кешування Redis за допомогою StackExchange.Redis
builder.Services.AddStackExchangeRedisCache(options =>
{
// Встановлюємо рядок з'єднання для Redis з конфігурації
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
Крок 3: Реалізація Логіки Cache-Aside
Створіть клас репозиторію, який інкапсулює логіку cache-aside:
public interface IShoppingCartRepository
{
///
/// Отримує асинхронно кошик для покупок для вказаного користувача. /// /// Ім'я користувача, для якого необхідно отримати кошик. /// Токен скасування для скасування операції, якщо потрібно (необов'язково). /// Таск, який представляє асинхронну операцію. Результат таску містить контейнер кошика для покупок. Task GetShoppingCartAsync( string userName, CancellationToken cancellationToken = default); /// /// Асинхронно зберігає надані дані кошика для покупок. /// /// Контейнер кошика для покупок, що має бути збережений. /// Токен скасування для скасування операції, якщо потрібно (необов'язково). /// Таск, який представляє асинхронну операцію. Результат таску вказує, чи була операція збереження успішною. Task StoreShoppingCartAsync( ShoppingCartContainer shoppingCart, CancellationToken cancellationToken = default); /// /// Асинхронно видаляє кошик для покупок для вказаного користувача. /// /// Ім'я користувача, для якого потрібно видалити кошик. /// Токен скасування для скасування операції, якщо потрібно (необов'язково). /// Таск, який представляє асинхронну операцію. Результат таску вказує, чи була операція видалення успішною. Task DeleteShoppingCartAsync( string userName, CancellationToken cancellationToken = default); } ``` ``` public class ShoppingCartRepository(IDocumentSession session) : IShoppingCartRepository { public async Task GetShoppingCartAsync(string userName, CancellationToken cancellationToken = default) { // Завантаження контейнера кошика для покупок для вказаного користувача з сесії.
var shoppingCart = await session.LoadAsync(userName, cancellationToken);
// Якщо кошик для покупок не знайдений, викидаємо власне виключення.
return shoppingCart is null ? throw new ShoppingCartNotFoundException(userName) : shoppingCart;
}
public async Task StoreShoppingCartAsync(ShoppingCartContainer shoppingCart, CancellationToken cancellationToken = default)
{
// Зберігаємо контейнер кошика для покупок у сесії.
session.Store(shoppingCart);
// Зберігаємо зміни асинхронно для персистенції даних кошика для покупок.
await session.SaveChangesAsync(cancellationToken);
return shoppingCart;
}
public async Task DeleteShoppingCartAsync(string userName, CancellationToken cancellationToken = default)
{
// Видаляємо контейнер кошика для покупок для вказаного користувача з сесії.
session.Delete(userName);
// Зберігаємо зміни асинхронно для персистенції видалення.
await session.SaveChangesAsync(cancellationToken);
return true;
}
}
public class CachedShoppingCartRepository
(IShoppingCartRepository repository,
IDistributedCache cache) : IShoppingCartRepository
{
public async Task GetShoppingCartAsync(string userName, CancellationToken cancellationToken = default)
{
// Спроба отримати кошик для покупок з кешу.
var cachedShoppingCart = await cache.GetStringAsync(userName, cancellationToken);
if (!string.IsNullOrEmpty(cachedShoppingCart))
{
// Якщо знайдено в кеші, десеріалізуємо та повертаємо.
return JsonSerializer.Deserialize(cachedShoppingCart)!;
}
// Якщо в кеші немає, отримуємо з репозиторію та зберігаємо в кеші для майбутнього доступу.
var shoppingCart = await repository.GetShoppingCartAsync(userName, cancellationToken);
await cache.SetStringAsync(userName, JsonSerializer.Serialize(shoppingCart), cancellationToken);
return shoppingCart;
}
public async Task StoreShoppingCartAsync(ShoppingCartContainer shoppingCart, CancellationToken cancellationToken = default)
{
// Зберігаємо кошик для покупок в репозиторії.
await repository.StoreShoppingCartAsync(shoppingCart, cancellationToken);
// Зберігаємо кошик для покупок в кеші для швидкого отримання в майбутньому.
await cache.SetStringAsync(shoppingCart.UserName, JsonSerializer.Serialize(shoppingCart), cancellationToken);
return shoppingCart;
}
public async Task DeleteShoppingCartAsync(string userName, CancellationToken cancellationToken = default)
{
// Видаляємо кошик для покупок з репозиторію.
await repository.DeleteShoppingCartAsync(userName, cancellationToken);
// Видаляємо кошик для покупок з кешу.
await cache.RemoveAsync(userName, cancellationToken);
return true;
}
}
Додавання та декорування сервісу IShoppingCartRepository
в контейнері ін'єкцій залежностей .NET:
// Реєструємо ShoppingCartRepository як реалізацію для IShoppingCartRepository
// з обмеженим терміном існування, що означає створення нової інстанції на кожен запит/операцію.
builder.Services.AddScoped();
// Декоруємо сервіс IShoppingCartRepository з CachedShoppingCartRepository.
// Це означає, що CachedShoppingCartRepository розширить функціональність
// ShoppingCartRepository, додаючи логіку кешування.
builder.Services.Decorate();
Пояснення:
1.
AddScoped();
- Це реєструє
ShoppingCartRepository
як реалізацію для інтерфейсуIShoppingCartRepository
з обмеженим терміном існування. - Сервіс з обмеженим терміном існування створюється один раз на кожен запит (або на кожен обсяг), що ідеально підходить для сервісів, які використовуються під час обробки одного запиту (наприклад, веб-запиту, фонової задачі).
- Decorate
Перекладено з: Leveraging the Cache-Aside Pattern in Microservices Architecture