長時間実行されるプロセスなどの特定の状況では、ASP.NET キャッシュをロックして、そのリソースに対する別のユーザーによる後続の要求が、キャッシュにヒットする代わりに長いプロセスを再度実行することを回避することが重要であることを知っています。
ASP.NETでキャッシュロックを実装するC#での最良の方法は何ですか?
基本的なパターンは次のとおりです。
コードでは、次のようになります。
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
}
完全を期すために、完全な例は次のようになります。
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;
キャッシュ インスタンス全体をロックする必要はなく、挿入する特定のキーのみをロックする必要があります。つまり、男性用トイレを使用している間、女性用トイレへのアクセスをブロックする必要はありません:)
以下の実装では、並行辞書を使用して特定のキャッシュ キーをロックできます。このようにして、2 つの異なるキーに対して同時に GetOrAdd() を実行できますが、同じキーに対して同時に実行することはできません。
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;
}
}
Pavel が言ったことを繰り返しますが、これが最もスレッドセーフな書き方だと思います
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;
}
Craig Shoemakerは、asp.netキャッシングで優れたショーを行いました:http: //polymorphicpodcast.com/shows/webperformance/
最近、Correct State Bag Access Pattern と呼ばれる 1 つのパターンを見ましたが、これに触れているようです。
スレッドセーフになるように少し変更しました。
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;
}
CodeGuruのこの記事では、さまざまなキャッシュロックのシナリオと、ASP.NETキャッシュロックのいくつかのベストプラクティスについて説明しています。
その特定の問題を解決するライブラリを作成しました: Rocks.Caching
また、この問題について詳しくブログに書き、なぜ重要なのかをここで説明しました。