MemoryCache Thread An toàn, Khóa có cần thiết không?


91

Đối với người mới bắt đầu, hãy để tôi chỉ cần ném nó ra khỏi đó rằng tôi biết mã bên dưới không an toàn cho chuỗi (sửa: có thể là). Điều tôi đang đấu tranh là tìm ra một triển khai phù hợp và một cách mà tôi thực sự có thể gặp thất bại trong quá trình thử nghiệm. Tôi đang cấu trúc lại một dự án WCF lớn ngay bây giờ cần một số (chủ yếu) dữ liệu tĩnh được lưu trong bộ nhớ cache và được điền từ cơ sở dữ liệu SQL. Nó cần phải hết hạn và "làm mới" ít nhất một lần một ngày, đó là lý do tại sao tôi đang sử dụng MemoryCache.

Tôi biết rằng mã dưới đây không được an toàn cho chuỗi nhưng tôi không thể khiến nó không thành công khi tải nặng và làm phức tạp vấn đề, tìm kiếm trên google hiển thị các triển khai theo cả hai cách (có và không có khóa kết hợp với các cuộc tranh luận xem chúng có cần thiết hay không).

Ai đó có kiến ​​thức về MemoryCache trong môi trường đa luồng có thể cho tôi biết chắc chắn liệu tôi có cần khóa ở nơi thích hợp hay không để lệnh gọi xóa (sẽ hiếm khi được gọi nhưng là yêu cầu của nó) sẽ không xảy ra trong quá trình truy xuất / lặp lại.

public class MemoryCacheService : IMemoryCacheService
{
    private const string PunctuationMapCacheKey = "punctuationMaps";
    private static readonly ObjectCache Cache;
    private readonly IAdoNet _adoNet;

    static MemoryCacheService()
    {
        Cache = MemoryCache.Default;
    }

    public MemoryCacheService(IAdoNet adoNet)
    {
        _adoNet = adoNet;
    }

    public void ClearPunctuationMaps()
    {
        Cache.Remove(PunctuationMapCacheKey);
    }

    public IEnumerable GetPunctuationMaps()
    {
        if (Cache.Contains(PunctuationMapCacheKey))
        {
            return (IEnumerable) Cache.Get(PunctuationMapCacheKey);
        }

        var punctuationMaps = GetPunctuationMappings();

        if (punctuationMaps == null)
        {
            throw new ApplicationException("Unable to retrieve punctuation mappings from the database.");
        }

        if (punctuationMaps.Cast<IPunctuationMapDto>().Any(p => p.UntaggedValue == null || p.TaggedValue == null))
        {
            throw new ApplicationException("Null values detected in Untagged or Tagged punctuation mappings.");
        }

        // Store data in the cache
        var cacheItemPolicy = new CacheItemPolicy
        {
            AbsoluteExpiration = DateTime.Now.AddDays(1.0)
        };

        Cache.AddOrGetExisting(PunctuationMapCacheKey, punctuationMaps, cacheItemPolicy);

        return punctuationMaps;
    }

    //Go oldschool ADO.NET to break the dependency on the entity framework and need to inject the database handler to populate cache
    private IEnumerable GetPunctuationMappings()
    {
        var table = _adoNet.ExecuteSelectCommand("SELECT [id], [TaggedValue],[UntaggedValue] FROM [dbo].[PunctuationMapper]", CommandType.Text);
        if (table != null && table.Rows.Count != 0)
        {
            return AutoMapper.Mapper.DynamicMap<IDataReader, IEnumerable<PunctuationMapDto>>(table.CreateDataReader());
        }

        return null;
    }
}

ObjectCache là một chuỗi an toàn, tôi không nghĩ rằng lớp của bạn có thể bị lỗi. msdn.microsoft.com/en-us/library/… Bạn có thể truy cập cơ sở dữ liệu cùng lúc nhưng điều đó sẽ chỉ sử dụng nhiều cpu hơn mức cần thiết.
the_lotus

1
Mặc dù ObjectCache là luồng an toàn, nhưng việc triển khai nó có thể không. Do đó, câu hỏi MemoryCache.
Haney

Câu trả lời:


78

