.NET MemoryCache'nin doğru kullanımı için kilitleme kalıbı


115

Bu kodun eşzamanlılık sorunları olduğunu varsayıyorum:

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;
}

Eşzamanlılık sorununun nedeni, birden çok iş parçacığının bir boş anahtar alabilmesi ve ardından önbelleğe veri eklemeye çalışabilmesidir.

Bu kod eşzamanlılığını kanıtlamanın en kısa ve en temiz yolu nedir? Önbellekle ilgili kodumda iyi bir model izlemeyi seviyorum. Çevrimiçi bir makaleye bağlantı çok yardımcı olacaktır.

GÜNCELLEME:

Bu kodu @ Scott Chamberlain'in cevabına dayanarak buldum. Bununla ilgili herhangi bir performans veya eşzamanlılık sorunu bulabilen var mı? Bu işe yararsa, birçok satır kod ve hatayı kaydeder.

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;
                }
            }
        }
    }
}

3
neden u kullanmak Yüklü ReaderWriterLockSlim?
DarthVader

2
DarthVader ile aynı fikirdeyim ... Yalın olduğunu düşünürdüm ReaderWriterLockSlim... Ama bu tekniği try-finallyifadelerden kaçınmak için de kullanırdım .
poy

1
Güncellenmiş sürümünüz için, artık tek bir cacheLock'u kilitlemeyeceğim, bunun yerine anahtar başına kilitleyecektim. Bu, Dictionary<string, object>anahtarın sizde kullandığınız anahtarla aynı MemoryCacheolduğu ve sözlükteki nesnenin Objectkilitlediğiniz temel bir nesne olduğu bir ortamda kolayca yapılabilir . Ancak, Jon Hanna'nın cevabını okumanızı tavsiye ederim. Doğru profilleme olmadan, programınızı kilitleme ile iki çalışma örneğine izin vermektense SomeHeavyAndExpensiveCalculation()ve bir sonucun atılmasına izin vermekten daha fazla yavaşlatıyor olabilirsiniz .
Scott Chamberlain

1
Bana öyle geliyor ki, pahalı değeri önbelleğe aldıktan sonra CacheItemPolicy oluşturmak daha doğru olur. "Pahalı dizeyi" (belki PDF raporunun dosya adını içerir) döndürmek için 21 dakika süren bir özet raporu oluşturmak gibi en kötü durum senaryosu, iade edilmeden önce "süresi dolmuş" olacaktır.
Wonderbird

1
@Wonderbird İyi nokta, bunu yapmak için cevabımı güncelledim.
Scott Chamberlain

Yanıtlar:


91

Bu, kodun 2. yinelemesidir. İş MemoryCacheparçacığı güvenli olduğundan , ilk okumayı kilitlemenize gerek yoktur, sadece okuyabilirsiniz ve önbellek null dönerse, dizeyi oluşturmanız gerekip gerekmediğini görmek için kilit kontrolünü yapın. Kodu büyük ölçüde basitleştirir.

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;
    }
}

DÜZENLEME : Aşağıdaki kod gereksizdir ancak orijinal yöntemi göstermek için bırakmak istedim. İş parçacığı güvenli okumaları olan ancak iş parçacığı olmayan güvenli yazımları olan farklı bir koleksiyon kullanan gelecekteki ziyaretçiler için yararlı olabilir ( System.Collectionsad alanı altındaki neredeyse tüm sınıflar böyledir).

ReaderWriterLockSlimErişimi korumak için bunu nasıl yapacağım . Kilidi almak için beklerken, önbelleğe alınan öğeyi başka birinin oluşturup oluşturmadığını görmek için bir tür " Çift Kontrollü Kilitleme " yapmanız gerekir .

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();
    }
}

1
@DarthVader yukarıdaki kod ne şekilde çalışmaz? Ayrıca bu kesinlikle "iki kez kontrol edilmiş kilitleme" değildir Sadece benzer bir model izliyorum ve onu tarif etmenin en iyi yolu buydu. Bu yüzden bir çeşit çift ​​kontrollü kilitleme olduğunu söyledim .
Scott Chamberlain

Kodunuz hakkında yorum yapmadım. Double Check Locking'in çalışmadığını söylüyordum. Kodunuz iyi.
DarthVader

