Pola penguncian untuk penggunaan yang benar dari .NET MemoryCache

115

Saya berasumsi kode ini memiliki masalah konkurensi:

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        expensiveString = MemoryCache.Default[CacheKey] as string;
    }
    else
    {
        CacheItemPolicy cip = new CacheItemPolicy()
        {
            AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
        };
        expensiveString = SomeHeavyAndExpensiveCalculation();
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
    }
    return expensiveString;
}

Alasan masalah konkurensi adalah bahwa beberapa utas bisa mendapatkan kunci null dan kemudian mencoba memasukkan data ke cache.

Apa cara terpendek dan terbersih untuk membuat kode ini menjadi bukti konkurensi? Saya suka mengikuti pola yang baik di seluruh kode terkait cache saya. Tautan ke artikel online akan sangat membantu.

MEMPERBARUI:

Saya membuat kode ini berdasarkan jawaban @Scott Chamberlain. Adakah yang bisa menemukan masalah kinerja atau konkurensi dengan ini? Jika berhasil, ini akan menghemat banyak baris kode dan kesalahan.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Caching;

namespace CachePoc
{
    class Program
    {
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        {
            string xyzData = MemoryCacheHelper.GetCachedData<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedData<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
        }

        private static string SomeHeavyAndExpensiveXYZCalculation() {return "Expensive";}
        private static string SomeHeavyAndExpensiveABCCalculation() {return "Expensive";}

        public static class MemoryCacheHelper
        {
            public static T GetCachedData<T>(string cacheKey, object cacheLock, int cacheTimePolicyMinutes, Func<T> GetData)
                where T : class
            {
                //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                if (cachedData != null)
                {
                    return cachedData;
                }

                lock (cacheLock)
                {
                    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
                    cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                    if (cachedData != null)
                    {
                        return cachedData;
                    }

                    //The value still did not exist so we now write it in to the cache.
                    CacheItemPolicy cip = new CacheItemPolicy()
                    {
                        AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(cacheTimePolicyMinutes))
                    };
                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, cip);
                    return cachedData;
                }
            }
        }
    }
}
Allan Xu
sumber
3
kenapa tidak kamu gunakan ReaderWriterLockSlim?
DarthVader
2
Saya setuju dengan DarthVader ... Saya pikir Anda ramping ReaderWriterLockSlim... Tapi saya juga akan menggunakan teknik ini untuk menghindari try-finallypernyataan.
poy
1
Untuk versi terbaru Anda, saya tidak akan mengunci satu cacheLock lagi, saya akan mengunci per kunci sebagai gantinya. Ini dapat dengan mudah dilakukan dengan Dictionary<string, object>kunci di mana kuncinya adalah kunci yang sama yang Anda gunakan di Anda MemoryCachedan objek dalam kamus hanyalah dasar yang ObjectAnda kunci. Namun, karena itu, saya akan menyarankan Anda membaca jawaban Jon Hanna. Tanpa pembuatan profil yang tepat Anda mungkin memperlambat program Anda lebih banyak dengan penguncian daripada membiarkan dua contoh SomeHeavyAndExpensiveCalculation()berjalan dan satu hasil dibuang.
Scott Chamberlain
1
Menurut saya, membuat CacheItemPolicy setelah mendapatkan nilai mahal ke cache akan lebih akurat. Dalam skenario kasus terburuk seperti membuat laporan ringkasan yang membutuhkan 21 menit untuk mengembalikan "string mahal" (mungkin berisi nama file dari laporan PDF) sudah "kedaluwarsa" sebelum dikembalikan.
Wonderbird
1
@Wonderbird Poin yang bagus, saya memperbarui jawaban saya untuk melakukan itu.
Scott Chamberlain

Jawaban:

91

Ini adalah iterasi kedua saya dari kode tersebut. Karena MemoryCachethread safe, Anda tidak perlu mengunci pembacaan awal, Anda cukup membaca dan jika cache mengembalikan null, lakukan pemeriksaan kunci untuk melihat apakah Anda perlu membuat string. Ini sangat menyederhanakan kode.

