Mẫu khóa để sử dụng đúng .NET MemoryCache


115

Tôi giả sử mã này có vấn đề về đồng thời:

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

Lý do cho vấn đề đồng thời là nhiều luồng có thể nhận được một khóa null và sau đó cố gắng chèn dữ liệu vào bộ nhớ cache.

Cách ngắn nhất và rõ ràng nhất để làm cho mã này bằng chứng đồng thời là gì? Tôi muốn làm theo một mô hình tốt trên mã liên quan đến bộ nhớ cache của mình. Một liên kết đến một bài báo trực tuyến sẽ là một trợ giúp tuyệt vời.

CẬP NHẬT:

Tôi đã nghĩ ra mã này dựa trên câu trả lời của @Scott Chamberlain. Bất cứ ai có thể tìm thấy bất kỳ vấn đề hiệu suất hoặc đồng thời với điều này? Nếu điều này hoạt động, nó sẽ lưu nhiều dòng mã và lỗi.

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
tại sao bạn không sử dụng ReaderWriterLockSlim?
DarthVader

2
Tôi đồng ý với DarthVader ... Tôi sẽ nghĩ bạn nghiêng ReaderWriterLockSlim... Nhưng tôi cũng sẽ sử dụng kỹ thuật này để tránh try-finallyphát biểu.
poy

1
Đối với phiên bản cập nhật của bạn, tôi sẽ không khóa trên một cacheLock duy nhất nữa, thay vào đó tôi sẽ khóa từng khóa. Điều này có thể dễ dàng thực hiện với Dictionary<string, object>khóa trong đó chính là khóa bạn sử dụng trong của mình MemoryCachevà đối tượng trong từ điển chỉ là đối tượng cơ bản Objectmà bạn khóa. Tuy nhiên, điều đó đang được nói ra, tôi khuyên bạn nên đọc qua câu trả lời của Jon Hanna. Nếu không có cấu hình thích hợp, bạn có thể làm chậm chương trình của mình nhiều hơn với việc khóa hơn là để hai trường hợp SomeHeavyAndExpensiveCalculation()chạy và có một kết quả bị loại bỏ.
Scott Chamberlain

1
Đối với tôi, dường như việc tạo CacheItemPolicy sau khi nhận được giá trị đắt tiền vào bộ nhớ cache sẽ chính xác hơn. Trong trường hợp xấu nhất, chẳng hạn như tạo một báo cáo tóm tắt mất 21 phút để trả lại "chuỗi đắt" (có thể chứa tên tệp của báo cáo PDF) sẽ "hết hạn" trước khi nó được trả lại.
Wonderbird

1
@Wonderbird Điểm tốt, tôi đã cập nhật câu trả lời của mình để làm điều đó.
Scott Chamberlain

Câu trả lời:


91

Đây là lần lặp lại mã thứ 2 của tôi. Vì MemoryCachelà chuỗi an toàn nên bạn không cần phải khóa lần đọc ban đầu, bạn chỉ có thể đọc và nếu bộ đệm ẩn trả về null thì hãy kiểm tra khóa để xem bạn có cần tạo chuỗi hay không. Nó đơn giản hóa rất nhiều mã.

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

CHỈNH SỬA : Đoạn mã dưới đây là không cần thiết nhưng tôi muốn để nó hiển thị phương pháp ban đầu. Nó có thể hữu ích cho những khách truy cập trong tương lai, những người đang sử dụng một bộ sưu tập khác có các lần đọc an toàn theo luồng nhưng ghi không an toàn theo luồng (hầu như tất cả các lớp trongSystem.Collections không gian tên đều như vậy).

Đây là cách tôi sẽ làm điều đó bằng cách sử dụng ReaderWriterLockSlimđể bảo vệ quyền truy cập. Bạn cần thực hiện một loại " Double Checked Lock " để xem liệu có ai khác đã tạo mục được lưu trong bộ nhớ cache trong khi chúng tôi chờ lấy khóa hay không.

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 theo cách nào thì đoạn mã trên sẽ không hoạt động? Đây cũng không phải là "khóa được kiểm tra hai lần". Tôi chỉ đang làm theo một mô hình tương tự và đó là cách tốt nhất tôi có thể nghĩ ra để mô tả nó. Đó là lý do tại sao tôi nói đó là một loại khóa được kiểm tra hai lần.
Scott Chamberlain

Tôi đã không bình luận về mã của bạn. Tôi đã nhận xét rằng Khóa kiểm tra hai lần không hoạt động. Mã của bạn là tốt.
DarthVader

