Cách tốt nhất để khóa bộ nhớ cache trong asp.net là gì?


80

Tôi biết trong một số trường hợp nhất định, chẳng hạn như các quy trình đang chạy dài, điều quan trọng là phải khóa bộ đệm ASP.NET để tránh các yêu cầu tiếp theo của người dùng khác đối với tài nguyên đó thực hiện lại quá trình dài thay vì nhấn bộ đệm.

Cách tốt nhất trong c # để thực hiện khóa bộ nhớ cache trong ASP.NET là gì?

Câu trả lời:


114

Đây là mẫu cơ bản:

  • Kiểm tra bộ nhớ cache để tìm giá trị, trả lại nếu nó có sẵn
  • Nếu giá trị không có trong bộ nhớ cache, thì hãy triển khai khóa
  • Bên trong ổ khóa, hãy kiểm tra lại bộ nhớ đệm, có thể bạn đã bị khóa
  • Thực hiện tra cứu giá trị và lưu vào bộ nhớ cache
  • Mở khóa

Trong mã, nó trông như thế này:

private static object ThisLock = new object();

public string GetFoo()
{

  // try to pull from cache here

  lock (ThisLock)
  {
    // cache was empty before we got the lock, check again inside the lock

    // cache is still empty, so retreive the value here

    // store the value in the cache here
  }

  // return the cached value here

}

4
Nếu lần tải đầu tiên của bộ nhớ cache mất vài phút, vẫn còn cách để truy cập các mục đã được tải? Hãy nói nếu tôi có GetFoo_AmazonArticlesByCategory (string categoryKey). Tôi đoán nó sẽ giống như một cái khóa cho mỗi danh mụcKey.
Mathias F

5
Được gọi là "khóa được kiểm tra hai lần". vi.wikipedia.org/wiki/Double-checked_locking
Brad Gagne

32

Để hoàn thiện, một ví dụ đầy đủ sẽ trông giống như thế này.

private static object ThisLock = new object();
...
object dataObject = Cache["globalData"];
if( dataObject == null )
{
    lock( ThisLock )
    {
        dataObject = Cache["globalData"];

        if( dataObject == null )
        {
            //Get Data from db
             dataObject = GlobalObj.GetData();
             Cache["globalData"] = dataObject;
        }
    }
}
return dataObject;