const string CacheKey = "CacheKey";
static readonly object cacheLock = new object();
private static string GetCachedData()
{

    //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
    var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

    if (cachedString != null)
    {
        return cachedString;
    }

    lock (cacheLock)
    {
        //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
        cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The value still did not exist so we now write it in to the cache.
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        CacheItemPolicy cip = new CacheItemPolicy()
                              {
                                  AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
                              };
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
        return expensiveString;
    }
}

EDIT : Kode di bawah ini tidak diperlukan tetapi saya ingin membiarkannya menunjukkan metode aslinya. Ini mungkin berguna bagi pengunjung di masa mendatang yang menggunakan koleksi berbeda yang memiliki thread aman membaca tetapi menulis aman non-thread (hampir semua kelas di bawah System.Collectionsnamespace seperti itu).

Berikut adalah cara saya melakukannya menggunakan ReaderWriterLockSlimuntuk melindungi akses. Anda perlu melakukan semacam " Penguncian Tercentang Ganda " untuk melihat apakah ada orang lain yang membuat item yang di-cache sementara kami menunggu untuk mengambil kunci.

const string CacheKey = "CacheKey";
static readonly ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
static string GetCachedData()
{
    //First we do a read lock to see if it already exists, this allows multiple readers at the same time.
    cacheLock.EnterReadLock();
    try
    {
        //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }
    }
    finally
    {
        cacheLock.ExitReadLock();
    }

    //Only one UpgradeableReadLock can exist at one time, but it can co-exist with many ReadLocks
    cacheLock.EnterUpgradeableReadLock();
    try
    {
        //We need to check again to see if the string was created while we where waiting to enter the EnterUpgradeableReadLock
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The entry still does not exist so we need to create it and enter the write lock
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        cacheLock.EnterWriteLock(); //This will block till all the Readers flush.
        try
        {
            CacheItemPolicy cip = new CacheItemPolicy()
            {
                AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
            };
            MemoryCache.Default.Set(CacheKey, expensiveString, cip);
            return expensiveString;
        }
        finally 
        {
            cacheLock.ExitWriteLock();
        }
    }
    finally
    {
        cacheLock.ExitUpgradeableReadLock();
    }
}
Scott Chamberlain
sumber
1
@DarthVader dengan cara apa kode di atas tidak berfungsi? juga ini tidak ketat "penguncian ganda" Saya hanya mengikuti pola yang sama dan itu adalah cara terbaik yang dapat saya pikirkan untuk menggambarkannya. Itulah mengapa saya mengatakan itu semacam penguncian ganda.
Scott Chamberlain
Saya tidak mengomentari kode Anda. Saya berkomentar bahwa Penguncian Periksa Ganda Tidak berfungsi. Kode Anda baik-baik saja.
DarthVader
1
Saya merasa sulit untuk melihat situasi apa dari penguncian semacam ini dan penyimpanan semacam ini yang masuk akal: Jika Anda mengunci semua kreasi nilai, MemoryCachekemungkinan besar setidaknya salah satu dari dua hal itu salah.
Jon Hanna
@ScottChamberlain hanya melihat kode ini, dan bukankah itu rentan terhadap pengecualian yang dilemparkan antara perolehan kunci dan blok percobaan. Penulis C # In a Nutshell membahas hal ini di sini, albahari.com/threading/part2.aspx#_MonitorEnter_and_MonitorExit
BrutalSimplicity
9
Kelemahan dari kode ini adalah bahwa CacheKey "A" akan memblokir permintaan ke CacheKey "B" jika keduanya belum di-cache. Untuk mengatasi ini, Anda bisa menggunakan concurrentDictionary <string, object> di mana Anda menyimpan kunci cache untuk dikunci
MichaelD
44