1
Mặc dù vậy, tôi thấy thật khó để biết được những tình huống nào mà kiểu khóa này và kiểu lưu trữ này sẽ có ý nghĩa: Nếu bạn đang khóa tất cả các sáng tạo của giá trị có thể xảy ra MemoryCachethì ít nhất một trong hai điều đó đã sai.
Jon Hanna

@ScottChamberlain chỉ cần nhìn vào mã này và nó không dễ bị trường hợp ngoại lệ được đưa ra giữa việc mua khóa và khối thử. Tác giả của C # In a Nutshell thảo luận về vấn đề này tại đây, albahari.com/threading/part2.aspx#_MonitorEnter_and_MonitorExit
BrutalSimparies

9
Một nhược điểm của mã này là CacheKey "A" sẽ chặn một yêu cầu đến CacheKey "B" nếu cả hai chưa được lưu trong bộ nhớ cache. Để giải quyết điều này, bạn có thể sử dụng một concurrentDictionary <string, object> mà bạn lưu trữ các cachekeys để khóa trên
MichaelD

44

Có một thư viện mã nguồn mở [từ chối trách nhiệm: mà tôi đã viết]: LazyCache mà IMO bao gồm yêu cầu của bạn bằng hai dòng mã:

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

Nó đã tích hợp sẵn khóa theo mặc định nên phương thức có thể lưu trong bộ nhớ cache sẽ chỉ thực thi một lần cho mỗi lần bỏ lỡ bộ nhớ cache và nó sử dụng lambda để bạn có thể thực hiện "lấy hoặc thêm" trong một lần. Nó được mặc định là hết hạn trượt 20 phút.

Thậm chí còn có một gói NuGet ;)


4
Dapper của bộ nhớ đệm.
Charles Burns

3
Điều này cho phép tôi trở thành một nhà phát triển lười biếng, đây là câu trả lời tốt nhất!
jdnew18

Đáng nói là bài báo mà trang github cho LazyCache chỉ ra là khá tốt vì những lý do đằng sau nó. alastaircrabtree.com/…
Rafael Merlin

2
Nó có khóa trên mỗi phím hoặc mỗi bộ nhớ cache?
jjxtra

1
@DirkBoer không nó sẽ không bị chặn vì cách ổ khóa và lười biếng được sử dụng trong lazycache
alastairtree

30

Tôi đã giải quyết vấn đề này bằng cách sử dụng phương thức AddOrGetExisting trên MemoryCache và sử dụng khởi tạo Lazy .

Về cơ bản, mã của tôi trông giống như sau:

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

Trường hợp tệ nhất ở đây là bạn tạo cùng một Lazyđối tượng hai lần. Nhưng điều đó là khá tầm thường. Việc sử dụng các AddOrGetExistingđảm bảo rằng bạn sẽ chỉ nhận được một phiên bản của Lazyđối tượng và vì vậy bạn cũng được đảm bảo chỉ gọi phương thức khởi tạo đắt tiền một lần.


4
Vấn đề với kiểu tiếp cận này là bạn có thể chèn dữ liệu không hợp lệ. Nếu SomeHeavyAndExpensiveCalculationThatResultsAString()ném một ngoại lệ, nó bị kẹt trong bộ nhớ cache. Ngay cả trường hợp ngoại lệ thoáng qua sẽ được lưu trữ với Lazy<T>: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx
Scott Wegner

2
Mặc dù đúng là Lazy <T> có thể trả về lỗi nếu ngoại lệ khởi tạo không thành công, đó là một điều khá dễ phát hiện. Sau đó, bạn có thể loại bỏ bất kỳ Lazy <T> nào giải quyết được lỗi khỏi bộ nhớ cache, tạo một Lazy <T> mới, đặt nó vào bộ nhớ cache và giải quyết nó. Trong mã riêng của chúng tôi, chúng tôi làm điều gì đó tương tự. Chúng tôi thử lại một số lần đã đặt trước khi gặp lỗi.
Keith

12
AddOrGetExisting trở lại null nếu mục đã không có mặt, vì vậy bạn nên kiểm tra và gửi lại lazyObject trong trường hợp đó
Gian Marco

1
Sử dụng LazyThreadSafetyMode.PublicationOnly sẽ tránh được các trường hợp ngoại lệ vào bộ nhớ đệm.
Clement