7
if (dataObject == null) {lock (ThisLock) {if (dataObject == null) // tất nhiên nó vẫn là null!
Constantin

30
@Constantin: không thực sự, ai đó có thể đã cập nhật bộ nhớ cache trong khi bạn đang chờ đợi để có được các khóa ()
Tudor Olariu

12
@John Owen - sau câu lệnh khóa, bạn phải thử lấy lại đối tượng từ bộ nhớ đệm!
Pavel Nikolov 14/10/09

3
-1, code bị sai (đọc các comment khác), tại sao bạn không sửa nó? Mọi người có thể thử sử dụng ví dụ của bạn.
khởi hành

11
Mã này thực sự vẫn sai. Bạn đang trở lại globalObjecttrong một phạm vi mà nó không thực sự tồn tại. Điều sẽ xảy ra là nó dataObjectnên được sử dụng bên trong kiểm tra null cuối cùng, và globalObject không cần phải tồn tại sự kiện nào cả.
Scott Anderson

22

Không cần phải khóa toàn bộ phiên bản bộ đệm, thay vào đó chúng tôi chỉ cần khóa khóa cụ thể mà bạn đang chèn. Tức là không cần phải chặn lối vào nhà vệ sinh nữ khi bạn sử dụng nhà vệ sinh nam :)

Việc triển khai bên dưới cho phép khóa các khóa bộ nhớ cache cụ thể bằng cách sử dụng một từ điển đồng thời. Bằng cách này, bạn có thể chạy GetOrAdd () cho hai khóa khác nhau cùng một lúc - nhưng không cho cùng một khóa cùng một lúc.

using System;
using System.Collections.Concurrent;
using System.Web.Caching;

public static class CacheExtensions
{
    private static ConcurrentDictionary<string, object> keyLocks = new ConcurrentDictionary<string, object>();

    /// <summary>
    /// Get or Add the item to the cache using the given key. Lazily executes the value factory only if/when needed
    /// </summary>
    public static T GetOrAdd<T>(this Cache cache, string key, int durationInSeconds, Func<T> factory)
        where T : class
    {
        // Try and get value from the cache
        var value = cache.Get(key);
        if (value == null)
        {
            // If not yet cached, lock the key value and add to cache
            lock (keyLocks.GetOrAdd(key, new object()))
            {
                // Try and get from cache again in case it has been added in the meantime
                value = cache.Get(key);
                if (value == null && (value = factory()) != null)
                {
                    // TODO: Some of these parameters could be added to method signature later if required
                    cache.Insert(
                        key: key,
                        value: value,
                        dependencies: null,
                        absoluteExpiration: DateTime.Now.AddSeconds(durationInSeconds),
                        slidingExpiration: Cache.NoSlidingExpiration,
                        priority: CacheItemPriority.Default,
                        onRemoveCallback: null);
                }

                // Remove temporary key lock
                keyLocks.TryRemove(key, out object locker);
            }
        }

        return value as T;
    }
}

keyLocks.TryRemove(key, out locker)<= công dụng của nó là gì?
iMatoria

2
Điều đó thật tuyệt. Toàn bộ điểm của việc khóa bộ nhớ đệm là để tránh sao chép công việc đã thực hiện để lấy giá trị cho khóa cụ thể đó. Khóa toàn bộ bộ nhớ cache hoặc thậm chí các phần của nó theo lớp là ngớ ngẩn. Bạn muốn chính xác điều này - một ổ khóa có nội dung "Tôi đang nhận được giá trị cho <khóa> mọi người khác chỉ cần đợi tôi." Phương thức mở rộng cũng rất mượt. Hai ý tưởng tuyệt vời trong một! Đây sẽ là câu trả lời mà mọi người tìm thấy. CẢM ƠN.
DanO

1
@iMatoria, sau khi có thứ gì đó trong bộ nhớ cache cho khóa đó, thì sẽ không có ích gì khi giữ đối tượng khóa đó xung quanh hoặc mục nhập trong từ điển khóa - đó là thử xóa vì khóa có thể đã bị xóa khỏi từ điển bởi người khác luồng đến trước - tất cả các luồng đã bị khóa đang chờ khóa đó giờ nằm ​​trong phần mã đó, nơi chúng chỉ lấy giá trị từ bộ nhớ cache, nhưng không còn khóa để loại bỏ.
DanO

Tôi thích cách tiếp cận này hơn nhiều so với câu trả lời được chấp nhận. Tuy nhiên, lưu ý nhỏ: Đầu tiên bạn sử dụng cache.Key, sau đó bạn sử dụng HttpRuntime.Cache.Get.
staccata

@MindaugasTvaronavicius Bắt tốt, bạn đã chính xác, nó là một trường hợp cạnh khi T2 và T3 đang thực thi factoryphương thức đồng thời. Chỉ nơi T1 đã thực thi trước factoryđó trả về null (vì vậy giá trị không được lưu vào bộ nhớ đệm). Nếu không, T2 và T3 sẽ chỉ nhận giá trị được lưu trong bộ nhớ cache đồng thời (điều này sẽ an toàn). Tôi đoán giải pháp dễ dàng là xóa keyLocks.TryRemove(key, out locker)nhưng sau đó ConcurrentDictionary có thể bị rò rỉ bộ nhớ nếu sử dụng một số lượng lớn các khóa khác nhau. Nếu không, hãy thêm một số logic để đếm số khóa cho một khóa trước khi loại bỏ, có lẽ bằng cách sử dụng semaphore?
cwills

13

Chỉ để lặp lại những gì Pavel đã nói, tôi tin rằng đây là cách viết nó an toàn nhất

private T GetOrAddToCache<T>(string cacheKey, GenericObjectParamsDelegate<T> creator, params object[] creatorArgs) where T : class, new()
    {
        T returnValue = HttpContext.Current.Cache[cacheKey] as T;
        if (returnValue == null)
        {
            lock (this)
            {
                returnValue = HttpContext.Current.Cache[cacheKey] as T;
                if (returnValue == null)
                {
                    returnValue = creator(creatorArgs);
                    if (returnValue == null)
                    {
                        throw new Exception("Attempt to cache a null reference");
                    }
                    HttpContext.Current.Cache.Add(
                        cacheKey,
                        returnValue,
                        null,
                        System.Web.Caching.Cache.NoAbsoluteExpiration,
                        System.Web.Caching.Cache.NoSlidingExpiration,
                        CacheItemPriority.Normal,
                        null);
                }
            }
        }

        return returnValue;
    }

7
'lock (this)' là không tốt . Bạn nên sử dụng một đối tượng khóa chuyên dụng không hiển thị cho người dùng trong lớp của bạn. Giả sử, trên đường, ai đó quyết định sử dụng đối tượng bộ nhớ cache để khóa. Họ sẽ không biết rằng nó đang được sử dụng trong nội bộ cho các mục đích khóa, điều này có thể dẫn đến sự tồi tệ.
tiêu


2

Tôi đã nghĩ ra phương pháp mở rộng sau:

private static readonly object _lock = new object();

public static TResult GetOrAdd<TResult>(this Cache cache, string key, Func<TResult> action, int duration = 300) {
    TResult result;
    var data = cache[key]; // Can't cast using as operator as TResult may be an int or bool

    if (data == null) {
        lock (_lock) {
            data = cache[key];

            if (data == null) {
                result = action();

                if (result == null)
                    return result;

                if (duration > 0)
                    cache.Insert(key, result, null, DateTime.UtcNow.AddSeconds(duration), TimeSpan.Zero);
            } else
                result = (TResult)data;
        }
    } else
        result = (TResult)data;

    return result;
}

Tôi đã sử dụng cả câu trả lời @John Owen và @ user378380. Giải pháp của tôi cho phép bạn lưu trữ các giá trị int và bool trong bộ nhớ cache.

Vui lòng sửa cho tôi nếu có bất kỳ lỗi nào hoặc liệu nó có thể được viết tốt hơn một chút.


Đó là độ dài bộ nhớ cache mặc định là 5 phút (60 * 5 = 300 giây).
nfplee

3
Rất tốt, nhưng tôi thấy một vấn đề: nếu bạn có nhiều bộ nhớ đệm, tất cả chúng sẽ dùng chung một khóa. Để làm cho nó mạnh mẽ hơn, hãy sử dụng từ điển để truy xuất khóa khớp với bộ đệm nhất định.
JoeCool

1

Gần đây tôi đã thấy một mẫu có tên là Mẫu truy cập túi trạng thái đúng, dường như có liên quan đến mẫu này.

Tôi đã sửa đổi nó một chút để an toàn cho chuỗi.

http://weblogs.asp.net/craigshoemaker/archive/2008/08/28/asp-net-caching-and-performance.aspx

private static object _listLock = new object();

public List List() {
    string cacheKey = "customers";
    List myList = Cache[cacheKey] as List;
    if(myList == null) {
        lock (_listLock) {
            myList = Cache[cacheKey] as List;
            if (myList == null) {
                myList = DAL.ListCustomers();
                Cache.Insert(cacheKey, mList, null, SiteConfig.CacheDuration, TimeSpan.Zero);
            }
        }
    }
    return myList;
}

Cả hai luồng không thể nhận được kết quả đúng cho (myList == null)? Sau đó, cả hai luồng thực hiện cuộc gọi đến DAL.ListCustomers () và chèn kết quả vào bộ nhớ cache.
frankadelic

4
Sau khi khóa, bạn cần kiểm tra lại bộ nhớ cache, không phải myListbiến cục bộ
orip

1
Điều này thực sự ổn trước khi bạn chỉnh sửa. Không cần khóa nếu bạn sử dụng Insertđể ngăn các trường hợp ngoại lệ, chỉ khi bạn muốn đảm bảo rằng nó DAL.ListCustomersđược gọi một lần (mặc dù nếu kết quả là null, nó sẽ được gọi mọi lần).
marapet


0

Tôi đã viết một thư viện giải quyết vấn đề cụ thể đó: Rocks.Caching

Ngoài ra, tôi đã viết blog về vấn đề này một cách chi tiết và giải thích tại sao nó lại quan trọng ở đây .


0

Tôi đã sửa đổi mã của @ user378380 để linh hoạt hơn. Thay vì trả về TResult bây giờ trả về đối tượng để chấp nhận các kiểu khác nhau theo thứ tự. Cũng thêm một số thông số để linh hoạt. Tất cả ý tưởng thuộc về @ user378380.

 private static readonly object _lock = new object();


//If getOnly is true, only get existing cache value, not updating it. If cache value is null then      set it first as running action method. So could return old value or action result value.
//If getOnly is false, update the old value with action result. If cache value is null then      set it first as running action method. So always return action result value.
//With oldValueReturned boolean we can cast returning object(if it is not null) appropriate type on main code.


 public static object GetOrAdd<TResult>(this Cache cache, string key, Func<TResult> action,
    DateTime absoluteExpireTime, TimeSpan slidingExpireTime, bool getOnly, out bool oldValueReturned)
{
    object result;
    var data = cache[key]; 

    if (data == null)
    {
        lock (_lock)
        {
            data = cache[key];

            if (data == null)
            {
                oldValueReturned = false;
                result = action();

                if (result == null)
                {                       
                    return result;
                }

                cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
            }
            else
            {
                if (getOnly)
                {
                    oldValueReturned = true;
                    result = data;
                }
                else
                {
                    oldValueReturned = false;
                    result = action();
                    if (result == null)
                    {                            
                        return result;
                    }

                    cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
                }
            }
        }
    }
    else
    {
        if(getOnly)
        {
            oldValueReturned = true;
            result = data;
        }
        else
        {
            oldValueReturned = false;
            result = action();
            if (result == null)
            {
                return result;
            }

            cache.Insert(key, result, null, absoluteExpireTime, slidingExpireTime);
        }            
    }

    return result;
}
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.