MS mặc định cung cấp MemoryCachelà hoàn toàn an toàn luồng. Bất kỳ triển khai tùy chỉnh nào bắt nguồn từ đó MemoryCachecó thể không an toàn cho chuỗi. Nếu bạn đang sử dụng MemoryCachengoài hộp, nó sẽ an toàn. Duyệt qua mã nguồn của giải pháp bộ nhớ đệm phân tán nguồn mở của tôi để xem cách tôi sử dụng nó (MemCache.cs):

https://github.com/haneytron/dache/blob/master/Dache.CacheHost/Storage/MemCache.cs


2
David, Chỉ cần xác nhận, trong lớp ví dụ rất đơn giản mà tôi có ở trên, lệnh gọi tới .Remove () trên thực tế là một chuỗi an toàn nếu một chuỗi khác đang trong quá trình gọi Get ()? Tôi cho rằng tôi chỉ nên sử dụng gương phản xạ và tìm hiểu sâu hơn nhưng có rất nhiều thông tin mâu thuẫn ngoài đó.
James Legan

12
Nó là chuỗi an toàn, nhưng dễ xảy ra các điều kiện đua ... Nếu Nhận của bạn xảy ra trước khi Xóa của bạn, dữ liệu sẽ được trả lại trên Nhận. Nếu loại bỏ xảy ra trước, nó sẽ không. Điều này giống như những lần đọc bẩn trên cơ sở dữ liệu.
Haney

10
đáng đề cập (như tôi đã nhận xét trong một câu trả lời khác bên dưới), việc triển khai lõi dotnet hiện KHÔNG hoàn toàn an toàn cho luồng. cụ thể là GetOrCreatephương pháp. có vấn đề về vấn đề đó trên github
AmitE

Bộ nhớ đệm có ném ngoại lệ "Hết bộ nhớ" trong khi chạy các luồng bằng bảng điều khiển không?
Faseeh Haris

49

Mặc dù MemoryCache thực sự an toàn cho luồng như các câu trả lời khác đã chỉ định, nhưng nó có một vấn đề đa luồng phổ biến - nếu 2 luồng cố gắng truy cập Get(hoặc kiểm tra Contains) bộ nhớ cache cùng một lúc, thì cả hai sẽ bỏ lỡ bộ nhớ cache và cả hai sẽ kết thúc tạo kết quả và cả hai sau đó sẽ thêm kết quả vào bộ nhớ cache.

Thường thì điều này là không mong muốn - luồng thứ hai nên đợi luồng đầu tiên hoàn thành và sử dụng kết quả của nó thay vì tạo kết quả hai lần.

Đây là một trong những lý do tôi viết LazyCache - một trình bao bọc thân thiện trên MemoryCache để giải quyết các loại vấn đề này. Nó cũng có sẵn trên Nuget .


"nó có một vấn đề đa luồng phổ biến" Đó là lý do tại sao bạn nên sử dụng các phương pháp nguyên tử như AddOrGetExisting, thay vì triển khai logic tùy chỉnh xung quanh Contains. Các AddOrGetExistingphương pháp MemoryCache là nguyên tử và threadsafe referencesource.microsoft.com/System.Runtime.Caching/R/...
Alex

1
Có AddOrGetExisting là chuỗi an toàn. Nhưng nó giả định rằng bạn đã có một tham chiếu đến đối tượng sẽ được thêm vào bộ nhớ cache. Thông thường bạn không muốn AddOrGetExisting mà bạn muốn "GetExistingOrGenerateThisAndCacheIt", đó là những gì LazyCache cung cấp cho bạn.
alastairtree

vâng, đã đồng ý về điểm "nếu bạn chưa có ý kiến"
Alex

17

Như những người khác đã nói, MemoryCache thực sự là một chuỗi an toàn. Tuy nhiên, sự an toàn chuỗi của dữ liệu được lưu trữ bên trong nó, hoàn toàn phụ thuộc vào việc bạn sử dụng nó.

Để trích dẫn Reed Copsey từ bài đăng tuyệt vời của anh ấy liên quan đến đồng thời và ConcurrentDictionary<TKey, TValue>loại. Tất nhiên là có thể áp dụng ở đây.

Nếu hai luồng gọi đây là [GetOrAdd] đồng thời, có thể dễ dàng xây dựng hai phiên bản TValue.

