Використання патерну Cache-Aside в архітектурі мікросервісів

Введення

У сучасній архітектурі мікросервісів ефективне управління отриманням та зберіганням даних має вирішальне значення для досягнення високої продуктивності та масштабованості. Однією з широко використовуваних технік для оптимізації доступу до даних є Cache-Aside Pattern (Патерн кешу на стороні). У цій статті розглядається, як працює цей патерн, його переваги та впровадження в середовищі мікросервісів.

Що таке Cache-Aside Pattern?

Cache-Aside Pattern — це підхід до проектування, при якому програма відповідає за управління кешем. Вона явно завантажує дані в кеш за потреби та оновлює або інвалідовує кеш, коли це необхідно. На відміну від інших стратегій кешування, кеш не синхронізується автоматично з базою даних, а діє як швидкий сховище даних для часто запитуваних даних.

Як працює Cache-Aside Pattern?

  1. Cache Miss: Коли програма запитує кеш для отримання даних, і дані не знайдені (cache miss), вона отримує дані з основного сховища даних (наприклад, з бази даних).
  2. Заповнення кешу: Програма потім зберігає отримані дані в кеш для подальших запитів.
  3. Cache Hit: Для наступних запитів програма отримує дані безпосередньо з кешу (cache hit), значно покращуючи час відповіді.
  4. Інвалідизація кешу: Коли дані в базі даних оновлюються, програма відповідає за інвалідизацію або оновлення відповідного кешу для підтримки узгодженості.

Процес роботи Cache-Aside Pattern можна підсумувати наступним чином:

pic

  1. Перевірка кешу: Коли клієнт (викликач) потребує доступу до даних, він спочатку перевіряє, чи доступні ці дані в кеші.
  2. Отримання з кешу: Якщо дані знайдені в кеші, клієнт (викликач) отримує їх і повертає викликачеві.
  3. Отримання з бази даних, якщо не в кеші: Якщо дані не знайдені в кеші, клієнт (викликач) отримує їх з бази даних, зберігає в кеші для подальшого використання і потім повертає їх клієнту (викликачеві).

Переваги 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 з обмеженим терміном існування.
  • Сервіс з обмеженим терміном існування створюється один раз на кожен запит (або на кожен обсяг), що ідеально підходить для сервісів, які використовуються під час обробки одного запиту (наприклад, веб-запиту, фонової задачі).
  1. Decorate

Перекладено з: Leveraging the Cache-Aside Pattern in Microservices Architecture

Leave a Reply

Your email address will not be published. Required fields are marked *