تطبيقات C # في C # .NET

مرحبا يا هابر! تحسبًا لبدء الدورة "C # ASP.NET Core Developer" ، قمنا بإعداد ترجمة لمواد مثيرة للاهتمام حول تنفيذ ذاكرة التخزين المؤقت في C #. استمتع بالقراءة.



يعد التخزين المؤقت أحد الأنماط الأكثر استخدامًا في تطوير البرامج . إنه مفهوم بسيط وفعال في نفس الوقت. الفكرة هي إعادة استخدام نتائج العمليات المنجزة. بعد عملية تستغرق وقتًا طويلاً ، نحفظ النتيجة في حاوية ذاكرة التخزين المؤقت . في المرة القادمة التي نحتاج فيها إلى هذه النتيجة ، سنستخرجها من حاوية ذاكرة التخزين المؤقت ، بدلاً من الاضطرار إلى إجراء عملية شاقة مرة أخرى.

على سبيل المثال ، للحصول على صورة رمزية للمستخدم ، قد تحتاج إلى طلبها من قاعدة البيانات. بدلاً من تنفيذ الطلب مع كل مكالمة ، سنقوم بحفظ هذه الصورة الرمزية في ذاكرة التخزين المؤقت ، واستخراجها من الذاكرة في كل مرة تحتاج إليها.

التخزين المؤقت رائع للبيانات التي تتغير بشكل غير منتظم. أو ، من الناحية المثالية ، لا تتغير أبدًا. يجب عدم تخزين البيانات التي تتغير باستمرار ، على سبيل المثال ، الوقت الحالي ، في ذاكرة التخزين المؤقت ، وإلا فإنك تخاطر بالحصول على نتائج غير صحيحة.

ذاكرة التخزين المؤقت المحلية وذاكرة التخزين المؤقت المحلية الثابتة وذاكرة التخزين المؤقت الموزعة


هناك 3 أنواع من ذاكرة التخزين المؤقت:

  • يُستخدم In-Memory Cache في الحالات التي تحتاج فيها فقط إلى تنفيذ ذاكرة التخزين المؤقت في عملية واحدة. عندما تموت العملية ، تموت ذاكرة التخزين المؤقت معها. إذا كنت تقوم بتشغيل نفس العملية على خوادم متعددة ، فسيكون لديك ذاكرة تخزين مؤقت منفصلة لكل خادم.
  • ذاكرة التخزين المؤقت المستمرة قيد المعالجة - يحدث ذلك عند عمل نسخة احتياطية من ذاكرة التخزين المؤقت خارج ذاكرة العملية. يمكن وضعه في ملف أو في قاعدة بيانات. إنه أكثر تعقيدًا من ذاكرة التخزين المؤقت في الذاكرة ، ولكن إذا تمت إعادة تشغيل العملية ، فلن يتم مسح ذاكرة التخزين المؤقت. الأنسب للحالات التي يكون فيها الحصول على عنصر مخبأ باهظ الثمن وتميل عمليتك إلى إعادة التشغيل بشكل متكرر.
  • ذاكرة التخزين المؤقت الموزعة هي عندما تحتاج إلى ذاكرة تخزين مؤقت مشتركة لأجهزة متعددة. عادة ما تكون هذه خوادم عديدة. يتم تخزين ذاكرة التخزين المؤقت الموزعة في خدمة خارجية. هذا يعني أنه في حالة احتفاظ خادم واحد بعنصر ذاكرة التخزين المؤقت ، يمكن للخوادم الأخرى استخدامه أيضًا. خدمات مثل Redis رائعة لهذا.

سنتحدث فقط عن ذاكرة التخزين المؤقت المحلية .

التنفيذ البدائي


لنبدأ بإنشاء تطبيق تخزين مؤقت بسيط للغاية في C #:

public class NaiveCache<TItem>
{
    Dictionary<object, TItem> _cache = new Dictionary<object, TItem>();
 
    public TItem GetOrCreate(object key, Func<TItem> createItem)
    {
        if (!_cache.ContainsKey(key))
        {
            _cache[key] = createItem();
        }
        return _cache[key];
    }
}

باستخدام:

var _avatarCache = new NaiveCache<byte[]>();
// ...
var myAvatar = _avatarCache.GetOrCreate(userId, () => _database.GetAvatar(userId));