Ada perpustakaan sumber terbuka [penafian: yang saya tulis]: LazyCache bahwa IMO mencakup kebutuhan Anda dengan dua baris kode:

IAppCache cache = new CachingService();
var cachedResults = cache.GetOrAdd("CacheKey", 
  () => SomeHeavyAndExpensiveCalculation());

Ini telah dibangun dalam penguncian secara default sehingga metode yang dapat di-cache hanya akan dijalankan satu kali per cache yang hilang, dan menggunakan lambda sehingga Anda dapat melakukan "dapatkan atau tambah" sekaligus. Secara default, kedaluwarsa geser 20 menit.

Bahkan ada paket NuGet ;)

alastairtree.dll
sumber
4
Dapper caching.
Charles Burns
3
Ini memungkinkan saya menjadi pengembang yang malas yang menjadikan ini jawaban terbaik!
jdnew18
Artikel yang dirujuk halaman github untuk LazyCache cukup bagus untuk alasan di baliknya. alastaircrabtree.com/…
Rafael Merlin
2
Apakah itu mengunci per kunci atau per cache?
jjxtra
1
@DirkBoer tidak itu tidak akan diblokir karena cara kunci dan malas digunakan di lazycache
alastairtree
30

Saya telah memecahkan masalah ini dengan menggunakan metode AddOrGetExisting di MemoryCache dan penggunaan inisialisasi Lazy .

Pada dasarnya, kode saya terlihat seperti ini:

static string GetCachedData(string key, DateTimeOffset offset)
{
    Lazy<String> lazyObject = new Lazy<String>(() => SomeHeavyAndExpensiveCalculationThatReturnsAString());
    var returnedLazyObject = MemoryCache.Default.AddOrGetExisting(key, lazyObject, offset); 
    if (returnedLazyObject == null)
       return lazyObject.Value;
    return ((Lazy<String>) returnedLazyObject).Value;
}

Skenario terburuk di sini adalah Anda membuat Lazyobjek yang sama dua kali. Tapi itu sangat sepele. Penggunaan AddOrGetExistingjaminan bahwa Anda hanya akan mendapatkan satu instance dari Lazyobjek, jadi Anda juga dijamin hanya akan memanggil metode inisialisasi mahal sekali.

Keith
sumber
4
Masalah dengan jenis pendekatan ini adalah Anda dapat memasukkan data yang tidak valid. Jika SomeHeavyAndExpensiveCalculationThatResultsAString()ada pengecualian, itu macet di cache. Bahkan pengecualian sementara akan disimpan dalam cache dengan Lazy<T>: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx
Scott Wegner
2
Memang benar bahwa Lazy <T> dapat mengembalikan kesalahan jika pengecualian inisialisasi gagal, itu adalah hal yang cukup mudah untuk dideteksi. Anda kemudian dapat mengeluarkan Lazy <T> apa pun yang menyelesaikan kesalahan dari cache, membuat Lazy <T> baru, memasukkannya ke dalam cache, dan mengatasinya. Dalam kode kami sendiri, kami melakukan hal serupa. Kami mencoba ulang beberapa kali sebelum kami membuat kesalahan.
Keith
12
AddOrGetExisting mengembalikan null jika item tidak ada, jadi Anda harus memeriksa dan mengembalikan lazyObject dalam kasus itu
Gian Marco
1
Menggunakan LazyThreadSafetyMode.PublicationOnly akan menghindari caching pengecualian.
Clement
2
Menurut komentar di posting blog ini jika sangat mahal untuk menginisialisasi entri cache, lebih baik hanya mengeluarkan pengecualian (seperti yang ditunjukkan pada contoh di posting blog) daripada menggunakan PublicationOnly, karena ada kemungkinan bahwa semua utas dapat memanggil penginisialisasi pada saat yang bersamaan.
bcr
15

Saya berasumsi kode ini memiliki masalah konkurensi:

Sebenarnya, itu sangat mungkin baik-baik saja, meskipun dengan kemungkinan peningkatan.

