Tekrarlanan Erişimleri Cache-Aside Pattern ile İyileştirme
Temiz bir kod yazabilmenin yanı sıra, sorumlu bir developer olarak uygulamamızın performanslı bir şekilde çalışabilmesini sağlamak da bir o kadar önemlidir. Bildiğimiz gibi kullanıcılara daha iyi bir deneyim sağlatabilmemiz için olabildiğince hızlı sonuçlar dönüyor olmalıyız. Tahmin edebileceğimiz gibi hızlıca uygulayabileceğimiz en temel performans iyileştirmelerinden bir tanesi, tekrarlanan erişimler için bir cache implementasyonu.
Her ne kadar basit bir işlem gibi gözüksede, bir çok durum karşısında uygulamalarımıza oldukça bir performans artışı kazandırmaktadır. Bunun yanı sıra kaynaklarımızı da daha efektif olarak kullanabilmemizi sağlamaktadır.
Bu makale kapsamında ise .NET içerisinde Cache-Aside pattern’ını nasıl kolay bir şekilde implemente edebileceğimizi göstermeye çalışacağım.
Tanıyalım
Yukarıdaki diyagramdan da anlayabileceğimiz üzere, yaklaşımımız her zaman data’yı ilk olarak cache’den okuyabilmek.
Eğer okumak istediğimiz data cache’de yoksa, data’yı ilgili data store’dan okuyup ardından cache’e ekleyerek response dönmemiz gerekmektedir. Böylece tekrarlanan erişimleri en aza indirerek kaynaklarımızı daha efektif kullanabilir ve kullanıcılara daha iyi bir deneyim sağlatabiliriz.
Consistency
Cache-aside pattern’ı herhangi bir consistency garantisi vermemektedir. Bir başka değişle, okumak istediğimiz bir data, farklı bir işlem tarafından değiştirilmiş olabilir.
Eğer distributed bir ortamda çalışıyorsak, pub/sub yaklaşımı ile data’yı olabildiğince güncel tutmaya çalışabiliriz. Tabi bu durumda eventual consistency’i de kabul ediyor olmalıyız.
Expiration Policy
Bir diğer önemli konu ise cache’lemek istediğimiz data’nın, bir expiration policy’e sahip olması. Aksi halde cachelediğimiz data, bir süre sonra geçersiz bir hale gelebilir.
Consistency’i sağlayabilmek için expiration policy’i dikkatli bir şekilde ayarlamamız gerekmektedir. Eğer bu expiration süresini çok fazla kısa tutarsak, bu sefer yine ilgili data soruce’a tekrar ve tekrar gidileceği için bu yaklaşımdan çok da fazla bir fayda alamayacağız.
Özetle, içerisinde bulunduğumuz domain’e göre bu süreyi dikkatli bir şekilde ayarlamamız gerekmektedir.
Implemente Edelim
Ben implementasyon işlemleri için basit bir ASP.NET 5 Web API projesi oluşturacağım. Burada amacımız, cache-aside pattern’ını implemente ederken minimum efor ile olabildiğince esnek ve reusable bir cache yapısı kurabilmek olacak.
Bunun için öncelikle aşağıdaki gibi “ICacheService” adında bir interface oluşturalım.
using System; using System.Threading.Tasks; namespace MyTodoAPI.Services { public interface ICacheService { Task<T> GetOrSetAsync<T>(string cacheKey, int cacheDurationInMin, Func<Task<T>> func); Task Remove(string cacheKey); } }
“GetOrSetAsync” method’u içerisinde cache-aside pattern’ının implementasyonunu gerçekleştireceğiz. “Remove” method’u içerisinde ise istediğimiz bir data’nın cache’ini invalide edebilmemiz için gerekli olan logic’i ekleyeceğiz.
Cache service’i olarak ise, Redis’i implemente edeceğiz. Ben bu konuda Azure Redis Cache service’ini kullanacağım. Azure Redis Cache service’inin kurulumu ile ilgili adımlara ise, buradan ulaşabilirsiniz.
Implementasyona başlamadan önce, “Microsoft.Extensions.Caching.StackExchangeRedis” paketini projeye NuGet üzerinden aşağıdaki gibi dahil edelim.
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis --version 5.0.1
Ardından projeye “RedisCacheService” adında bir class oluşturarak, “ICacheService” interface’inin implementasyonunu aşağıdaki gibi gerçekleştirelim.
using System; using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; namespace MyTodoAPI.Services { public class RedisCacheService : ICacheService { private readonly IDistributedCache _distributedCache; public RedisCacheService(IDistributedCache distributedCache) { _distributedCache = distributedCache; } public async Task<T> GetOrSetAsync<T>(string cacheKey, int cacheDurationInMin, Func<Task<T>> func) { string cachedItem = await _distributedCache.GetStringAsync(cacheKey); if(!string.IsNullOrEmpty(cachedItem)) { return JsonSerializer.Deserialize<T>(cachedItem); } var item = await func(); if (item != null) { var cacheEntryOptions = new DistributedCacheEntryOptions() .SetSlidingExpiration(TimeSpan.FromMinutes(cacheDurationInMin)); string serializedItem = JsonSerializer.Serialize(item); await _distributedCache.SetStringAsync(cacheKey, serializedItem, cacheEntryOptions); } return item; } public async Task Remove(string cacheKey) { await _distributedCache.RemoveAsync(cacheKey); } } }
Yukarıdaki kod bloğuna baktığımızda, caching işlemleri için build-in olarak gelen “IDistributedCache” interface’ini kullandığımızı görebiliriz. Adaptör olarak redis’i ise, bir sonraki aşamada inject edeceğiz.
“GetOrSetAsync” method’u içerisinde ise tekrar kullanılabilir bir yapı elde edebilmek için func delegate’inden yararlandık. Ayrıca method akışının ise yukarıda paylaştığım diyagram ile benzer olduğunu da görebiliriz.
Son olarak cache’lenmiş data’nın invalidation işlemleri için ise, “Remove” method’unu kullanacağız.
Örnek Bir Kullanım
Örnek gerçekleştirebilmek için default template ile gelen “WeatherForecast” üzerinden gideceğim.
Bunun için “IWeatherForecastService” interface’ini ve “WeatherForecastService” class’ını aşağıdaki gibi oluşturalım ve cache service’ini implemente edelim.
using System.Collections.Generic; using System.Threading.Tasks; namespace MyTodoAPI.Services { public interface IWeatherForecastService { Task GetWeatherForecasts(); Task AddNewWeatherSummary(string summary); } }
using System; using System.Collections.Generic; using System.Threading.Tasks; namespace MyTodoAPI.Services { public class WeatherForecastService : IWeatherForecastService { private readonly ICacheService _cacheService; private static List Summaries = new() { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private const string GetWeatherForecastsCacheKey = "GetWeatherForecasts"; private const string GetWeatherForecastsCacheDurationInMin = 30; public WeatherForecastService(ICacheService cacheService) { _cacheService = cacheService; } public async Task GetWeatherForecasts() { List weatherForecasts = await _cacheService.GetOrSetAsync(cacheKey: GetWeatherForecastsCacheKey, cacheDurationInMin: GetWeatherForecastsCacheDurationInMin, func: () => { var rng = new Random(); var weatherForecasts = new List(); foreach (var item in Summaries) { weatherForecasts.Add(new WeatherForecast { Date = DateTime.Now, TemperatureC = rng.Next(-20, 55), Summary = item }); } return Task.FromResult(weatherForecasts); }); return weatherForecasts; } public Task AddNewWeatherSummary(string summary) { Summaries.Add(summary); _cacheService.Remove(GetWeatherForecastsCacheKey); return Task.CompletedTask; } } }
“GetWeatherForecasts” method’u içerisinde “ICacheService” ini kullanarak basit bir logic implemente ettik. Eğer istediğimiz data daha önce cache’e eklenmiş ise, bu veriye hızlı bir şekilde cache üzerinden erişebileceğiz.
Ayrıca yeni bir “summary” ekleme ihtiyacında da gördüğümüz gibi ilgili cachi’i, “Remove” method’u ile invalidate edeceğiz.
Son olarak injection işlemlerini ise “Startup” içerisinde aşağıdaki gibi gerçekleştirelim.
services.AddStackExchangeRedisCache(opt => { opt.Configuration = Configuration.GetConnectionString("redis:connection_string"); }); services.AddSingleton<ICacheService, RedisCacheService>(); services.AddScoped<IWeatherForecastService, WeatherForecastService>();
Sonuç
Gördüğümüz gibi cache-aside pattern’ı, implemente etmesi oldukça kolay bir pattern. Uygulamaların isteği üzerine istenilen data’yı cache’e ekleyebilmemize ve cache’lenmiş data’yı elde edebilmemize olanak sağlamaktadır. Böylelikle öngörülemeyen durumlarda da caching işlemlerinden olabildiğince faydalanabilmekteyiz.
Referanslar
Cache-Aside pattern – Cloud Design Patterns | Microsoft Docs
Eline sağlık Gökhan.