يحل هذا الرمز البسيط مشكلة مهمة. للحصول على صورة رمزية للمستخدم ، سيكون الطلب الأول فقط هو الطلب الفعلي من قاعدة البيانات. يتم تخزين بيانات الصورة الرمزية ( byte []) نتيجة الطلب في ذاكرة العملية. سوف تسترد جميع طلبات الصورة الرمزية اللاحقة من الذاكرة ، مما يوفر الوقت والموارد.

ولكن ، مثل معظم الأشياء في البرمجة ، فإن الأمور ليست بهذه البساطة. التنفيذ أعلاه ليس حلاً جيدًا لعدد من الأسباب. من ناحية ، هذا التطبيق ليس خيط آمن . عند استخدامها من خيوط متعددة ، قد تحدث استثناءات. بالإضافة إلى ذلك ، ستبقى العناصر المخزنة مؤقتًا في الذاكرة إلى الأبد ، وهو أمر سيئ جدًا في الواقع.

هذا هو السبب في أننا يجب إزالة العناصر من ذاكرة التخزين المؤقت:

  1. يمكن أن تبدأ ذاكرة التخزين المؤقت في استهلاك الكثير من الذاكرة ، مما يؤدي في النهاية إلى استثناءات بسبب نقصها وتعطلها.
  2. يمكن أن يؤدي الاستهلاك المرتفع للذاكرة إلى ضغط الذاكرة (المعروف أيضًا باسم ضغط GC ). في هذه الحالة ، يعمل جامع القمامة أكثر مما ينبغي ، مما يقلل الأداء.
  3. قد يلزم تحديث ذاكرة التخزين المؤقت عند تغيير البيانات. يجب أن تدعم البنية التحتية للتخزين المؤقت لدينا هذه الميزة.

لحل هذه المشاكل الموجودة في أطر سياسات النزوح (المعروف أيضا باسم إزالة السياسة - سياسات الإخلاء / الإزالة ). هذه هي قواعد إزالة العناصر من ذاكرة التخزين المؤقت وفقًا للمنطق المحدد. من بين سياسات الإزالة الشائعة ما يلي:

  • سياسة انتهاء الصلاحية المطلقة التي تزيل عنصرًا من ذاكرة التخزين المؤقت بعد فترة زمنية محددة ، بغض النظر عن أي شيء.
  • سياسة Slide Exp انتهاء الصلاحية التي تزيل عنصرًا من ذاكرة التخزين المؤقت إذا لم يتم الوصول إليه لفترة زمنية معينة. بمعنى ، إذا قمت بتعيين وقت انتهاء الصلاحية على دقيقة واحدة ، فسيظل العنصر في ذاكرة التخزين المؤقت أثناء استخدامه كل 30 ثانية. إذا لم أستخدمه لأكثر من دقيقة ، فسيتم حذف العنصر.
  • سياسة تحديد الحجم ، والتي ستحد من حجم ذاكرة التخزين المؤقت.

الآن بعد أن اكتشفنا كل ما نحتاجه ، دعنا ننتقل إلى حلول أفضل.

حلول أفضل


إلى خيبة أملي الكبيرة كمدون ، أنشأت Microsoft بالفعل تطبيقًا رائعًا للتخزين المؤقت. حرمني هذا من متعة إنشاء تطبيق مماثل بنفسي ، ولكن على الأقل كتابة هذه المقالة أقل أيضًا.

سأوضح لك حل Microsoft ، وكيفية استخدامه بفعالية ، ثم كيفية تحسينه لبعض السيناريوهات.

System.Runtime.Caching / MemoryCache vs Microsoft.Extensions.Caching.Memory


لدى Microsoft حلان ، حزمتان تخزين مؤقت NuGet مختلفتان. كلاهما عظيم. وفقًا لتوصيات Microsoft ، يفضل استخدامه Microsoft.Extensions.Caching.Memory، لأنه يتكامل بشكل أفضل مع Asp. صافي النواة. يمكن دمجها بسهولة في آلية حقن التبعية Asp .NET Core.

هنا مثال بسيط مع Microsoft.Extensions.Caching.Memory:

public class SimpleMemoryCache<TItem>
{
    private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
 