Sekarang, secara umum pola di mana kita memiliki beberapa utas yang mengatur nilai bersama pada penggunaan pertama, untuk tidak mengunci nilai yang diperoleh dan disetel dapat berupa:

  1. Bencana - kode lain akan menganggap hanya ada satu contoh.
  2. Disastrous - kode yang memperoleh instance tidak hanya dapat mentolerir satu (atau mungkin sejumlah kecil) operasi bersamaan.
  3. Bencana - cara penyimpanan tidak aman untuk thread (mis. Memiliki dua thread yang ditambahkan ke kamus dan Anda bisa mendapatkan semua jenis kesalahan yang tidak menyenangkan).
  4. Sub-optimal - kinerja keseluruhan lebih buruk daripada jika penguncian memastikan hanya satu utas yang melakukan pekerjaan untuk mendapatkan nilai.
  5. Optimal - biaya untuk memiliki banyak utas melakukan pekerjaan yang berlebihan lebih murah daripada biaya pencegahannya, terutama karena itu hanya dapat terjadi selama periode yang relatif singkat.

Namun, mengingat di sini yang MemoryCachedapat menggusur entri maka:

  1. Jika memiliki lebih dari satu contoh MemoryCachemerupakan bencana, maka pendekatan yang salah.
  2. Jika Anda harus mencegah pembuatan serentak, Anda harus melakukannya pada saat pembuatan.
  3. MemoryCache thread-safe dalam hal akses ke objek itu, jadi itu bukan masalah di sini.

Kedua kemungkinan ini tentunya harus dipikirkan, meskipun satu-satunya saat memiliki dua contoh dari string yang sama dapat menjadi masalah adalah jika Anda melakukan pengoptimalan khusus yang tidak berlaku di sini *.

Jadi, kami memiliki kemungkinan:

  1. Lebih murah untuk menghindari biaya panggilan ganda ke SomeHeavyAndExpensiveCalculation().
  2. Lebih murah untuk tidak menghindari biaya panggilan ganda ke SomeHeavyAndExpensiveCalculation().

Dan mengerjakannya bisa jadi sulit (memang, hal yang perlu diprofilkan daripada berasumsi Anda bisa menyelesaikannya). Perlu dipertimbangkan di sini meskipun cara paling jelas untuk mengunci penyisipan akan mencegah semua penambahan ke cache, termasuk yang tidak terkait.

Ini berarti bahwa jika kita memiliki 50 utas yang mencoba menetapkan 50 nilai berbeda, maka kita harus membuat semua 50 utas menunggu satu sama lain, meskipun mereka bahkan tidak akan melakukan perhitungan yang sama.

Karena itu, Anda mungkin lebih baik menggunakan kode yang Anda miliki, daripada dengan kode yang menghindari kondisi balapan, dan jika kondisi balapan menjadi masalah, Anda mungkin perlu menanganinya di tempat lain, atau memerlukan kode lain. strategi caching daripada yang membuang entri lama †.

Satu hal yang ingin saya ubah adalah saya akan mengganti panggilan ke Set()dengan satu ke AddOrGetExisting(). Dari penjelasan di atas, harus jelas bahwa itu mungkin tidak perlu, tetapi itu akan memungkinkan item yang baru diperoleh untuk dikumpulkan, mengurangi penggunaan memori secara keseluruhan dan memungkinkan rasio yang lebih tinggi dari koleksi generasi rendah ke generasi tinggi.

Jadi ya, Anda bisa menggunakan penguncian ganda untuk mencegah konkurensi, tetapi konkurensi sebenarnya bukan masalah, atau Anda menyimpan nilai dengan cara yang salah, atau penguncian ganda di penyimpanan bukanlah cara terbaik untuk menyelesaikannya. .