1
Bu tür bir kilitlemenin ve bu tür bir depolamanın hangi durumlarda mantıklı olacağını anlamakta zorlanıyorum: Tüm değer yaratımlarına kilitleniyorsanız, bir MemoryCacheşansa giriyorsanız , bu iki şeyden en az biri yanlıştı.
Jon Hanna

@ScottChamberlain sadece bu koda bakıyor ve kilidin edinilmesi ile deneme bloğu arasında atılan bir istisnaya duyarlı değil mi? C # In a Nutshell kitabının yazarı bunu burada tartışıyor, albahari.com/threading/part2.aspx#_MonitorEnter_and_MonitorExit
BrutalSimplicity

9
Bu kodun bir dezavantajı, CacheKey "A" nın, her ikisi de henüz önbelleğe alınmamışsa, CacheKey "B" ye yapılan isteği engellemesidir. Bunu çözmek için, kilitlenecek önbellekleri sakladığınız bir concurrentDictionary <string, object> kullanabilirsiniz
MichaelD

44

Açık kaynak kodlu bir kitaplık var [feragatname: yazdığım]: LazyCache , IMO'nun iki satır kodla gereksinimlerinizi karşılıyor :

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

Önbelleğe alınabilir yöntem, önbellek başına yalnızca bir kez çalıştırılacağı için varsayılan olarak yerleşik kilitlemeye sahiptir ve tek seferde "alma veya ekleme" yapabilmeniz için bir lambda kullanır. Varsayılan olarak 20 dakikalık kayan süre sonu.

Bir NuGet paketi bile var ;)


4
Önbelleğe alma şıklığı.
Charles Burns

3
Bu, bunu en iyi cevap yapan tembel bir geliştirici olmamı sağlıyor!
jdnew18

LazyCache için github sayfasının işaret ettiği makaleden bahsetmeye değer, arkasındaki nedenlerden dolayı oldukça iyi bir okuma. alastaircrabtree.com/…
Rafael Merlin

2
Anahtar başına mı yoksa önbellek başına mı kilitlenir?
jjxtra

1
@DirkBoer hayır, lazycache'de kilitlerin ve tembellerin kullanılma şekli nedeniyle engellenmeyecek
alastairtree

30

Bu sorunu , MemoryCache'de AddOrGetExisting yöntemini ve Lazy başlatma özelliğini kullanarak çözdüm .

Esasen, kodum şuna benzer:

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;
}

Buradaki en kötü durum senaryosu, aynı Lazynesneyi iki kez oluşturmanızdır . Ama bu oldukça önemsiz. Kullanımı AddOrGetExisting, Lazynesnenin yalnızca bir örneğini alacağınızı garanti eder ve bu nedenle pahalı başlatma yöntemini yalnızca bir kez çağırmanız garanti edilir.


4
Bu tür bir yaklaşımla ilgili sorun, geçersiz veriler girebilmenizdir. Bir SomeHeavyAndExpensiveCalculationThatResultsAString()istisna atılırsa, önbellekte sıkışır. Geçici istisnalar bile şu şekilde önbelleğe alınacak Lazy<T>: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx
Scott Wegner

2
Lazy <T> 'nin, başlatma istisnası başarısız olursa bir hata döndürebileceği doğru olsa da, bu tespit edilmesi oldukça kolay bir şeydir. Ardından, önbellekten bir hataya dönüşen herhangi bir Lazy <T> öğesini çıkarabilir, yeni bir Lazy <T> oluşturabilir, bunu önbelleğe yerleştirebilir ve çözebilirsiniz. Kendi kodumuzda benzer bir şey yapıyoruz. Bir hata atmadan önce belirli sayıda yeniden deneriz.
Keith

12
AddOrGetExisting, öğe mevcut değilse boş döndürür, bu nedenle bu durumda lazyObject'i kontrol edip iade etmelisiniz
Gian Marco

1
LazyThreadSafetyMode.PublicationOnly kullanılması, istisnaların önbelleğe alınmasını önler.
Clement

2
Bu blog gönderisindeki yorumlara göre , önbellek girişini başlatmak çok pahalıysa, PublicationOnly kullanmaktansa bir istisnayı çıkarmak daha iyidir (blog gönderisindeki örnekte gösterildiği gibi), çünkü tüm iş parçacıkları aynı anda başlatıcıyı çağırabilir.
bcr