Bạn có thể tưởng tượng rằng điều này sẽ đặc biệt tồi tệ nếu TValueviệc xây dựng tốn kém.

Để giải quyết vấn đề này, bạn có thể tận dụng Lazy<T>rất dễ dàng, điều này thật trùng hợp là rất rẻ để xây dựng. Làm điều này đảm bảo nếu chúng ta rơi vào tình huống đa luồng, rằng chúng ta chỉ đang xây dựng nhiều phiên bản Lazy<T>(giá rẻ).

GetOrAdd()( GetOrCreate()trong trường hợp của MemoryCache) sẽ trả về giống nhau, số ít Lazy<T>cho tất cả các luồng, các trường hợp "bổ sung" của Lazy<T>chỉ đơn giản là bị loại bỏ.

Vì đối tượng Lazy<T>không làm bất cứ điều gì cho đến khi .Valueđược gọi, nên chỉ có một thể hiện của đối tượng từng được xây dựng.

Bây giờ cho một số mã! Dưới đây là một phương pháp mở rộng để IMemoryCachethực hiện những điều trên. Nó tùy ý được thiết lập SlidingExpirationdựa trên một int secondstham số phương thức. Nhưng điều này hoàn toàn có thể tùy chỉnh dựa trên nhu cầu của bạn.

Lưu ý rằng điều này dành riêng cho các ứng dụng .netcore2.0

public static T GetOrAdd<T>(this IMemoryCache cache, string key, int seconds, Func<T> factory)
{
    return cache.GetOrCreate<T>(key, entry => new Lazy<T>(() =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(seconds);

        return factory.Invoke();
    }).Value);
}

Để gọi:

IMemoryCache cache;
var result = cache.GetOrAdd("someKey", 60, () => new object());

Để thực hiện tất cả điều này một cách không đồng bộ, tôi khuyên bạn nên sử dụng cách triển khai tuyệt vời của Stephen ToubAsyncLazy<T> được tìm thấy trong bài viết của anh ấy trên MSDN. Kết hợp trình khởi tạo lười biếng nội trang Lazy<T>với lời hứa Task<T>:

public class AsyncLazy<T> : Lazy<Task<T>>
{
    public AsyncLazy(Func<T> valueFactory) :
        base(() => Task.Factory.StartNew(valueFactory))
    { }
    public AsyncLazy(Func<Task<T>> taskFactory) :
        base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap())
    { }
}   

Bây giờ là phiên bản không đồng bộ của GetOrAdd():

public static Task<T> GetOrAddAsync<T>(this IMemoryCache cache, string key, int seconds, Func<Task<T>> taskFactory)
{
    return cache.GetOrCreateAsync<T>(key, async entry => await new AsyncLazy<T>(async () =>
    { 
        entry.SlidingExpiration = TimeSpan.FromSeconds(seconds);

        return await taskFactory.Invoke();
    }).Value);
}

Và cuối cùng, để gọi:

IMemoryCache cache;
var result = await cache.GetOrAddAsync("someKey", 60, async () => new object());

2
Tôi đã thử nó và nó dường như không hoạt động (dot net core 2.0). mỗi GetOrCreate sẽ tạo một phiên bản Lazy mới và bộ đệm ẩn được cập nhật với Lazy mới, và do đó, Giá trị get được đánh giá \ tạo nhiều lần (trong một chuỗi nhiều env).
AmitE 23/03/18

2
từ vẻ của nó, .netcore 2.0 MemoryCache.GetOrCreatekhông phải là thread an toàn theo cách tương tự ConcurrentDictionary
Amite

Có thể là một câu hỏi ngớ ngẩn, nhưng bạn có chắc đó là Giá trị được tạo nhiều lần chứ không phải Lazy? Nếu vậy, làm thế nào bạn xác nhận điều này?
pim