2
Theo các nhận xét trong bài đăng trên blog này nếu việc khởi tạo mục nhập bộ nhớ cache là cực kỳ tốn kém, thì tốt hơn nên loại bỏ một ngoại lệ (như được hiển thị trong ví dụ trong bài đăng trên blog) thay vì sử dụng PublicationOnly, vì có khả năng tất cả các luồng có thể gọi trình khởi tạo cùng một lúc.
bcr,

15

Tôi giả sử mã này có vấn đề về đồng thời:

Trên thực tế, nó có thể khá ổn, mặc dù có thể cải thiện.

Bây giờ, nói chung, mẫu mà chúng ta có nhiều luồng thiết lập một giá trị được chia sẻ trong lần sử dụng đầu tiên, để không khóa giá trị đang nhận và đặt có thể là:

  1. Thảm hại - mã khác sẽ cho rằng chỉ có một trường hợp tồn tại.
  2. Tai hại - mã lấy được phiên bản không chỉ có thể chịu đựng một (hoặc có thể là một số lượng nhỏ) hoạt động đồng thời.
  3. Tai hại - phương tiện lưu trữ không an toàn cho chuỗi (ví dụ: có hai chuỗi thêm vào từ điển và bạn có thể mắc đủ loại lỗi khó chịu).
  4. Tối ưu dưới mức - hiệu suất tổng thể kém hơn nếu khóa đã đảm bảo chỉ một luồng thực hiện công việc lấy giá trị.
  5. Tối ưu - chi phí để có nhiều luồng thực hiện công việc dư thừa sẽ ít hơn chi phí ngăn chặn nó, đặc biệt vì điều đó chỉ có thể xảy ra trong một khoảng thời gian tương đối ngắn.

Tuy nhiên, xem xét ở đây MemoryCachecó thể loại bỏ các mục nhập sau đó:

  1. Nếu thật tai hại khi có nhiều hơn một trường hợp thì đó MemoryCachelà cách tiếp cận sai lầm.
  2. Nếu bạn phải ngăn chặn việc tạo đồng thời, bạn nên làm như vậy tại thời điểm tạo.
  3. MemoryCache là luồng an toàn về quyền truy cập vào đối tượng đó, vì vậy đó không phải là mối quan tâm ở đây.

Tất nhiên, cả hai khả năng này đều phải được nghĩ đến, mặc dù lần duy nhất có hai trường hợp của cùng một chuỗi tồn tại có thể là một vấn đề nếu bạn đang thực hiện các tối ưu hóa rất cụ thể không áp dụng ở đây *.

Vì vậy, chúng tôi còn lại với các khả năng:

  1. Sẽ rẻ hơn để tránh chi phí của các cuộc gọi trùng lặp đến SomeHeavyAndExpensiveCalculation() .
  2. Nó là rẻ hơn để tránh chi phí của các cuộc gọi trùng lặp đến SomeHeavyAndExpensiveCalculation().

Và việc giải quyết vấn đề đó có thể khó khăn (thực sự, đây là điều mà nó đáng để lập hồ sơ hơn là giả định rằng bạn có thể giải quyết nó). Điều đáng xem xét ở đây mặc dù rằng các cách khóa chèn rõ ràng nhất sẽ ngăn chặn tất cả các bổ sung vào bộ nhớ cache, kể cả những phần không liên quan.

Điều này có nghĩa là nếu chúng ta có 50 luồng đang cố gắng đặt 50 giá trị khác nhau, thì chúng ta sẽ phải làm cho tất cả 50 luồng chờ lẫn nhau, mặc dù chúng thậm chí sẽ không thực hiện cùng một phép tính.

Như vậy, có lẽ bạn nên sử dụng mã bạn có tốt hơn là với mã tránh điều kiện chủng tộc và nếu điều kiện chủng tộc là một vấn đề, bạn có thể cần phải xử lý điều đó ở một nơi khác hoặc cần một mã khác chiến lược bộ nhớ đệm hơn một chiến lược loại bỏ các mục cũ †.

Một điều tôi sẽ thay đổi là tôi sẽ thay thế cuộc gọi đến Set()bằng một cuộc gọi đến AddOrGetExisting(). Từ những điều trên, rõ ràng là nó có thể không cần thiết, nhưng nó sẽ cho phép thu thập mục mới thu được, giảm mức sử dụng bộ nhớ tổng thể và cho phép tỷ lệ bộ sưu tập thế hệ thấp hơn cao hơn.

Vì vậy, bạn có thể sử dụng khóa kép để ngăn chặn đồng thời, nhưng đồng thời thực sự không phải là vấn đề hoặc việc bạn lưu trữ các giá trị sai cách hoặc khóa kép trên cửa hàng sẽ không phải là cách tốt nhất để giải quyết nó .