* Jika Anda mengetahui hanya satu dari setiap rangkaian string yang ada, Anda dapat mengoptimalkan perbandingan kesetaraan, yang kira-kira satu-satunya saat memiliki dua salinan string bisa salah daripada hanya sub-optimal, tetapi Anda ingin melakukannya jenis cache yang sangat berbeda agar masuk akal. Misalnya semacam itu XmlReaderdilakukan secara internal.

† Sangat mungkin salah satu yang menyimpan tanpa batas waktu, atau yang menggunakan referensi lemah sehingga hanya akan mengeluarkan entri jika tidak ada penggunaan.

Jon Hanna
sumber
1

Untuk menghindari kunci global, Anda dapat menggunakan SingletonCache untuk mengimplementasikan satu kunci per kunci, tanpa ledakan penggunaan memori (objek kunci dihapus ketika tidak lagi direferensikan, dan perolehan / rilis adalah thread yang aman, menjamin bahwa hanya 1 instance yang pernah digunakan melalui bandingkan dan bertukar).

Menggunakannya terlihat seperti ini:

SingletonCache<string, object> keyLocks = new SingletonCache<string, object>();

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        return MemoryCache.Default[CacheKey] as string;
    }

    // double checked lock
    using (var lifetime = keyLocks.Acquire(url))
    {
        lock (lifetime.Value)
        {
           if (MemoryCache.Default.Contains(CacheKey))
           {
              return MemoryCache.Default[CacheKey] as string;
           }

           cacheItemPolicy cip = new CacheItemPolicy()
           {
              AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
           };
           expensiveString = SomeHeavyAndExpensiveCalculation();
           MemoryCache.Default.Set(CacheKey, expensiveString, cip);
           return expensiveString;
        }
    }      
}

Kode ada di sini di GitHub: https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

Ada juga implementasi LRU yang bobotnya lebih ringan daripada MemoryCache, dan memiliki beberapa keunggulan - lebih cepat membaca dan menulis secara bersamaan, ukuran terbatas, tidak ada thread latar belakang, penghitung kinerja internal, dll. (Pelepasan tanggung jawab, saya menulisnya).

Alex Peck
sumber
0

Contoh konsol dari MemoryCache , "Cara menyimpan / mendapatkan objek kelas sederhana"

Output setelah meluncurkan dan menekan Any keykecuali Esc:

Menyimpan ke cache!
Mendapatkan dari cache!
Some1
Some2

    class Some
    {
        public String text { get; set; }

        public Some(String text)
        {
            this.text = text;
        }

        public override string ToString()
        {
            return text;
        }
    }

    public static MemoryCache cache = new MemoryCache("cache");

    public static string cache_name = "mycache";

    static void Main(string[] args)
    {

        Some some1 = new Some("some1");
        Some some2 = new Some("some2");

        List<Some> list = new List<Some>();
        list.Add(some1);
        list.Add(some2);

        do {

            if (cache.Contains(cache_name))
            {
                Console.WriteLine("Getting from cache!");
                List<Some> list_c = cache.Get(cache_name) as List<Some>;
                foreach (Some s in list_c) Console.WriteLine(s);
            }
            else
            {
                Console.WriteLine("Saving to cache!");
                cache.Set(cache_name, list, DateTime.Now.AddMinutes(10));                   
            }

        } while (Console.ReadKey(true).Key != ConsoleKey.Escape);

    }
fr0ga
sumber
0
public interface ILazyCacheProvider : IAppCache
{
    /// <summary>
    /// Get data loaded - after allways throw cached result (even when data is older then needed) but very fast!
    /// </summary>
    /// <param name="key"></param>
    /// <param name="getData"></param>
    /// <param name="slidingExpiration"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    T GetOrAddPermanent<T>(string key, Func<T> getData, TimeSpan slidingExpiration);
}

/// <summary>
/// Initialize LazyCache in runtime
/// </summary>
public class LazzyCacheProvider: CachingService, ILazyCacheProvider
{
    private readonly Logger _logger = LogManager.GetLogger("MemCashe");
    private readonly Hashtable _hash = new Hashtable();
    private readonly List<string>  _reloader = new List<string>();
    private readonly ConcurrentDictionary<string, DateTime> _lastLoad = new ConcurrentDictionary<string, DateTime>();  