3
Tôi đã sử dụng một chức năng gốc để in ra màn hình khi nó được sử dụng + tạo một số ngẫu nhiên và bắt đầu 10 luồng tất cả cố gắng GetOrCreatesử dụng cùng một khóa và nhà máy đó. kết quả, nhà máy đã được sử dụng 10 lần khi sử dụng với bộ nhớ đệm bộ nhớ (nhìn thấy bản in) + mỗi lần GetOrCreatetrả về một giá trị khác nhau! Tôi chạy thử nghiệm tương tự bằng cách sử dụng ConcurrentDicionaryvà thấy nhà máy chỉ được sử dụng một lần và luôn có giá trị tương tự. Tôi đã tìm thấy một vấn đề đã đóng trên đó trong github, tôi vừa viết một nhận xét ở đó rằng nó nên được mở lại
AmitE

Cảnh báo: câu trả lời này không hoạt động: Lazy <T> không ngăn nhiều lần thực thi chức năng được lưu trong bộ nhớ cache. Xem câu trả lời @snicker. [net core 2.0]
Fernando Silva

11

Kiểm tra liên kết này: http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx

Đi tới cuối trang (hoặc tìm kiếm văn bản "An toàn chuỗi").

Bạn sẽ thấy:

^ An toàn chuỗi

Loại này là an toàn chủ đề.


7
Tôi đã ngừng tin tưởng vào định nghĩa "An toàn luồng" của MSDN khá lâu trước đây dựa trên kinh nghiệm cá nhân. Đây là một bài đọc hay: liên kết
James Legan

3
Bài đăng đó hơi khác so với liên kết tôi đã cung cấp ở trên. Sự khác biệt rất quan trọng ở chỗ liên kết tôi cung cấp không đưa ra bất kỳ cảnh báo nào đối với tuyên bố về độ an toàn của luồng. Tôi cũng có kinh nghiệm cá nhân khi sử dụng MemoryCache.Defaultvới âm lượng rất cao (hàng triệu lượt truy cập bộ nhớ cache mỗi phút) mà không gặp vấn đề về luồng.
EkoostikMartin

Tôi nghĩ ý của họ là các hoạt động đọc và ghi diễn ra theo nguyên tử. Tóm lại, trong khi luồng A cố gắng đọc giá trị hiện tại, nó luôn đọc các hoạt động ghi đã hoàn thành, không phải ở giữa việc ghi dữ liệu vào bộ nhớ bởi bất kỳ luồng nào. Khi luồng A cố gắng để ghi vào bộ nhớ không có sự can thiệp nào có thể được thực hiện bởi bất kỳ luồng nào. Đó là sự hiểu biết của tôi nhưng có rất nhiều câu hỏi / bài báo về điều đó không dẫn đến một kết luận hoàn chỉnh như sau. stackoverflow.com/questions/3137931/msdn-what-is-thread-safety
erhan355

3

Vừa tải lên thư viện mẫu để giải quyết vấn đề cho .Net 2.0.

Hãy xem repo này:

RedisLazyCache

Tôi đang sử dụng bộ nhớ cache của Redis nhưng nó cũng chuyển đổi dự phòng hoặc chỉ Memorycache nếu thiếu Connectionstring.

Nó dựa trên thư viện LazyCache đảm bảo thực hiện một lệnh gọi lại để ghi trong trường hợp đa luồng cố gắng tải và lưu dữ liệu, đặc biệt nếu lệnh gọi lại rất tốn kém để thực thi.


Vui lòng chỉ chia sẻ câu trả lời, các thông tin khác có thể được chia sẻ dưới dạng bình luận
ElasticCode

1
@WaelAbbas. Tôi đã thử nhưng có vẻ như tôi cần 50 danh tiếng trước. : D. Mặc dù đây không phải là câu trả lời trực tiếp cho câu hỏi OP (có thể được trả lời bằng Có / Không với một số giải thích tại sao), câu trả lời của tôi là một giải pháp khả thi cho câu trả lời Không.
Francis Marasigan

1

Như đã đề cập bởi @AmitE trong câu trả lời của @pimbrouwers, ví dụ của anh ấy không hoạt động như được minh họa ở đây:

class Program
{
    static async Task Main(string[] args)
    {
        var cache = new MemoryCache(new MemoryCacheOptions());

        var tasks = new List<Task>();
        var counter = 0;

        for (int i = 0; i < 10; i++)
        {
            var loc = i;
            tasks.Add(Task.Run(() =>
            {
                var x = GetOrAdd(cache, "test", TimeSpan.FromMinutes(1), () => Interlocked.Increment(ref counter));
                Console.WriteLine($"Interation {loc} got {x}");
            }));
        }

        await Task.WhenAll(tasks);
        Console.WriteLine("Total value creations: " + counter);
        Console.ReadKey();
    }