* Nếu bạn biết chỉ có một trong mỗi tập hợp các chuỗi tồn tại, bạn có thể tối ưu hóa so sánh bình đẳng, đó là khoảng thời gian duy nhất có hai bản sao của một chuỗi có thể không chính xác thay vì chỉ là tối ưu phụ, nhưng bạn muốn làm rất nhiều loại bộ nhớ đệm khác nhau để điều đó có ý nghĩa. Ví dụ: sắp xếp XmlReadernội bộ.

† Rất có thể là một trong số đó lưu trữ vô thời hạn, hoặc một trong đó sử dụng các tài liệu tham khảo yếu, do đó nó sẽ chỉ loại bỏ các mục nhập nếu không còn mục nào sử dụng.


1

Để tránh khóa toàn cục, bạn có thể sử dụng SingletonCache để thực hiện một khóa cho mỗi khóa, mà không sử dụng bộ nhớ gây nổ (các đối tượng khóa sẽ bị xóa khi không còn được tham chiếu nữa và việc lấy / phát hành là chuỗi an toàn đảm bảo rằng chỉ có 1 phiên bản được sử dụng thông qua so sánh và hoán đổi).

Sử dụng nó trông như thế này:

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

Mã ở đây trên GitHub: https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

Ngoài ra còn có một triển khai LRU có trọng lượng nhẹ hơn MemoryCache và có một số ưu điểm - đọc và ghi đồng thời nhanh hơn, kích thước giới hạn, không có luồng nền, bộ đếm hiệu suất nội bộ, v.v. (tôi đã viết từ chối trách nhiệm).


0

Ví dụ về bảng điều khiển của MemoryCache , "Cách lưu / lấy các đối tượng lớp đơn giản"

Đầu ra sau khi khởi chạy và nhấn Any keyngoại trừEsc :

Đang lưu vào bộ nhớ cache!
Lấy từ bộ nhớ 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);

    }

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

LazyCache rất nhanh :) Tôi đã viết mã này cho kho lưu trữ API REST.
art24war

0

Tuy nhiên, nó hơi muộn ...

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

Đây là getPageContentchữ ký:

async Task<string> getPageContent(RequestQuery requestQuery);

Và đây là cách MemoryCacheWithPolicythực hiện:

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

nloggerchỉ là nLogđối tượng để theo dõi MemoryCacheWithPolicyhành vi. Tôi tạo lại bộ nhớ đệm nếu đối tượng yêu cầu ( RequestQuery requestQuery) được thay đổi thông qua ủy nhiệm ( Func<TParameter, TResult> createCacheData) hoặc tạo lại khi trượt hoặc thời gian tuyệt đối đạt đến giới hạn của chúng. Lưu ý rằng mọi thứ cũng không đồng bộ;)


Có thể câu trả lời của bạn liên quan nhiều hơn đến câu hỏi này: Async threadsafe Nhận từ MemoryCache
Theodor Zoulias

Tôi đoán vậy, nhưng vẫn trao đổi kinh nghiệm hữu ích;)
Sam Saarian

0

Rất khó để chọn cái nào tốt hơn; khóa hoặc ReaderWriterLockSlim. Bạn cần thống kê trong thế giới thực về các con số và tỷ lệ đọc và ghi, v.v.

Nhưng nếu bạn tin rằng sử dụng "khóa" là cách chính xác. Sau đó, đây là một giải pháp khác nhau cho các nhu cầu khác nhau. Tôi cũng bao gồm giải pháp của Allan Xu trong mã. Bởi vì cả hai đều có thể cần thiết cho những nhu cầu khác nhau.

Dưới đây là các yêu cầu, hướng tôi đến giải pháp này:

  1. Bạn không muốn hoặc không thể cung cấp chức năng 'GetData' vì một số lý do. Có lẽ hàm 'GetData' nằm trong một số lớp khác với hàm tạo nặng và bạn không muốn tạo một thể hiện cho đến khi đảm bảo rằng nó không thể sử dụng được.
  2. Bạn cần truy cập cùng một dữ liệu đã lưu trong bộ nhớ cache từ các vị trí / tầng khác nhau của ứng dụng. Và những vị trí khác nhau không có quyền truy cập vào cùng một đối tượng khóa.
  3. Bạn không có khóa bộ nhớ cache liên tục. Ví dụ; cần lưu một số dữ liệu vào bộ nhớ đệm bằng khóa sessionId cache.

Mã:

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

}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.