Как правильно блокировать одновременные несколько запросов

#c# #caching #concurrency

Вопрос:

У меня есть кэш автоматического обновления в нашей системе, в котором возникают некоторые проблемы из-за условий гонки.

Во время запуска _internalCache параллельный словарь пуст.

Это было реализовано много лет назад в качестве универсального кэша автоматического обновления, используемого в нашей системе.

Действие обновления, которое вызывает большинство проблем, обновляет несколько тысяч строк из базы данных.

 public bool TryGet(TKey key, out TValue value)
{
    if (_internalCache.TryGetValue(key, out value))
    {
        return true;
    }
    lock (_internalCache.SyncRoot)
    {
        this._refreshCacheAction(this._internalCache);
        return _internalCache.TryGetValue(key, out value);
    }
}
 

Если одновременно поступает несколько запросов (что случается чаще, чем мне хотелось бы) Затем мы несколько раз обновляем наш кэш.

Редактировать: После дальнейшего обсуждения в комментариях, похоже, что этот кэш серьезно поврежден. Некоторые из наших клиентов испытывают тайм-ауты, которые мне нужно быстро исправить.

Как я могу предотвратить многократное обновление кэша? (Приветствуются хаки Дженки)

Комментарии:

1. Как насчет а ConcurrentDictionary или а MemoryCache ?

2. _internalCache является эквивалентом ConcurrentDictionary

3. Это похоже на фундаментальную проблему с параллельными запросами — в какой-то момент вам нужно либо опустить ногу и сказать «нет, каждый запрос будет обрабатываться по одному», либо согласиться с «возможной согласованностью», где каждый раз, когда вы попадаете в кэш, результаты могут немного отличаться.

4. @johnny5 Что вы делаете в _refreshCacheAction? Похоже, это не зависит от не найденного ключа? Вы обновляете/перестраиваете весь кэш при каждом промахе?

5. Да, судя по этому небольшому фрагменту, весь дизайн кэша кажется серьезно нарушенным. Возможно, вам придется переделать его с нуля (почти).

Ответ №1:

В целом дизайн выглядит ущербным. Возможно, можно использовать даже стандартный компонент, такой как ConcurrentDictionary или MemoryCache.

Однако одним из возможных исправлений является повторная проверка значения во внутреннем кэше внутри блокировки. Это должно сократить количество раз, когда выполняется действие обновления.

 public bool TryGet(TKey key, out TValue value)
{
    if (_internalCache.TryGetValue(key, out value))
    {
        return true;
    }
    lock (_internalCache.SyncRoot)
    {
        // cache has already been refreshed
        if (_internalCache.TryGetValue(key, out value))
        {
           return true;
        }

        // refresh cache
        this._refreshCacheAction(this._internalCache);
        return _internalCache.TryGetValue(key, out value);
    }
}
 

Ответ №2:

Убедитесь, что класс, содержащий TryGet , является одноэлементным экземпляром во всех вызовах, поскольку он должен создаваться только один раз в течение срока службы приложения. Достаточно конструктора частного экземпляра в сочетании со статическим свойством класса, которое ссылается на единственный экземпляр, созданный в статическом конструкторе класса:

 public class ASingletonClass
{
    static ASingletonClass()
    {
         Instance = new ASingletonClass();
    }

    private ASingletonClass()
    {
    }

    public static ASingletonClass Instance { get; private set; }
}
 

Кроме того, замените вызов SyncRoot новым полем объекта в вашем классе, равным new object() . Более новые коллекции, такие как ConcurrentDictionary, не поддерживают SyncRoot.