    public static T GetOrAdd<T>(IMemoryCache cache, string key, TimeSpan expiration, Func<T> valueFactory)
    {
        return cache.GetOrCreate(key, entry =>
        {
            entry.SetSlidingExpiration(expiration);
            return new Lazy<T>(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication);
        }).Value;
    }
}

Đầu ra:

Interation 6 got 8
Interation 7 got 6
Interation 2 got 3
Interation 3 got 2
Interation 4 got 10
Interation 8 got 9
Interation 5 got 4
Interation 9 got 1
Interation 1 got 5
Interation 0 got 7
Total value creations: 10

Có vẻ như GetOrCreateluôn trả về mục nhập đã tạo. May mắn thay, điều đó rất dễ sửa:

public static T GetOrSetValueSafe<T>(IMemoryCache cache, string key, TimeSpan expiration,
    Func<T> valueFactory)
{
    if (cache.TryGetValue(key, out Lazy<T> cachedValue))
        return cachedValue.Value;

    cache.GetOrCreate(key, entry =>
    {
        entry.SetSlidingExpiration(expiration);
        return new Lazy<T>(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication);
    });

    return cache.Get<Lazy<T>>(key).Value;
}

Điều đó hoạt động như mong đợi:

Interation 4 got 1
Interation 9 got 1
Interation 1 got 1
Interation 8 got 1
Interation 0 got 1
Interation 6 got 1
Interation 7 got 1
Interation 2 got 1
Interation 5 got 1
Interation 3 got 1
Total value creations: 1

2
Điều này cũng không hoạt động. Chỉ cần cố gắng nó nhiều lần và bạn sẽ thấy rằng đôi khi giá trị không phải lúc nào cũng 1.
mlessard

1

Bộ nhớ cache là an toàn cho luồng, nhưng giống như những người khác đã nêu, có thể GetOrAdd sẽ gọi nhiều loại func nếu gọi từ nhiều loại.

Đây là bản sửa lỗi tối thiểu của tôi về điều đó

private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);

await _cacheLock.WaitAsync();
var data = await _cache.GetOrCreateAsync(key, entry => ...);
_cacheLock.Release();

Tôi nghĩ đó là một giải pháp tốt, nhưng nếu tôi có nhiều phương pháp thay đổi các bộ nhớ đệm khác nhau, nếu tôi sử dụng khóa, chúng sẽ bị khóa mà không cần thiết! Trong trường hợp này, chúng ta nên có nhiều _cacheLocks, tôi nghĩ sẽ tốt hơn nếu cachelock cũng có thể có một Key!
Daniel

Có nhiều cách để giải quyết nó, một là chung chung semaphore sẽ là duy nhất cho mỗi trường hợp của MyCache <T>. Sau đó, bạn có thể đăng ký nó AddSingleton (typeof (IMyCache <>), typeof (MyCache <>));
Anders

Tôi có lẽ sẽ không làm cho toàn bộ bộ nhớ cache trở thành một singleton có thể dẫn đến rắc rối nếu bạn cần gọi các kiểu tạm thời khác. Vì vậy, có thể có một lưu trữ semaphore ICacheLock <T> là singleton
Anders

Vấn đề với phiên bản này là nếu bạn có 2 thứ khác nhau để lưu vào bộ nhớ cache cùng một lúc thì bạn phải đợi cái đầu tiên tạo xong trước khi bạn có thể kiểm tra bộ nhớ cache cho cái thứ hai. Sẽ hiệu quả hơn khi cả hai đều có thể kiểm tra bộ nhớ đệm (và tạo) cùng một lúc nếu các khóa khác nhau. LazyCache sử dụng kết hợp Lazy <T> và khóa sọc để đảm bảo các mục của bạn được lưu vào bộ nhớ cache nhanh nhất có thể và chỉ tạo một lần cho mỗi khóa. Xem github.com/alastairtree/lazycache
alastairtree
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.