در قسمت اول، درمورد سیستم Cache پیشفرض موجود در Asp.Net Core و مزیتها و معایب آن گفتیم. اگر قسمت اول را نخواندید، قسمت اول مقاله را میتوانید از این لینک بخوانید.
در این قسمت میخواهیم یک پکیج محبوب و کاربردی را برای پیاده سازی کش، در Asp.Net Core را بررسی کنیم.
در دنیای امروز، برنامه نویسی پکیجها و فریمورکها، نقش بسیار مهمی را ایفا میکنند؛ بطوریکه در بسیاری از این موارد، استفاده از این پکیجها، عمل عاقلانهتری نسبت به دوباره نویسی فیچرهای مربوطه است. برای عمل کشینگ در Asp.Net Core نیز پکیجهای فوقالعادهای وجود دارند که در این مقاله، به بررسی و استفاده پکیج این میپردازیم.
در این پکیج، هر یک از متدهای موجود در عملیات کشینگ، بصورت بهینهای تعریف شده و قابل استفادهاند. سیستمی که این پکیج برای کش کردن دادهها استفاده میکند، همان سیستم کش Asp.Net Core هست و بهنوعی، سوار بر این سیستم، قابلیتهای بیشتر و بهتری را ارائه میدهد و این متدها شامل موارد زیر هستند:
- Get/GetAsync(with data retriever)
- Get/GetAsync(without data retriever)
- Set/SetAsync
- Remove/RemoveAsync
- ~~Refresh/RefreshAsync (was removed)~~
- RemoveByPrefix/RemoveByPrefixAsync
- SetAll/SetAllAsync
- GetAll/GetAllAsync
- GetByPrefix/GetByPrefixAsync
- RemoveAll/RemoveAllAsync
- GetCount
- Flush/FlushAsync
- TrySet/TrySetAsync
- GetExpiration/GetExpirationAsync
مفهوم استفاده از این متدها، با همان مفهوم متدهای کش در core، برابری میکند که در قسمت اول این مقاله به آن پرداختیم. همانطور که میبینید، این پکیج از Async Methodها هم پشتیبانی میکند و میتوانید کشهای خود را بصورت Async بنویسید.
یکی از قابلیتهای دیگر این پکیج، سازگاری آن با انواع Cache Providerهای موجود است. بطور خلاصه Cache Providerها، همان ارائه دهندگان حافظهی Ram، در قالبها و ابزارهای مختلف هستند. برخی از اینها با داشتن الگوریتمهای بهینهتر، سرعت بالاتری از رد و بدل کردن اطلاعات در Ram را در اختیار ما قرار میدهند و Local بودن یا Distributed بودن را کنترل میکنند. Cache providerهای گوناگونی وجود دارند که هریک به شکلی کار میکند؛ برای مثال شما میتوانید با Provider ای مستقیما با خود Ram، برای Get و Set کردن کشهای خود در ارتباط باشید و یا در روشی دیگر، از یک دیتابیس(Redis)، جدا از دیتابیس اصلی برنامه که حافظه مصرفی آن Ram هست و منابع حافظه شما را نیز مدیریت میکند، برای کشهای خود استفاده کنید و اطلاعات را بصورت ایندکس گذاری شده در Ram ذخیره کنید که به سرعت واکشی آن میافزاید.
بطور کل Cache Provider هایی که پکیج EasyCaching با آنها سازگار است شامل موارد زیر است:
- In-Memory
- Memcached
- Redis(Based on StackExchange.Redis)
- Redis(Based on csredis)
- SQLite
- Hybrid
- Disk
- LiteDb
یکی دیگر از مزیتهای این پکیج، سازگاری آن با Serializerهای مختلف است. همانطور که میدانید دیتاهای ورودی و خروجی در برنامه، نیاز به Serialize شدن دارند. وقتی میخواهید دیتایی را در دیتابیس ذخیره کنید، آن را در قالب یک شی (Model) از کاربر دریافت میکنید و شما باید برای ذخیره این دیتا، اطلاعات درون شیء را به قالبی که قابل ذخیره شدن باشد، در آورید که این عمل Serialize نام دارد. دقیقا برعکس این روند، بعد از واکشی اطلاعات از دیتابیس، اطلاعات را در قالب اشیایی که قابل نمایش به کاربر باشد (DeSerialize) در میاوریم.
در کش کردن هم چیزی که شما با آن سروکار دارید، دیتا است؛ پس برای ذخیره و واکشی این دیتا، از هر حافظهای، چه دیتابیس و چه Ram، باید از یک Serializer استفاده کنید تا عملیات Serialize و DeSerialize را برایتان انجام دهد. Serializerهای مختلفی وجود دارند که بصورت پکیجهایی ارائه شدهاند و اما Serializer هایی که سیستم EasyCaching آنهارا پشتیبانی میکند، شامل موارد ذیل هستند:
- BinaryFormatter
- MessagePack
- Newtonsoft.Json
- Protobuf
- System.Text.Json
در ادامه به پیاده سازی کش، با استفاده از EasyCaching در سه Provider مختلف از این پکیج میپردازیم.
1_ پروایدر InMemory :
پروایدر InMemory، یک سیستم Local Caching را برای ما به وجود میاورد. در قسمت قبلی مقاله سیستمهای Local(InMemory) و Distributed را بررسی کردیم و تفاوتهای میان آنها را گفتیم.
برای استفاده از پروایدر InMemory در EasyCaching باید پکیج زیر را نصب کنید:
Install-Package EasyCaching.InMemory
در مرحله بعد، کانفیگهای مربوط به این پکیج را در کلاس Startup برنامه خود میاوریم. راحتترین روش افزودن این پکیج به Startup، صرفا افزودن حالت پیشفرض آن به متد ConfigureServices است که به شرح زیر عمل میکنیم:
services.AddEasyCaching(options => { // use memory cache with a simple way options.UseInMemory(); }
این حالت از کانفیگ، پکیج تنظیمات پیشفرض خود پکیج را برای برنامه قرار میدهد؛ شما میتوانید با استفاده از optionهای دیگری که در متد ()UseInMemory وجود دارند، تنظیمات شخصی سازی شده از سیستم کشینگ خود را اعمال کنید.
و تمام. هم اکنون میتوان با استفاده از اینترفیس IEasyCachingProvider که این سرویس در اختیارمان قرار داده و عمل تزریق وابستگی آن در کلاسها و کنترلرهای مان دیتای در حال عبور را کش کنیم. متدهای موجود در این اینترفیس به شرح زیر میباشد :
// تنظیم یک کش با کلید - مقدار - زمان انقضا void Set<T>(string cacheKey, T cacheValue, TimeSpan expiration); Task SetAsync<T>(string cacheKey, T cacheValue, TimeSpan expiration); // تنظیم یک کش با مقدار و زمان انقضا که تایپ مقدار از نوع دیکشنری هست و کلید دیکشنری بعنوان کلید کش قرار میگیرد void SetAll<T>(IDictionary<string, T> value, TimeSpan expiration); Task SetAllAsync<T>(IDictionary<string, T> value, TimeSpan expiration); // تنظیم یک کش با کلید - مقدار - زمان انقضا // اگر کلیدی همنام وجود داشته باشد مقدار نادرست و در غیر اینصورت مقدار نادرست را برمیگرداند bool TrySet<T>(string cacheKey, T cacheValue, TimeSpan expiration); Task<bool> TrySetAsync<T>(string cacheKey, T cacheValue, TimeSpan expiration); // گرفتن یک کش با کلید CacheValue<T> Get<T>(string cacheKey); Task<CacheValue<T>> GetAsync<T>(string cacheKey); // CacheValue<T> Get<T>(string cacheKey, Func<T> dataRetriever, TimeSpan expiration); Task<CacheValue<T>> GetAsync<T>(string cacheKey, Func<Task<T>> dataRetriever, TimeSpan expiration); // گرفتن یک کش با چند کاراکتر پیشین کلید آن // برای مثال یک کلید با نام // MyKey // تنها با داشتن چند حرف اول // MyK // میتوانیم این کش را دریافت کنیم IDictionary<string, CacheValue<T>> GetByPrefix<T>(string prefix); Task<IDictionary<string, CacheValue<T>>> GetByPrefixAsync<T>(string prefix); // IDictionary<string, CacheValue<T>> GetAll<T>(IEnumerable<string> cacheKeys); Task<IDictionary<string, CacheValue<T>>> GetAllAsync<T>(IEnumerable<string> cacheKeys); // گرفتن تعداد کشهای با کاراکترهای پیشین کلید که میان چند کلید یکسان است int GetCount(string prefix = ""); Task<int> GetCountAsync(string prefix = ""); // گرفتن زمان انقضا باقیمانده از یک کش با کلید آن TimeSpan GetExpiration(string cacheKey); Task<TimeSpan> GetExpirationAsync(string cacheKey); // حذف کردن یک کش با کلید void Remove(string cacheKey); Task RemoveAsync(string cacheKey); // حذف کردن یک کش با چند کاراکتر پیشین کلید void RemoveByPrefix(string prefix); Task RemoveByPrefixAsync(string prefix); // حذف کردن چند کش با لیستی از کلیدها void RemoveAll(IEnumerable<string> cacheKeys); Task RemoveAllAsync(IEnumerable<string> cacheKeys); // بررسی وجود یا عدم وجود یک کش با کلید bool Exists(string cacheKey); Task<bool> ExistsAsync(string cacheKey); // حذف کردن همه کشها void Flush(); Task FlushAsync();
همانطور که قبلا گفته شد، سیستم کش، با دیتا مرتبط است و نیازمند یک Object Serializer جهت Serialize کردن اطلاعات ورودی و ذخیره آن در Target Storage مشخص شده است. پکیج EasyCaching برای Providerهای خود، یک Object Serializer پیشفرض قرار دادهاست و تا وقتی که شما آن را طبق نیازی خاص، بصورت سفارشی تغییر نداده باشید، از آن استفاده میکند.
در میان پنج Serializer معرفی شده که EasyCaching آنها را پشتیبانی میکند، BinaryFormatter بصورت پیشفرض در همهی Providerها برقرار است و تا وقتی یک Serializer انتخابی به EasyCaching معرفی نکنید، این پکیج از این Serializer استفاده میکند.
برای استفاده از Serializerهای دیگری که معرفی شده میتوانید از لینکهای زیر کمک بگیرید :
2 - پروایدر Redis :
ردیس، یک دیتابیس Key Value محور هست که محل ذخیره سازی آن Ram است و اطلاعات، بصورت موقت در آن ذخیره میشوند. بطور خلاصه، Key Value یعنی یکبار کلید و مقداری برای آن کلید تعریف میشود و هر وقت نام کلید تعریف شده، صدا زده شد، مقدار نسبت داده شده به آن، در اختیار ما قرار میگیرد. برای مثال کلید "Name" و مقدار "James". با این انتساب، هروقت "Name" فراخوانده شود، مقدار "James" را خواهیم داشت. سیستم Key Value بخاطر عدم پیچیدگی و سادگیای که دارد، بسیار سریع عمل میکند و همچنین ایندکس گذاریهایی که ردیس روی دیتاها انجام میدهد، باعث افزایش سرعت آن نیز خواهد شد که ردیس را به سریعترین دیتابیس Key Value دنیا تبدیل کرده.
در اینجا با توجه به قابلیت هایی که ردیس داراست، یکی از بهترین گزینهها برای انتخاب بعنوان فضای ذخیره سازی کشها بصورت Distributed است.
برای استفاده از این دیتابیس قدرتمند ابتدا باید از طریق یکی از روشهای معمول اقدام به نصب آن کنید. میتوانید فایل نصبی را از وبسایت رسمی آن دانلود کنید و یا یا با استفاده از Docker اقدام به نصب آن نمایید.
پس از نصب این دیتابیس روی سیستم خود ، برای استفاده از آن در EasyCaching ابتدا باید پکیج مورد نیاز را نصب کنید.
Install-Package EasyCaching.Redis
services.AddEasyCaching(option => { option.UseRedis(config => { config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379)); }); });
با استفاده از متد UseRedis شما قابلیت استفاده از ردیس را در EasyCaching فعال میکنید و سپس باید اطلاعات Host و Port ردیس نصب شدهی روی سیستم خود را به این متد معرفی کنید.
اگر ردیس را بدون تنظیمات شخصی سازی شده و در همان حالت پیشفرض خودش نصب کرده باشید، Host و Port شما مانند نمونه بالا 127.0.0.1 و 6379 خواهد بود و نیازی به تغییر نیست.
در مرحله بعد برای استفاده از پروایدر ردیس ، اینترفیس IRedisCachingProvider در سرتاسر برنامه در دسترس خواهد بود. این اینترفیس علاوه بر اینکه متدهای اصلی موجود در EasyCaching را ساپورت کرده ، بخاطر ساختار دیتابیسی که خود ردیس در اختیار ما قرار میدهد قابلیتهای بیشتری نیز اراعه خواهد داد. این قابلیتها خصیصههای ردیس هست چرا که این دیتابیس هم دقیقا شبیه به ساختار سیستم کش Key , Value را پشتیبانی میکند و در پی آن قابلیت هایی برای مدیریت بهتر کلیدها و مقادیر اراعه میدهد.
اینترفیس IRedisCachingProvider شامل تعداد زیادی از متدها برای پشتیبانی از قابلیتهای ردیس است که در ادامه همه آنهارا نام برده و برخی را توضیح مختصری خواهیم داد:
- متدهای Keys
// حذف کردن یک کلید در صورت وجود bool KeyDel(string cacheKey); Task<bool> KeyDelAsync(string cacheKey); // تنظیم تاریخ انتضا به یک کلید موجود بر حسب ثانیه bool KeyExpire(string cacheKey, int second); Task<bool> KeyExpireAsync(string cacheKey, int second); // بررسی وجود یا عدم وجود یک کلید bool KeyExists(string cacheKey); Task<bool> KeyExistsAsync(string cacheKey); // گرفتن زمان انتقضا باقیمانده یک کلید long TTL(string cacheKey); Task<long> TTLAsync(string cacheKey); // جستجو بین همه کلیدها براساس فیلتر شامل بودن نام کلید از مقدار ورودی List<string> SearchKeys(string cacheKey, int? count = null);
- متدهای String
// افزودن یک عدد (پیشقرض 1) به مقدار نوع عددی یک کلید long IncrBy(string cacheKey, long value = 1); Task<long> IncrByAsync(string cacheKey, long value = 1); // افزودن یک عدد (پیشقرض 1) به مقدار نوع عددی یک کلید double IncrByFloat(string cacheKey, double value = 1); Task<double> IncrByFloatAsync(string cacheKey, double value = 1); // تنظیم یک کلید و مقدار وقتی مقدار از نوع رشته باشد bool StringSet(string cacheKey, string cacheValue, TimeSpan? expiration = null, string when = ""); Task<bool> StringSetAsync(string cacheKey, string cacheValue, TimeSpan? expiration = null, string when = ""); // گرفتن کلید و مقدار آن وقتی مقدار از نوع رشته باشد string StringGet(string cacheKey); Task<string> StringGetAsync(string cacheKey); // گرفتن تعداد کاراکترهای مقدار یک کلید وقتی مقدار از نوع رشته باشد long StringLen(string cacheKey); Task<long> StringLenAsync(string cacheKey); // جایگزاری یک رشته درون رشته مقدار یک کلید بعد از شماره کاراکتر مشخص شده در ورودی برای مثال // "Hello World" // 6 , jack // "Hello jack" long StringSetRange(string cacheKey, long offest, string value); Task<long> StringSetRangeAsync(string cacheKey, long offest, string value); // گرفتن یک بازه از رشته مقدار یک کلید با شماره کاراکتر شروع و پایان string StringGetRange(string cacheKey, long start, long end); Task<string> StringGetRangeAsync(string cacheKey, long start, long end);
- متدهای Hashes
// شما میتوانید دو کلید با نامهای یکسان داشته باشید که در کلید تایپ دیکشنری مقدار خود باهم متفاوت هستند bool HMSet(string cacheKey, Dictionary<string, string> vals, TimeSpan? expiration = null); Task<bool> HMSetAsync(string cacheKey, Dictionary<string, string> vals, TimeSpan? expiration = null); // شما میتوانید دو کلید با نامهای یکسان داشته باشید که در ورودی فیلد باهم متفاوت هستند bool HSet(string cacheKey, string field, string cacheValue); Task<bool> HSetAsync(string cacheKey, string field, string cacheValue); // بررسی وجود یا عدم وجود یک کلید و فیلد bool HExists(string cacheKey, string field); Task<bool> HExistsAsync(string cacheKey, string field); // حذف کردن کلیدهای همنام موجود با همه فیلدهای متفاوت در حالت پیشفرض مگر اینکه کلید و نام فیلد را بهمراه آن مشخص کنید long HDel(string cacheKey, IList<string> fields = null); Task<long> HDelAsync(string cacheKey, IList<string> fields = null); // گرفتن مقدار با نام کلید و نام فیلد string HGet(string cacheKey, string field); Task<string> HGetAsync(string cacheKey, string field); // گرفتن فیلد و مقدار با کلید Dictionary<string, string> HGetAll(string cacheKey); Task<Dictionary<string, string>> HGetAllAsync(string cacheKey); // افزودن یک عدد (پیشقرض 1) به مقدار نوع عددی یک کلید و فیلد long HIncrBy(string cacheKey, string field, long val = 1); Task<long> HIncrByAsync(string cacheKey, string field, long val = 1); // گرفتن فیلدهای متفاوت یک کلید List<string> HKeys(string cacheKey); Task<List<string>> HKeysAsync(string cacheKey); // گرفتن تعداد فیلدهای متفاوت یک کلید long HLen(string cacheKey); Task<long> HLenAsync(string cacheKey); // گرفتن مقادیر یک کلید بدون در نظر گرفتن فیلدهای متفاوت List<string> HVals(string cacheKey); Task<List<string>> HValsAsync(string cacheKey); // گرفتن مقدار دیکشنری با کلید و نام فیلدها Dictionary<string, string> HMGet(string cacheKey, IList<string> fields); Task<Dictionary<string, string>> HMGetAsync(string cacheKey, IList<string> fields);
- متدهای List
// گرفتن یک مقدار از لیست مقادیر با شماره ایندکس آن T LIndex<T>(string cacheKey, long index); Task<T> LIndexAsync<T>(string cacheKey, long index); // گرفتن تعداد مقادیر در لیست یک کلید long LLen(string cacheKey); Task<long> LLenAsync(string cacheKey); // گرفتن اولین مقدار از مقادیر یک لیست در یک کلید T LPop<T>(string cacheKey); Task<T> LPopAsync<T>(string cacheKey); // ایجاد یک کلید که لیستی از مقادیر را پشتیبانی میکند و میتوانید هر بار مقدار جدید به لیست آن اضافه کنید long LPush<T>(string cacheKey, IList<T> cacheValues); Task<long> LPushAsync<T>(string cacheKey, IList<T> cacheValues); // گرفتن مقادیر یک لیست از داده بر اساس شماره ایندکس شروع و پایان برای مثال مقادیر ۳ تا ۷ از ۱۰ مقدار List<T> LRange<T>(string cacheKey, long start, long stop); Task<List<T>> LRangeAsync<T>(string cacheKey, long start, long stop); // حذف کردن مقادیر یک لیست بر اساس تعداد وارد شده که بعد از مقدار وارد شده شروع به شمارش میشود long LRem<T>(string cacheKey, long count, T cacheValue); Task<long> LRemAsync<T>(string cacheKey, long count, T cacheValue); // افزودن یک مقدار به لیستی از مقادیر یک کلید با گرفتن شماره ایندکس bool LSet<T>(string cacheKey, long index, T cacheValue); Task<bool> LSetAsync<T>(string cacheKey, long index, T cacheValue); // بررسی میکند که لیست مقداری برای شماره ایندکس شروع و پایان درون خودش دارد یا خیر bool LTrim(string cacheKey, long start, long stop); Task<bool> LTrimAsync(string cacheKey, long start, long stop); // https://redis.io/commands/lpushx long LPushX<T>(string cacheKey, T cacheValue); Task<long> LPushXAsync<T>(string cacheKey, T cacheValue); // https://redis.io/commands/linsert long LInsertBefore<T>(string cacheKey, T pivot, T cacheValue); Task<long> LInsertBeforeAsync<T>(string cacheKey, T pivot, T cacheValue); // https://redis.io/commands/linsert long LInsertAfter<T>(string cacheKey, T pivot, T cacheValue); Task<long> LInsertAfterAsync<T>(string cacheKey, T pivot, T cacheValue); // https://redis.io/commands/rpushx long RPushX<T>(string cacheKey, T cacheValue); Task<long> RPushXAsync<T>(string cacheKey, T cacheValue); // https://redis.io/commands/rpush long RPush<T>(string cacheKey, IList<T> cacheValues); Task<long> RPushAsync<T>(string cacheKey, IList<T> cacheValues); // https://redis.io/commands/rpop T RPop<T>(string cacheKey); Task<T> RPopAsync<T>(string cacheKey);
- متدهای Set
// https://redis.io/commands/SAdd long SAdd<T>(string cacheKey, IList<T> cacheValues, TimeSpan? expiration = null); Task<long> SAddAsync<T>(string cacheKey, IList<T> cacheValues, TimeSpan? expiration = null); // https://redis.io/commands/SCard long SCard(string cacheKey); Task<long> SCardAsync(string cacheKey); // https://redis.io/commands/SIsMember bool SIsMember<T>(string cacheKey, T cacheValue); Task<bool> SIsMemberAsync<T>(string cacheKey, T cacheValue); // https://redis.io/commands/SMembers List<T> SMembers<T>(string cacheKey); Task<List<T>> SMembersAsync<T>(string cacheKey); // https://redis.io/commands/SPop T SPop<T>(string cacheKey); Task<T> SPopAsync<T>(string cacheKey); // https://redis.io/commands/SRandMember List<T> SRandMember<T>(string cacheKey, int count = 1); Task<List<T>> SRandMemberAsync<T>(string cacheKey, int count = 1); // https://redis.io/commands/SRem long SRem<T>(string cacheKey, IList<T> cacheValues = null); Task<long> SRemAsync<T>(string cacheKey, IList<T> cacheValues = null);
- متدهای Stored Set
// https://redis.io/commands/ZAdd long ZAdd<T>(string cacheKey, Dictionary<T, double> cacheValues); Task<long> ZAddAsync<T>(string cacheKey, Dictionary<T, double> cacheValues); // https://redis.io/commands/ZCard long ZCard(string cacheKey); Task<long> ZCardAsync(string cacheKey); // https://redis.io/commands/ZCount long ZCount(string cacheKey, double min, double max); Task<long> ZCountAsync(string cacheKey, double min, double max); // https://redis.io/commands/ZIncrBy double ZIncrBy(string cacheKey, string field, double val = 1); Task<double> ZIncrByAsync(string cacheKey, string field, double val = 1); // https://redis.io/commands/ZLexCount long ZLexCount(string cacheKey, string min, string max); Task<long> ZLexCountAsync(string cacheKey, string min, string max); // https://redis.io/commands/ZRange List<T> ZRange<T>(string cacheKey, long start, long stop); Task<List<T>> ZRangeAsync<T>(string cacheKey, long start, long stop); // https://redis.io/commands/ZRank long? ZRank<T>(string cacheKey, T cacheValue); Task<long?> ZRankAsync<T>(string cacheKey, T cacheValue); // https://redis.io/commands/ZRem long ZRem<T>(string cacheKey, IList<T> cacheValues); Task<long> ZRemAsync<T>(string cacheKey, IList<T> cacheValues); // https://redis.io/commands/ZScore double? ZScore<T>(string cacheKey, T cacheValue); Task<double?> ZScoreAsync<T>(string cacheKey, T cacheValue);
- متدهای Hyperloglog
// https://redis.io/commands/PfAdd bool PfAdd<T>(string cacheKey, List<T> values); Task<bool> PfAddAsync<T>(string cacheKey, List<T> values); // https://redis.io/commands/PfCount long PfCount(List<string> cacheKeys); Task<long> PfCountAsync(List<string> cacheKeys); // https://redis.io/commands/PfMerge bool PfMerge(string destKey, List<string> sourceKeys); Task<bool> PfMergeAsync(string destKey, List<string> sourceKeys);
- متدهای Geo
// https://redis.io/commands/GeoAdd long GeoAdd(string cacheKey, List<(double longitude, double latitude, string member)> values); Task<long> GeoAddAsync(string cacheKey, List<(double longitude, double latitude, string member)> values); // https://redis.io/commands/GeoDist double? GeoDist(string cacheKey, string member1, string member2, string unit = "m"); Task<double?> GeoDistAsync(string cacheKey, string member1, string member2, string unit = "m"); // https://redis.io/commands/GeoHash List<string> GeoHash(string cacheKey, List<string> members); Task<List<string>> GeoHashAsync(string cacheKey, List<string> members); // https://redis.io/commands/GeoPos List<(decimal longitude, decimal latitude)?> GeoPos(string cacheKey, List<string> members); Task<List<(decimal longitude, decimal latitude)?>> GeoPosAsync(string cacheKey, List<string> members);
3 - پروایدر Hybrid :
این پروایدر، روشی از کشینگ را مابین local caching و distributed caching، ارائه میدهد و میتوانید از یک پروایدر Local مثل InMemory و پروایدر Distributed مثل Redis، همزمان باهم استفاده کنید که در یک کانال باهم و در راستای هم کار میکنند.
اما سوال اینجاست که این قابلیت دقیقا چه کاری انجام میدهد؟
همانطور که قبلا گفته شد، کش In-Memory سرعت بالاتری نسبت به کش Distributed دارد؛ اما دچار معایبی در حالت چند سروری هست که این معایب از جمله حذف شدن دیتای یک سرور، در صورت Down شدن آن، Sync نبودن کش سرورها باهم دیگر و دو نسخه، کش کردن دیتا در هر سرور و موارد دیگری که میتوان نام برد. اما از طرفی کش Distributed مشکلات چند سروری را با قرار دادن یک مرکزیت واحد کش در حافظه شبکه شده سرورها برطرف میکند و اطلاعات سرورها، از یک منبع خوانده میشود و طبعا مشکلات In-Memory را نخواهیم داشت؛ اما به دلیل رد و بدل شدن دیتا در محیط شبکه و عمل Serialize , Deserialize که هنگام عبور دیتا روی آن صورت میگیرد، بخشی از سرعت، کاهش خواهد یافت و درنهایت Performance کمتری را نسبت به In-Memory ارائه میدهد.
حالا برای اینکه بتوانیم سیستم کش خودمان را طوری طراحی کنیم که عیبهای (Local)In-Memory و Distributed را نداشته باشیم و بتوانیم از هریک به شکلی درست استفاده کنیم که هم اطلاعاتمان Sync باشد و هم از سرعت بالای In-Memory برخوردار شویم، میتوانیم از پروایدر Hybrid استفاده کنیم.
شیوه کار این پروایدر به این صورت است که وقتی برنامه برای بار اول به کش In-Memory درخواستی را ارسال میکند و کش مورد نظر در آن وجود ندارد، برنامه یک درخواست دیگر را به کش Distributed ارسال میکند و دیتای مورد نظر را به کاربر بازگشت میدهد و علاوه بر آن یک کپی از کش آن دیتا، در کش In-Memory هم ایجاد میکند. با این ساختار از دفعات بعد که کاربر درخواستی را ارسال کند، دیتای درخواستی در In-Memory نیز موجود خواهد بود و سریعتر از بار اول پاسخ را ارسال خواهد کرد.
از طرفی نیز وقتی کاربر دیتای جدیدی را ذخیره میکند، ابتدا آن دیتا در In-Memory کش شده و سپس با درخواست خود پروایدر، در کش Distributed هم اعمال میشود تا در نهایت دیتابیس نیز آن را ذخیره کند.
وقتی این اتفاق میافتد، پروایدر Hybrid با کمک پکیج Bus.Redis به کش In-Memory سرورهای دیگر دستور Pull کردن دیتا کشهای جدید را ارسال میکند و در نهایت همه سرورها نیز به کمک Distributed مرکزی باهم Sync خواهند بود.
برای فعال سازی این پروایدر باید پکیجهای زیر را در برنامه خود نصب کنید:
Install-Package EasyCaching.HybridCache Install-Package EasyCaching.InMemory Install-Package EasyCaching.Redis Install-Package EasyCaching.Bus.Redis
در این مجموعه از پکیجها، از یک پروایدر Local(InMemory) و یک پروایدر distributed(Redis) استفاده شده و همانطور که گفته شد، مدیریت هماهنگ سازی این دو، توسط پکیج دیگری بنام EasyCaching.Bus.Redis صورت میگیرد.
تنظیمات فعالسازی این پروایدر هم متشکل از تنظیمات دو پروایدر In-Memory و Redis، بعلاوه معرفی این دو به هم در متد UseHybrid خواهد بود.
services.AddEasyCaching(option => // local option.UseInMemory("c1"); // distributed option.UseRedis(config => config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379)); }, "c2"); // combine local and distributed option.UseHybrid(config => // specify the local cache provider name after v0.5.4 config.LocalCacheProviderName = "c1" // specify the distributed cache provider name after v0.5.4 config.DistributedCacheProviderName = "c2" }); // use redis bus .WithRedisBus(busConf => busConf.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379)); }); });
برای استفاده از این پروایدر، متفاوت با پروایدرهای قبلی، باید اینترفیس IHybridCachingProvider را فراخوانی کنیم. متدهای موجود در این اینترفیس، همان متدهایی است که در اینترفیس IEasyCachingProvider وجود دارند و از نظر نام متد و روش استفاده، تفاوتی میان آن نیست.
پیشنهاد شخصی در Distributed Cacheها
همانطور که گفته شد Distributed کشها، گزینه مناسبتری برای برنامههای چند سروری هستند؛ اما در این حالت مواردی مثل Round Trip شبکه و جابجایی اطلاعات در این محیط بعلاوه Serialize , Deserialize هایی که باید انجام شوند دلیلی میشود تا سرعت آن در پاسخ به درخواستهای برنامه، نسبت به حالت تک سروری(In-Memory) کمتر باشد. Hybrid Provider یکی از روشهای حل این مشکل بوده که معرفی کردیم. اما برای اینکه تیر خلاص را به پیکره سیستم Distributed Cache خود بزنید و تریک فنی آخر را نیز روی آن اجرا کنید، پیشنهاد میکنم از پکیج EasyCaching.Extensions.EasyCompressor که بر پایه پکیج EasyCaching نوشته شده استفاده کنید. این پکیج، اطلاعات را قبل از کش شدن، فشرده سازی میکند و حجم اطلاعات را به طور محسوسی کاهش میدهد که میزان فضای اشغالی Ram را کم کرده و همچنین عمل جابجایی اطلاعات را نیز تسریع میبخشد. میتوانید از این پکیج هم در Redis و هم در Hybrid استفاده کنید. چگونگی استفاده از آن نیز در لینک Github ذکر شده موجود است.
معرفی پروژه
تا اینجا با مفاهیمی که برای شروع استفاده حرفهای از کش در پروژهتان نیاز بود، آشنا شدید. در پروژههای واقعی، میتوانیم از این سیستم به روشهای مختلفی در سطوح مختلفی از برنامه استفاده کنیم؛ برای مثال کدهای مربوط به عملیات کش را میتوان بصورت سادهای در هر کنترلر تزریق و در اکشنها استفاده کرد؛ یا از لایه کنترلر، آن را به لایه سرویس منتقل کرد. در روشی دیگر میتوانیم یک Attribute را برای این عمل در نظر بگیریم و یا اینکه آن را بصورت یک Middleware اختصاصی در برنامه پیاده کنیم.
در این پروژه علاوه بر اینکه سعی کردهام استفاده از Providerهای معرفی شده را در محیط واقعیتر پیاده سازی کنم، در هر پروژه از این Solution، کش را به شیوهای متفاوت در لایههای مختلفی از برنامه قرار دادهام تا شما همراهان بتوانید طبق نیازتان از روشی مناسب و بهینه در پروژههای واقعی خود از آن استفاده کنید.