#c# #.net #.net-core
#c# #.net #.net-ядро
Вопрос:
Я использую приведенный ниже код для кэширования элементов. Это довольно просто.
Проблема, с которой я сталкиваюсь, заключается в том, что каждый раз, когда он кэширует элемент, блокируется часть кода. Итак, примерно миллион элементов, поступающих каждый час или около того, это проблема.
Я попытался создать словарь объектов статической блокировки для каждого CacheKey, чтобы блокировка была детализированной, но это само по себе становится проблемой при управлении их истечением срока действия и т.д…
Есть ли лучший способ реализовать минимальную блокировку?
private static readonly object cacheLock = new object();
public static T GetFromCache<T>(string cacheKey, 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 retrieval.
T cachedData = MemoryCache.Default.Get(cacheKey) 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) as T;
if (cachedData != null) {
return cachedData;
}
// The value still did not exist so we now write it in to the cache.
cachedData = GetData();
MemoryCache.Default.Set(cacheKey, cachedData, new CacheItemPolicy(...));
return cachedData;
}
}
Комментарии:
1. Вы говорите, что у вас миллион запросов кеша в час, но как часто вы создаете новые кеши? Если у вас всего 5 кэшей и они очищаются в среднем каждые 30 минут, то ваша блокировка практически не влияет на ваши накладные расходы. С другой стороны, если вы заполняете 10 кэшей каждые 30 секунд, ваша стратегия блокировки приведет к большим накладным расходам. также имеет значение, как часто вы запрашиваете элементы из разных кэшей, которые не заполнены. Если этого много, вам не следует этого делать, если это не так, вряд ли это узкое место.
2. В течение часа поступает около миллиона
new
элементов @Servy. Таким образом, блокировка будет установлена, элемент извлечен и добавлен в кэш.
Ответ №1:
Возможно, вы захотите рассмотреть возможность использования ReaderWriterLockSlim, с помощью которого вы можете получить блокировку записи только при необходимости.
Использование cacheLock.EnterReadLock();
и cacheLock.EnterWriteLock();
должно значительно повысить производительность.
В приведенной мной ссылке даже есть пример кэша, именно то, что вам нужно, я копирую здесь:
public class SynchronizedCache
{
private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
private Dictionary<int, string> innerCache = new Dictionary<int, string>();
public int Count
{ get { return innerCache.Count; } }
public string Read(int key)
{
cacheLock.EnterReadLock();
try
{
return innerCache[key];
}
finally
{
cacheLock.ExitReadLock();
}
}
public void Add(int key, string value)
{
cacheLock.EnterWriteLock();
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
}
public bool AddWithTimeout(int key, string value, int timeout)
{
if (cacheLock.TryEnterWriteLock(timeout))
{
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
return true;
}
else
{
return false;
}
}
public AddOrUpdateStatus AddOrUpdate(int key, string value)
{
cacheLock.EnterUpgradeableReadLock();
try
{
string result = null;
if (innerCache.TryGetValue(key, out result))
{
if (result == value)
{
return AddOrUpdateStatus.Unchanged;
}
else
{
cacheLock.EnterWriteLock();
try
{
innerCache[key] = value;
}
finally
{
cacheLock.ExitWriteLock();
}
return AddOrUpdateStatus.Updated;
}
}
else
{
cacheLock.EnterWriteLock();
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
return AddOrUpdateStatus.Added;
}
}
finally
{
cacheLock.ExitUpgradeableReadLock();
}
}
public void Delete(int key)
{
cacheLock.EnterWriteLock();
try
{
innerCache.Remove(key);
}
finally
{
cacheLock.ExitWriteLock();
}
}
public enum AddOrUpdateStatus
{
Added,
Updated,
Unchanged
};
~SynchronizedCache()
{
if (cacheLock != null) cacheLock.Dispose();
}
}
Ответ №2:
Я не знаю, как MemoryCache.Default
это реализовано, и есть ли у вас над этим контроль. Но в целом, предпочитаю использовать ConcurrentDictionary
вместо Dictionary
с блокировкой в многопоточной среде.
GetFromCache
просто стало бы
ConcurrentDictionary<string, T> cache = new ConcurrentDictionary<string, T>();
...
cache.GetOrAdd("someKey", (key) =>
{
var data = PullDataFromDatabase(key);
return data;
});
Есть еще две вещи, о которых нужно позаботиться.
Срок действия
Вместо сохранения T
в качестве значения словаря вы можете определить тип
struct CacheItem<T>
{
public T Item { get; set; }
public DateTime Expiry { get; set; }
}
И сохраняем кэш как CacheItem
с определенным сроком действия.
cache.GetOrAdd("someKey", (key) =>
{
var data = PullDataFromDatabase(key);
return new CacheItem<T>() { Item = data, Expiry = DateTime.UtcNow.Add(TimeSpan.FromHours(1)) };
});
Теперь вы можете реализовать истечение срока действия в асинхронном потоке.
Timer expirationTimer = new Timer(ExpireCache, null, 60000, 60000);
...
void ExpireCache(object state)
{
var needToExpire = cache.Where(c => DateTime.UtcNow >= c.Value.Expiry).Select(c => c.Key);
foreach (var key in needToExpire)
{
cache.TryRemove(key, out CacheItem<T> _);
}
}
Раз в минуту выполняется поиск всех записей кэша, срок действия которых должен истечь, и их удаление.
«Блокировка»
Использование ConcurrentDictionary
гарантирует, что одновременное чтение / запись не приведет к повреждению словаря или возникновению исключения. Но вы все равно можете столкнуться с ситуацией, когда два одновременных чтения заставят вас дважды извлекать данные из базы данных.
Один из способов решить эту проблему — обернуть значение словаря с Lazy
ConcurrentDictionary<string, Lazy<CacheItem<T>>> cache = new ConcurrentDictionary<string, Lazy<CacheItem<T>>>();
...
var data = cache.GetOrData("someKey", key => new Lazy<CacheItem<T>>(() =>
{
var data = PullDataFromDatabase(key);
return new CacheItem<T>() { Item = data, Expiry = DateTime.UtcNow.Add(TimeSpan.FromHours(1)) };
})).Value;
Объяснение
с GetOrAdd
вы могли бы в конечном итоге вызвать делегат «получить из базы данных, если не в кэше» несколько раз в случае одновременных запросов. Однако GetOrAdd
в конечном итоге будет использоваться только одно из значений, возвращенных делегатом, и, возвращая Lazy
, вы гарантируете, что будет вызвано только одно Lazy
.
Комментарии:
1. Теперь у вас никогда ничего не истекает и, возможно, вы выполняете работу по загрузке кэшей несколько раз. В коде операционной системы нет ни одной проблемы.
2. Трюк с Lazy<T> проблематичен (или был в других реализациях), потому что, если
PullFromDatabase
возвращает исключение, оно сохранит исключение в кэше. Не так ли?3. @AngryHacker Вы правы, я не думал об этом. Я думаю, что обходным путем было бы перехватить исключение внутри отложенного делегата и установить для кэша значение null в
catch
блоке. Затем проверьте проверку, чтобы увидеть, имеет ли она значение null. если это так, удалите элемент из кэша (и при необходимости создайте исключение).