15

Bu kodun eşzamanlılık sorunları olduğunu varsayıyorum:

Aslında, olası bir iyileştirme ile olsa da, muhtemelen iyi.

Şimdi, genel olarak, elde edilen ve ayarlanan değere kilitlenmemek için ilk kullanımda paylaşılan bir değer ayarlayan birden çok iş parçacığına sahip olduğumuz model şu şekilde olabilir:

  1. Felaket - diğer kod yalnızca bir örneğin var olduğunu varsayacaktır.
  2. Felaket - örneği alan kod, yalnızca bir (veya belki de belirli bir az sayıdaki) eşzamanlı işlemi tolere edemez.
  3. Felaket - depolama araçları iş parçacığı için güvenli değildir (örneğin, bir sözlüğe eklenen iki iş parçacığı varsa ve her türlü kötü hatayı alabilirsiniz).
  4. Optimum altı - genel performans, kilitlemenin değeri elde etmek için yalnızca bir iş parçacığının çalışmasını sağladığından daha kötüdür.
  5. Optimal - birden fazla iş parçacığının fazladan iş yapmasının maliyeti, bunu önleme maliyetinden daha azdır, özellikle de bu yalnızca nispeten kısa bir süre içinde gerçekleşebilir.

Ancak, burada dikkate alındığında MemoryCache durum girişleri kaldırabilir:

  1. Birden fazla örneğe sahip olmak felaketse, o zaman MemoryCache yanlış yaklaşımdır.
  2. Eşzamanlı yaratmayı engellemeniz gerekiyorsa, bunu yaratma noktasında yapmalısınız.
  3. MemoryCache bu nesneye erişim açısından iş parçacığı açısından güvenlidir, bu nedenle burada bir endişe kaynağı değildir.

Elbette bu olasılıkların her ikisi de düşünülmelidir, ancak aynı dizenin iki örneğinin mevcut olması bir sorun olabilir, burada geçerli olmayan çok özel optimizasyonlar yapıyorsanız *.

Yani, olasılıklarla baş başa kaldık:

  1. Yinelenen aramaların maliyetinden kaçınmak daha ucuzdur SomeHeavyAndExpensiveCalculation() .
  2. Tekrarlanan aramaların maliyetinden kaçınmamak daha ucuzdur. SomeHeavyAndExpensiveCalculation() .

Ve bunu çözmek zor olabilir (aslında, çözebileceğinizi varsaymaktansa profil oluşturmaya değecek türden bir şey). Burada dikkate alınmaya değer olsa da, kesici uç üzerinde kilitlemenin en bariz yolları, , ilgisiz olanlar da dahil olmak üzere önbelleğe yapılan tüm eklemeleri .

Bu, 50 farklı değer ayarlamaya çalışan 50 iş parçacığımız olsaydı, o zaman aynı hesaplamayı yapmayacak olsalar bile 50 iş parçacığının tümünü birbirini bekletmek zorunda kalacağımız anlamına gelir.

Bu nedenle, sahip olduğunuz kodla, yarış koşullarını engelleyen koddan daha iyi durumda olabilirsiniz ve eğer yarış durumu bir sorunsa, büyük olasılıkla ya başka bir yerde bunu halletmeniz ya da farklı bir önbelleğe alma stratejisi eski girdileri dışarı atan bir stratejiden †.

Değiştireceğim tek şey, aramayı Set()bir ile değiştireceğimAddOrGetExisting() . Yukarıdakilerden, muhtemelen gerekli olmadığı açıkça anlaşılmalıdır, ancak yeni elde edilen öğenin toplanmasına, genel bellek kullanımının azaltılmasına ve düşük nesilden yüksek nesil koleksiyonlara daha yüksek bir oran sağlanmasına izin verecektir.

Yani evet, eşzamanlılığı önlemek için çift kilitlemeyi kullanabilirsiniz, ancak ya eşzamanlılık aslında bir sorun değildir ya da değerleri yanlış şekilde saklamanız ya da mağazada çift kilitleme sorunu çözmenin en iyi yolu olmayacaktır. .