    T ILazyCacheProvider.GetOrAddPermanent<T>(string dataKey, Func<T> getData, TimeSpan slidingExpiration)
    {
        var currentPrincipal = Thread.CurrentPrincipal;
        if (!ObjectCache.Contains(dataKey) && !_hash.Contains(dataKey))
        {
            _hash[dataKey] = null;
            _logger.Debug($"{dataKey} - first start");
            _lastLoad[dataKey] = DateTime.Now;
            _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
            _lastLoad[dataKey] = DateTime.Now;
           _logger.Debug($"{dataKey} - first");
        }
        else
        {
            if ((!ObjectCache.Contains(dataKey) || _lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) < DateTime.Now) && _hash[dataKey] != null)
                Task.Run(() =>
                {
                    if (_reloader.Contains(dataKey)) return;
                    lock (_reloader)
                    {
                        if (ObjectCache.Contains(dataKey))
                        {
                            if(_lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) > DateTime.Now)
                                return;
                            _lastLoad[dataKey] = DateTime.Now;
                            Remove(dataKey);
                        }
                        _reloader.Add(dataKey);
                        Thread.CurrentPrincipal = currentPrincipal;
                        _logger.Debug($"{dataKey} - reload start");
                        _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
                        _logger.Debug($"{dataKey} - reload");
                        _reloader.Remove(dataKey);
                    }
                });
        }
        if (_hash[dataKey] != null) return (T) (_hash[dataKey]);

        _logger.Debug($"{dataKey} - dummy start");
        var data = GetOrAdd(dataKey, getData, slidingExpiration);
        _logger.Debug($"{dataKey} - dummy");
        return (T)((object)data).CloneObject();
    }
}
art24war
sumber
LazyCache sangat cepat :) saya menulis kode ini untuk repositori REST API.
art24war
0

Ini agak terlambat, namun ... Implementasi penuh:

    [HttpGet]
    public async Task<HttpResponseMessage> GetPageFromUriOrBody(RequestQuery requestQuery)
    {
        log(nameof(GetPageFromUriOrBody), nameof(requestQuery));
        var responseResult = await _requestQueryCache.GetOrCreate(
            nameof(GetPageFromUriOrBody)
            , requestQuery
            , (x) => getPageContent(x).Result);
        return Request.CreateResponse(System.Net.HttpStatusCode.Accepted, responseResult);
    }
    static MemoryCacheWithPolicy<RequestQuery, string> _requestQueryCache = new MemoryCacheWithPolicy<RequestQuery, string>();

Ini getPageContenttandatangannya:

async Task<string> getPageContent(RequestQuery requestQuery);

Dan inilah MemoryCacheWithPolicyimplementasinya:

public class MemoryCacheWithPolicy<TParameter, TResult>
{
    static ILogger _nlogger = new AppLogger().Logger;
    private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions() 
    {
        //Size limit amount: this is actually a memory size limit value!
        SizeLimit = 1024 
    });

    /// <summary>
    /// Gets or creates a new memory cache record for a main data
    /// along with parameter data that is assocciated with main main.
    /// </summary>
    /// <param name="key">Main data cache memory key.</param>
    /// <param name="param">Parameter model that assocciated to main model (request result).</param>
    /// <param name="createCacheData">A delegate to create a new main data to cache.</param>
    /// <returns></returns>
    public async Task<TResult> GetOrCreate(object key, TParameter param, Func<TParameter, TResult> createCacheData)
    {
        // this key is used for param cache memory.
        var paramKey = key + nameof(param);

        if (!_cache.TryGetValue(key, out TResult cacheEntry))
        {
            // key is not in the cache, create data through the delegate.
            cacheEntry = createCacheData(param);
            createMemoryCache(key, cacheEntry, paramKey, param);

            _nlogger.Warn(" cache is created.");
        }
        else
        {
            // data is chached so far..., check if param model is same (or changed)?
            if(!_cache.TryGetValue(paramKey, out TParameter cacheParam))
            {
                //exception: this case should not happened!
            }

            if (!cacheParam.Equals(param))
            {
                // request param is changed, create data through the delegate.
                cacheEntry = createCacheData(param);
                createMemoryCache(key, cacheEntry, paramKey, param);
                _nlogger.Warn(" cache is re-created (param model has been changed).");
            }
            else
            {
                _nlogger.Trace(" cache is used.");
            }

        }
        return await Task.FromResult<TResult>(cacheEntry);
    }
    MemoryCacheEntryOptions createMemoryCacheEntryOptions(TimeSpan slidingOffset, TimeSpan relativeOffset)
    {
        // Cache data within [slidingOffset] seconds, 
        // request new result after [relativeOffset] seconds.
        return new MemoryCacheEntryOptions()

            // Size amount: this is actually an entry count per 
            // key limit value! not an actual memory size value!
            .SetSize(1)

            // Priority on removing when reaching size limit (memory pressure)
            .SetPriority(CacheItemPriority.High)

            // Keep in cache for this amount of time, reset it if accessed.
            .SetSlidingExpiration(slidingOffset)

            // Remove from cache after this time, regardless of sliding expiration
            .SetAbsoluteExpiration(relativeOffset);
        //
    }
    void createMemoryCache(object key, TResult cacheEntry, object paramKey, TParameter param)
    {
        // Cache data within 2 seconds, 
        // request new result after 5 seconds.
        var cacheEntryOptions = createMemoryCacheEntryOptions(
            TimeSpan.FromSeconds(2)
            , TimeSpan.FromSeconds(5));

        // Save data in cache.
        _cache.Set(key, cacheEntry, cacheEntryOptions);

        // Save param in cache.
        _cache.Set(paramKey, param, cacheEntryOptions);
    }
    void checkCacheEntry<T>(object key, string name)
    {
        _cache.TryGetValue(key, out T value);
        _nlogger.Fatal("Key: {0}, Name: {1}, Value: {2}", key, name, value);
    }
}

nloggerhanyalah nLogobjek untuk melacak MemoryCacheWithPolicyperilaku. Saya membuat ulang cache memori jika request object ( RequestQuery requestQuery) diubah melalui delegate ( Func<TParameter, TResult> createCacheData) atau membuat ulang saat geser atau waktu absolut mencapai batasnya. Perhatikan bahwa semuanya juga asinkron;)

Sam Saarian
sumber
Mungkin jawaban Anda lebih terkait dengan pertanyaan ini: Async threadsafe Dapatkan dari MemoryCache
Theodor Zoulias
Saya kira begitu, tapi masih pertukaran pengalaman yang berguna;)
Sam Saarian
0

Sulit untuk memilih mana yang lebih baik; lock atau ReaderWriterLockSlim. Anda membutuhkan statistik dunia nyata untuk membaca dan menulis angka dan rasio, dll.

Tetapi jika Anda yakin menggunakan "kunci" adalah cara yang benar. Lalu berikut adalah solusi berbeda untuk kebutuhan yang berbeda. Saya juga menyertakan solusi Allan Xu dalam kode. Karena keduanya bisa dibutuhkan untuk kebutuhan yang berbeda.

Berikut adalah persyaratannya, yang mendorong saya ke solusi ini:

  1. Anda tidak ingin atau tidak dapat menyediakan fungsi 'GetData' karena alasan tertentu. Mungkin fungsi 'GetData' terletak di beberapa kelas lain dengan konstruktor yang berat dan Anda bahkan tidak ingin membuat sebuah instance sampai memastikannya tidak dapat dihindari.
  2. Anda perlu mengakses data cache yang sama dari lokasi / tingkatan aplikasi yang berbeda. Dan lokasi yang berbeda tersebut tidak memiliki akses ke objek loker yang sama.
  3. Anda tidak memiliki kunci cache yang konstan. Sebagai contoh; perlu menyimpan beberapa data ke cache dengan kunci cache sessionId.

