مکانیزم Caching در Asp.net Core
اگر بخواهیم با یک مثال ساده توضیح دهیم، اجازه دهید User-1 داده ای رو درخواست کند و 12-15 ثانیه طول می کشد تا سرور داده ها را واکشی کند. در حین واکشی، ما یک کپی از داده های واکشی شده خود را به موازات هر ذخیره سازی موقت ایجاد می کنیم. بنابراین اکنون، زمانی که User-2 همان دادهها را درخواست میکند، این بار ما به سادگی او را از cache سرویس میدهیم و پاسخ فقط 1-2 ثانیه طول میکشد زیرا قبلاً پاسخ را در cache خود ذخیره کرده بودیم.
دو اصطلاح مهم برای کش استفاده می شود، cache hit و cache miss. Cache hit زمانی اتفاق میافتد که دادهها را بتوان در cache یافت و از cache miss زمانی رخ میدهد که دادهها در cache یافت نشوند.
Caching به طور قابل توجهی عملکرد یک برنامه را بهبود می بخشد و پیچیدگی تولید محتوا را کاهش می دهد. طراحی برنامه به گونه ای مهم است که هرگز مستقیماً به حافظه کش وابسته نباشد. برنامه فقط باید دادههایی را ذخیره کند که مرتباً تغییر نمیکنند و فقط در صورت موجود بودن از دادههای کش استفاده کند.
Asp.net core دارای امکانات caching زیادی است. اما دو تا از انواع اصلی آنها موارد زیر هستند:
- In-memory caching
- Distributed Caching
In-memory Caching
In-memory cache در حافظه یک سرور واحد که میزبان برنامه است ذخیره می شود. اساساً داده ها درون برنامه ذخیره می شوند. این ساده ترین راه برای بهبود چشمگیر عملکرد برنامه است.
مزیت اصلی in-memory cache این است که بسیار سریعتر از کش توزیع شده است زیرا از برقراری ارتباط از طریق شبکه جلوگیری می کند و برای برنامه های کاربردی در مقیاس کوچک مناسب است. و نقطه ضعف اصلی حفظ سازگاری کش در حین استقرار در فضای ابری است.
پیاده سازی in-memory caching در asp.net core
ابتدا یک برنامه ASP.NET Core web API ایجاد کنید.
حالا داخل فایل Startup.cs فقط خط زیر را اضافه کنید. این یک پیاده سازی ذخیره سازی غیر توزیع شده در حافظه را به برنامه ما اضافه می کند.
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
//Rest of the code
}
حالا بیایید یک کنترلر جدید "EmployeeController" ایجاد کنیم. و در این کنترلر کش خود را پیاده سازی خواهیم کرد.
[Route("api/[controller]")]
[ApiController]
public class EmployeeController : ControllerBase
{
private readonly IMemoryCache _memoryCache;
private readonly ApplicationContext _context;
public EmployeeController(IMemoryCache memoryCache, ApplicationContext context)
{
_memoryCache = memoryCache;
_context = context;
}
[HttpGet]
public async Task<IActionResult> GetAllEmployee()
{
var cacheKey = "employeeList";
//checks if cache entries exists
if (!_memoryCache.TryGetValue(cacheKey, out List<Employee> employeeList))
{
//calling the server
employeeList = await _context.Employees.ToListAsync();
//setting up cache options
var cacheExpiryOptions = new MemoryCacheEntryOptions
{
AbsoluteExpiration = DateTime.Now.AddSeconds(50),
Priority = CacheItemPriority.High,
SlidingExpiration = TimeSpan.FromSeconds(20)
};
//setting cache entries
_memoryCache.Set(cacheKey, employeeList, cacheExpiryOptions);
}
return Ok(employeeList);
}
}
این یک پیاده سازی بسیار ساده است. به سادگی بررسی می کنیم که آیا مقدار ذخیره شده ای برای کلید کش خاص موجود است یا خیر. اگر وجود داشته باشد، داده ها را از کش ارائه می کند، در غیر این صورت، سرویس خود را فراخوانی میکنیم و داده ها را در کش ذخیره می کنیم.
توضیح
خط 9: تزریق ImemoryCache به سازنده
خط 16: ایجاد یک کلید کش. همانطور که می دانیم داده ها به صورت جفت کلید-مقدار ذخیره می شوند.
خط 18: بررسی اینکه آیا مقدار کش برای کلید خاص موجود است یا خیر.
خط 24: تنظیم حافظه پنهان. MemoryCacheEntryOptions برای تعریف ویژگی های حیاتی کش استفاده می شود. برخی از ویژگی ها عبارتند از:
- Priority - Priority اولویت حفظ ورودی کش در حافظه پنهان را مشخص می کند. مقدار پیش فرض روی Normal تنظیم شده است.
- Siding Expiration - بازه زمانی مشخصی که در آن کش در صورتی که توسط کسی استفاده نشود منقضی میشود. در مثال Siding Expiration را روی 20 ثانیه تنظیم کردیم، به این معنی که پس از ورود کش، اگر درخواست کلاینت برای 20 ثانیه وجود نداشته باشد، کش منقضی می شود.
- Absolute Expiration - به انقضای واقعی ورودی کش بدون در نظر گرفتن Siding Expiration اشاره دارد. در مثال بالا انقضای مطلق را 50 ثانیه قرار داده ایم. بنابراین به این معنی است که کش هر 50 ثانیه به طور قطع منقضی می شود.
اکنون بیایید افزایش عملکرد برنامه خود را پس از اجرای حافظه پنهان در حافظه مشاهده کنیم.
برای این کار برنامه را اجرا کنید و با استفاده از Postman یک درخواست دریافت به web API ارسال کنید. بنابراین اولین باری که درخواستی را به API خود ارسال می کنیم حدود 2061 میلی ثانیه طول می کشد.
بنابراین برای اولین بار وقتی API خود را فرا میخوانیم، مستقیماً دادهها را از پایگاه داده واکشی میکند و به موازات آن دادهها را در حافظه پنهان ذخیره میکنیم.
حالا اگر همان نقطه پایانی را برای همان داده درخواست کنیم، این بار فقط 20 میلی ثانیه طول می کشد.
بنابراین این یک پیشرفت بسیار شگفت انگیز است. در مورد من، مجموعه داده کوچک است. اگر مجموعه ای بزرگ از داده ها در مورد آن مورد وجود داشته باشد، خدمات ما را به شدت بهبود می بخشد.
Distributed Caching
کش توزیع شده کشی است که می تواند توسط یک یا چند برنامه به اشتراک گذاشته شود و به عنوان یک سرویس خارجی که برای همه سرورها قابل دسترسی است نگهداری می شود. بنابراین کش توزیع شده خارج از برنامه است.
مزیت اصلی کش توزیع شده این است که داده ها در چندین سرور سازگار هستند، زیرا سرور خارج از برنامه است، هر گونه خرابی هر برنامه ای روی سرور کش تأثیر نمی گذارد.
در اینجا سعی خواهیم کرد تا Distributed Caching را با Redis پیاده سازی کنیم.
Redis یک ذخیره ساز ساختار داده in-memory و open source (دارای مجوز BSD) است که به عنوان کش پایگاه داده و message broker استفاده می شود. این پایگاه داده key-value خیلی سریع و حتی پایگاه داده NoSQL نیز هست. بنابراین Redis یک گزینه عالی برای پیاده سازی کش با دسترسی خیلی بالاست.
راه اندازی Redis در Docker
مرحله 1 –image ردیس را از داکر هاب دریافت کنید.
docker pull redis
مرحله ۲- با نگاشت پورت Redis به پورت سیستم محلی، image ردیس را اجرا کنید.
docker run --name myrediscache -p 5003:379 -d redis
مرحله ۳ – اجرای container
docker start myrediscache
Redis اکنون بالا آمده است ، بیایید به سراغ پیاده سازی کش توزیع شده با ASP.NET Core Application برویم.
پیاده سازی کش توزیع شده (Redis) با ASP.NET Core
یک پروژه ASP.NET Core Web API ایجاد کنید و کتابخانه StackExchange.Redis را با استفاده از Nuget Package Manager نصب کنید.
بعد از اضافه شدن package مورد نیاز حالا سرویس را در فایل Startup.cs ثبت کنید.
public void ConfigureServices(IServiceCollection services)
{
//Rest of the code
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration.GetConnectionString("Redis");
options.InstanceName = "localRedis_";
});
}
"options.InstanceName" به عنوان پیشوند نام کلید ما در سرور redis عمل می کند. مثال اگر کشی با نام employeeList را در سرور redis ذخیره کنیم، چیزی شبیه localRedis_employeelist خواهد بود.
تنظیمات مربوط به پیکربندی را برای Redis در appsettings.json ارائه خواهیم کرد.
{
"AllowedHosts": "*",
"ConnectionStrings": {
"Redis": "localhost:5003",
"DefaultConnection": "Data Source=.;Initial Catalog=BuildingDataDB;Integrated Security=True"
}
}
یک کلاس کمکی "DistributedCacheExtensions" ایجاد کنید که در آن مقادیر را از و به Redis Cache دریافت و ذخیره کنیم.
public static class DistributedCacheExtension {
public static async Task SetRecordAsync <T> (this IDistributedCache cache, string recodeId, T data, TimeSpan ? absoluteExpireTime = null, TimeSpan ? slidingExpirationTime = null) {
var options = new DistributedCacheEntryOptions();
options.AbsoluteExpirationRelativeToNow = absoluteExpireTime ?? TimeSpan.FromSeconds(60);
options.SlidingExpiration = slidingExpirationTime;
var jsonData = JsonSerializer.Serialize(data);
await cache.SetStringAsync(recodeId, jsonData, options);
}
public static async Task <T> GetRecordAsync <T> (this IDistributedCache cache, string recordId) {
var jsonData = await cache.GetStringAsync(recordId);
if (jsonData is null) {
return default (T);
}
return JsonSerializer.Deserialize <T> (jsonData);
}
}
در متد "SetRecodeAsync" ما داده ها را در کش Redis ذخیره می کنیم. در اینجا ما سرور IDistributedCache را با AbsoluteExpirationRelativeToNow و SlidingExpiration (خط 12 و خط 13) پیکربندی کردهایم و قبلاً در بخش ذخیرهسازی in-memory درباره این شرایط بحث کردهایم.
و در "GetRecordAsync" بسته به مقداری recodeKey، مقدار کش شده را دریافت می کنیم.
اکنون یک کنترلر به نام "StudentController" ایجاد می کنیم.
public class StudentController : ControllerBase
{
private readonly ApplicationContext _context = null;
private readonly IDistributedCache _cache;
public StudentController(ApplicationContext context, IDistributedCache cache)
{
_context = context;
_cache = cache;
}
[HttpGet]
public async Task<ActionResult<List<Student>>> Get()
{
var cacheKey = "GET_ALL_STUDENTS";
List<Student> students = new List<Student>();
var data = await _cache.GetRecordAsync<List<Student>>(cacheKey);
if (data is null)
{
Thread.Sleep(10000);
data = _context.Student.ToList();
await _cache.SetRecordAsync(cacheKey, data);
}
return data;
}
}
توضیح
خط 5: تزریق IDistributeCache در سازنده کلاس.
خط 15: ایجاد یک کلید کش
خط 18: تلاش برای دریافت از سرور کش Redis. اگر دادهای در سرور کش پیدا شود، دادههای ذخیرهشده را به مشتری ارائه میکند.
خط 20: بررسی اینکه آیا داده های کش موجود است یا خیر. اگر داده ها کش نباشند، داده ها را از پایگاه داده یا سرویس های دیگر دریافت می کند. بنابراین برای شبیه سازی آن، عمداً با استفاده از متد ()Thread.Sleep مقداری تاخیر قرار دادیم.
بنابراین بسیار ساده است.
حالا بیایید اپلیکیشن را اجرا کنیم.
بنابراین برای اولین بار، ما متد Get از StudentController را فراخوانی می کنیم که بارگذاری داده ها حدود 12 ثانیه طول می کشد. در مورد اولین اجرا، داده ها را در کش پیدا نکرد، بنابراین به پایگاه داده می رود و داده ها را واکشی می کند و به طور موازی داده های واکشی شده را در سرور کش Redis ذخیره می کند.
اما در اجرای دوم، داده ها را در 28 میلی ثانیه واکشی می کند. از آنجا که برای دومین بار کاربر همان برنامه داده را درخواست می کند، متوجه شد که داده ها در سرور Redis ذخیره شده اند، بنابراین کاربر را با داده های ذخیره شده در حافظه پنهان سرور می کند.
این یک بهینه سازی فوق العاده از برنامه ما با سرعت فوق العاده سریع است.
منبع: https://www.c-sharpcorner.com/article/caching-mechanism-in-asp-net-core/