    public TItem GetOrCreate(object key, Func<TItem> createItem)
    {
        TItem cacheEntry;
        if (!_cache.TryGetValue(key, out cacheEntry)) //    .
        {
            //    ,   .
            cacheEntry = createItem();
            
            //    . 
            _cache.Set(key, cacheEntry);
        }
        return cacheEntry;
    }
}

باستخدام:

var _avatarCache = new SimpleMemoryCache<byte[]>();
// ...
var myAvatar = _avatarCache.GetOrCreate(userId, () => _database.GetAvatar(userId));

إنه يذكرني جدا NaiveCache، فما الذي تغير؟ حسنًا ، أولاً ، إنه تطبيق آمن للخيط . يمكنك الاتصال به بأمان من سلاسل رسائل متعددة في وقت واحد.

ثانيًا ، MemoryCacheيأخذ في الاعتبار جميع سياسات الازدحام التي تحدثنا عنها سابقًا. هنا مثال:

IMemoryCache مع سياسات الاستباقية:

public class MemoryCacheWithPolicy<TItem>
{
    private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions()
    {
        SizeLimit = 1024
    });
 
    public TItem GetOrCreate(object key, Func<TItem> createItem)
    {
        TItem cacheEntry;
        if (!_cache.TryGetValue(key, out cacheEntry))//    .
        {
            //    ,   . 
            cacheEntry = createItem();
 
            var cacheEntryOptions = new MemoryCacheEntryOptions()
         	.SetSize(1)// 
         	//        (  )
                .SetPriority(CacheItemPriority.High)
                //       ,    .
                 .SetSlidingExpiration(TimeSpan.FromSeconds(2))
                //       ,    .
                .SetAbsoluteExpiration(TimeSpan.FromSeconds(10));
 
            //    .
            _cache.Set(key, cacheEntry, cacheEntryOptions);
        }
        return cacheEntry;
    }
}

لنحلل العناصر الجديدة:

  1. تمت إضافة MemoryCacheOptions SizeLimit. هذا يضيف سياسة حد الحجم لحاوية التخزين المؤقت لدينا. لا تحتوي ذاكرة التخزين المؤقت على آلية لقياس حجم السجلات. لذلك ، نحتاج إلى تعيين حجم كل إدخال في ذاكرة التخزين المؤقت. في هذه الحالة ، في كل مرة نقوم بتعيين الحجم على 1 مع SetSize(1). هذا يعني أن ذاكرة التخزين المؤقت لدينا سيكون لها حد 1024 عنصر.
  2. , ? .SetPriority (CacheItemPriority.High). : Low (), Normal (), High () NeverRemove ( ).
  3. SetSlidingExpiration(TimeSpan.FromSeconds(2)) 2 . , 2 , .
  4. SetAbsoluteExpiration(TimeSpan.FromSeconds(10)) 10 . , 10 , .

بالإضافة إلى الخيارات الموجودة في المثال ، يمكنك أيضًا تعيين مفوض RegisterPostEvictionCallbackسيتم استدعاؤه عند حذف العنصر.

هذه مجموعة واسعة إلى حد ما من الوظائف ، ولكن ، مع ذلك ، نحتاج إلى التفكير فيما إذا كان هناك أي شيء آخر لإضافته. في الواقع هناك شيئان

المشاكل والميزات المفقودة


هناك العديد من الأجزاء المهمة المفقودة من هذا التطبيق.

  1. بينما يمكنك تعيين حد الحجم ، فإن التخزين المؤقت لا يتحكم بالفعل في ضغط الذاكرة. إذا قمنا بالمراقبة ، فيمكننا تشديد السياسة بالضغط العالي وإضعاف السياسة مع انخفاض.
  2. , . . , , , 10 . 2 , , ( ), .

فيما يتعلق بالمشكلة الأولى للضغط على gc: من الممكن التحكم في الضغط على gc بعدة طرق واستدلال. لا يتعلق هذا المنشور بهذا الأمر ، ولكن يمكنك قراءة مقالتي "البحث عن مشاكل الذاكرة وإصلاحها في C # .NET: 8 أفضل الممارسات وإصلاحها ومنعها" للتعرف على بعض الطرق المفيدة.

المشكلة الثانية أسهل في حلها . في الواقع ، هنا تنفيذ MemoryCacheيحلها بالكامل:

public class WaitToFinishMemoryCache<TItem>
{
    private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
    private ConcurrentDictionary<object, SemaphoreSlim> _locks = new ConcurrentDictionary<object, SemaphoreSlim>();
 
    public async Task<TItem> GetOrCreate(object key, Func<Task<TItem>> createItem)
    {
        TItem cacheEntry;
 
        if (!_cache.TryGetValue(key, out cacheEntry))//    .
        {
            SemaphoreSlim mylock = _locks.GetOrAdd(key, k => new SemaphoreSlim(1, 1));
 
            await mylock.WaitAsync();
            try
            {
                if (!_cache.TryGetValue(key, out cacheEntry))
                {
                    //    ,   .
                    cacheEntry = await createItem();
                    _cache.Set(key, cacheEntry);
                }
            }
            finally
            {
                mylock.Release();
            }
        }
        return cacheEntry;
    }
}

باستخدام:

var _avatarCache = new WaitToFinishMemoryCache<byte[]>();
// ...
var myAvatar =
await _avatarCache.GetOrCreate(userId, async () => await _database.GetAvatar(userId));

في هذا التطبيق ، عند محاولة الحصول على عنصر ، إذا كان نفس العنصر بالفعل قيد الإنشاء بواسطة مؤشر ترابط آخر ، فسوف تنتظر حتى يكتمل مؤشر الترابط الأول. ثم ستحصل على عنصر مخبأ بالفعل تم إنشاؤه بواسطة مؤشر ترابط آخر.

تحليل الكود


يمنع هذا التطبيق إنشاء عنصر. يحدث التأمين على مفتاح. على سبيل المثال ، إذا كنا ننتظر الصورة الرمزية لـ Alexey ، فلا يزال بإمكاننا الحصول على القيم المخبأة لـ Zhenya أو Barbara في موضوع آخر. يخزن

القاموس _locksجميع الأقفال. لا تعمل الأقفال العادية async/await، لذلك نحن بحاجة إلى استخدام SemaphoreSlim .

هناك نوعان من التحقق للتحقق مما إذا كانت القيمة مخزنة مؤقتًا بالفعل إذا (!_Cache.TryGetValue(key, out cacheEntry)). الذي في القفل هو الذي يوفر الإنشاء الوحيد للعنصر. واحد خارج القفل ، للتحسين.

متى يجب استخدام WaitToFinishMemoryCache


من الواضح أن هذا التنفيذ لديه بعض النفقات العامة. دعنا نلقي نظرة عندما يكون ذا صلة.

استخدم WaitToFinishMemoryCacheعندما:

  • عندما يكون لوقت إنشاء العنصر أي قيمة ، وتريد تقليل عدد الإبداعات قدر الإمكان.
  • عندما يكون وقت إنشاء عنصر طويلاً جدًا.
  • متى يجب إنشاء عنصر مرة واحدة لكل مفتاح.

لا تستخدمه WaitToFinishMemoryCacheعندما:

  • لا يوجد خطر من أن سلاسل رسائل متعددة ستتمكن من الوصول إلى نفس عنصر ذاكرة التخزين المؤقت.
  • أنت لست قاطعًا ضد إنشاء العناصر أكثر من مرة. على سبيل المثال ، إذا كان استعلام إضافي لقاعدة البيانات لا يؤثر بشكل كبير على أي شيء.

ملخص


يعد التخزين المؤقت نمطًا قويًا جدًا. كما أنها خطرة ولها عيوبها. التخزين المؤقت كثيرًا ويمكن أن يسبب ضغطًا على GC. ذاكرة التخزين المؤقت قليلة جدًا ويمكن أن تسبب مشاكل في الأداء. هناك أيضًا ذاكرة تخزين مؤقت موزعة ، والتي تمثل عالمًا جديدًا بالكامل لاستكشافه. هذا هو تطوير البرمجيات ، هناك دائمًا شيء جديد يمكن إتقانه.

آمل أن يحظى هذا المقال بإعجابكم. إذا كنت مهتمًا بإدارة الذاكرة ، فسوف تركز مقالتي التالية على مخاطر الضغط على GC وكيفية منعه ، لذا قم بالتسجيل . استمتع بالترميز الخاص بك.



تعلم المزيد عن الدورة.



All Articles