* Bir dizi diziden yalnızca birinin olduğunu biliyorsanız, eşitlik karşılaştırmalarını optimize edebilirsiniz; bu, bir dizenin iki kopyasına sahip olmanın yalnızca optimalin altında olmaktan ziyade yanlış olabileceği tek zamandır, ancak yapmak istersiniz mantıklı olması için çok farklı önbelleğe alma türleri. Örneğin sıralama XmlReaderdahili olarak yapılır.

† Büyük olasılıkla ya süresiz olarak depolayan ya da zayıf referanslardan yararlanan biri, bu nedenle yalnızca mevcut kullanım yoksa girişleri kaldıracaktır.


1

Global kilitten kaçınmak için, bellek kullanımını patlatmadan anahtar başına bir kilit uygulamak için SingletonCache kullanabilirsiniz (kilit nesneleri artık referans gösterilmediğinde kaldırılır ve alma / bırakma iş parçacığı açısından güvenlidir ve karşılaştırma yoluyla yalnızca 1 örneğin kullanımda olduğunu garanti eder. ve takas).

Bunu kullanmak şöyle görünür:

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;
        }
    }      
}

Kod GitHub'da burada: https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

MemoryCache'den daha hafif olan ve birkaç avantajı olan bir LRU uygulaması da vardır - daha hızlı eşzamanlı okuma ve yazma, sınırlı boyut, arka plan iş parçacığı yok, dahili performans sayaçları vb. (Feragatname, yazdım).


0

Konsol örnek bir MemoryCache , "Nasıl kaydetmek için / basit sınıf nesneleri olsun"

Aşağıdakiler Any keyhariç, başlattıktan ve basıldıktan sonra çıktı Esc:

Önbelleğe kaydediliyor!
Önbellekten alınıyor!
Bazıları1
Bazıları2

    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);

    }

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();
    }
}

Çok hızlı LazyCache :) Bu kodu REST API depoları için yazdım.
art24war

0

Ancak biraz geç ... Tam uygulama:

    [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>();

İşte getPageContentimza:

async Task<string> getPageContent(RequestQuery requestQuery);

Ve işte MemoryCacheWithPolicyuygulama:

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);
    }
}

nloggersadece davranışı nLogizleme MemoryCacheWithPolicyamaçlıdır. RequestQuery requestQueryTemsilci ( Func<TParameter, TResult> createCacheData) aracılığıyla istek nesnesi ( ) değiştirilirse veya kayan veya mutlak süre sınırlarına ulaştığında yeniden oluşturulursa bellek önbelleğini yeniden oluştururum. Her şeyin eşzamansız olduğunu da unutmayın;)


Belki cevabınız bu soruyla daha çok ilgilidir: Async threadafe Get from MemoryCache
Theodor Zoulias

Sanırım öyle, ama yine de faydalı deneyim alışverişi;)
Sam Saarian,

0

Hangisinin daha iyi olduğunu seçmek zordur; Lock veya ReaderWriterLockSlim. Okuma ve yazma sayıları ve oranları gibi gerçek dünya istatistiklerine ihtiyacınız var.

Ancak "kilit" kullanmanın doğru yol olduğuna inanıyorsanız. O zaman burada farklı ihtiyaçlar için farklı bir çözüm var. Ayrıca, Allan Xu'nun çözümünü koda dahil ediyorum. Çünkü her ikisi de farklı ihtiyaçlar için gerekli olabilir.

İşte beni bu çözüme yönlendiren gereksinimler:

  1. Herhangi bir nedenle 'GetData' işlevini sağlamak istemiyorsunuz veya sağlayamıyorsunuz. Belki de 'GetData' işlevi, ağır bir kurucuya sahip başka bir sınıfta yer almaktadır ve kaçınılmaz olduğundan emin olana kadar bir örnek oluşturmak bile istemezsiniz.
  2. Uygulamanın farklı konumlarından / katmanlarından aynı önbelleğe alınmış verilere erişmeniz gerekir. Ve bu farklı konumların aynı dolap nesnesine erişimi yok.
  3. Sabit bir önbellek anahtarınız yok. Örneğin; bazı verileri sessionId önbellek anahtarıyla önbelleğe alma ihtiyacı.

Kod:

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";
        }
    }

}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.