Kode:

using System;
using System.Runtime.Caching;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace CachePoc
{
    class Program
    {
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        {
            //Allan Xu's usage
            string xyzData = MemoryCacheHelper.GetCachedDataOrAdd<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedDataOrAdd<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);

            //My usage
            string sessionId = System.Web.HttpContext.Current.Session["CurrentUser.SessionId"].ToString();
            string yvz = MemoryCacheHelper.GetCachedData<string>(sessionId);
            if (string.IsNullOrWhiteSpace(yvz))
            {
                object locker = MemoryCacheHelper.GetLocker(sessionId);
                lock (locker)
                {
                    yvz = MemoryCacheHelper.GetCachedData<string>(sessionId);
                    if (string.IsNullOrWhiteSpace(yvz))
                    {
                        DatabaseRepositoryWithHeavyConstructorOverHead dbRepo = new DatabaseRepositoryWithHeavyConstructorOverHead();
                        yvz = dbRepo.GetDataExpensiveDataForSession(sessionId);
                        MemoryCacheHelper.AddDataToCache(sessionId, yvz, 5);
                    }
                }
            }
        }


        private static string SomeHeavyAndExpensiveXYZCalculation() { return "Expensive"; }
        private static string SomeHeavyAndExpensiveABCCalculation() { return "Expensive"; }

        public static class MemoryCacheHelper
        {
            //Allan Xu's solution
            public static T GetCachedDataOrAdd<T>(string cacheKey, object cacheLock, int minutesToExpire, Func<T> GetData) where T : class
            {
                //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                if (cachedData != null)
                    return cachedData;

                lock (cacheLock)
                {
                    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
                    cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                    if (cachedData != null)
                        return cachedData;

                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, DateTime.Now.AddMinutes(minutesToExpire));
                    return cachedData;
                }
            }

            #region "My Solution"

            readonly static ConcurrentDictionary<string, object> Lockers = new ConcurrentDictionary<string, object>();
            public static object GetLocker(string cacheKey)
            {
                CleanupLockers();

                return Lockers.GetOrAdd(cacheKey, item => (cacheKey, new object()));
            }

            public static T GetCachedData<T>(string cacheKey) where T : class
            {
                CleanupLockers();

                T cachedData = MemoryCache.Default.Get(cacheKey) as T;
                return cachedData;
            }

            public static void AddDataToCache(string cacheKey, object value, int cacheTimePolicyMinutes)
            {
                CleanupLockers();

                MemoryCache.Default.Add(cacheKey, value, DateTimeOffset.Now.AddMinutes(cacheTimePolicyMinutes));
            }

            static DateTimeOffset lastCleanUpTime = DateTimeOffset.MinValue;
            static void CleanupLockers()
            {
                if (DateTimeOffset.Now.Subtract(lastCleanUpTime).TotalMinutes > 1)
                {
                    lock (Lockers)//maybe a better locker is needed?
                    {
                        try//bypass exceptions
                        {
                            List<string> lockersToRemove = new List<string>();
                            foreach (var locker in Lockers)
                            {
                                if (!MemoryCache.Default.Contains(locker.Key))
                                    lockersToRemove.Add(locker.Key);
                            }

                            object dummy;
                            foreach (string lockerKey in lockersToRemove)
                                Lockers.TryRemove(lockerKey, out dummy);

                            lastCleanUpTime = DateTimeOffset.Now;
                        }
                        catch (Exception)
                        { }
                    }
                }

            }
            #endregion
        }
    }

    class DatabaseRepositoryWithHeavyConstructorOverHead
    {
        internal string GetDataExpensiveDataForSession(string sessionId)
        {
            return "Expensive data from database";
        }
    }

}
yvzman
sumber