TGViewer
.NET Разработчик .NET Разработчик @netdeveloperdiary · 6.74K subscribers
Post #3273 1.65K
День 2732. #ЗаметкиНаПолях
Решаем Проблему Паники в Кэше
В приложении ASP.NET Core API кэшировался довольно ресурсоёмкий отчёт на 60 секунд. При обычной нагрузке всё было нормально: один запрос перестраивал кэш, все остальные читали из него. При пиковой нагрузке десятки запросов поступали одновременно, все видели промах кэша и параллельно выполняли один и тот же ресурсоёмкий запрос. Слой кэширования не помогал. Это называется «паникой в кэше» (cache stampede).

Почему это происходит
IMemoryCache.GetOrCreate (и его асинхронный аналог) сами по себе не добавляют блокировок:
public async Task<Report> GetReportAsync(string key)
{
if (_cache.TryGetValue(key, out Report cached))
return cached;

var report =
await _service.BuildReportAsync(key);

_cache.Set(key, report, TimeSpan.FromSeconds(60));

return report;
}

Если 10 запросов придут одновременно после истечения срока действия кэша, для всех TryGetValue вернёт false, и все они вызовут BuildReportAsync. Это не специфично для IMemoryCache. Распределённые кэши, такие как Redis, имеют ту же проблему. Сам кэш не знает и не заботится о том, что параллельные вызывающие процессы собираются запросить тот же ключ.

Решение 1: блокировка для каждого ключа с помощью SemaphoreSlim
Самое простое решение — заставить одновременные вызывающие процессы для одного и того же ключа ждать завершения первого:
private static readonly 
ConcurrentDictionary<string, SemaphoreSlim> _locks = new();

public async Task<Report> GetReportAsync(string key)
{
if (_cache.TryGetValue(key, out Report cached))
return cached;

var keyLock = _locks.GetOrAdd(key,
_ => new SemaphoreSlim(1, 1));

await keyLock.WaitAsync();
try
{
// Перепроверка: другой запрос мог уже
// задать значение, пока мы ждали семафора
if (_cache.TryGetValue(key, out cached))
return cached;

var report = await _service.BuildReportAsync(key);
_cache.Set(key, report, TimeSpan.FromSeconds(60));
return report;
}
finally
{
keyLock.Release();
}
}

Примечание: ConcurrentDictionary<string, SemaphoreSlim> будет бесконечно расти, если его не чистить. Для небольшого набора ключей это нормально. Иначе - удаляйте неиспользуемые семафоры.

Решение 2: HybridCache сделает это за вас
Microsoft.Extensions.Caching.Hybrid.HybridCache координирует одновременные вызовы для одного и того же ключа, так что одновременно выполняется только один вызов:
// регистрация
services.AddHybridCache(options =>
{
opts.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromSeconds(60),
LocalCacheExpiration = TimeSpan.FromSeconds(60)
};
});

// использование
public async Task<Report> GetReportAsync(string key)
{
return await _cache.GetOrCreateAsync(
key,
async ct => await _service.BuildReportAsync(key, ct),
cancellationToken: default);
}

HybridCache также предоставляет двухуровневый кэш. Защита от «паники» применяется на локальном уровне на каждом узле; она не предотвращает одновременную перестройку одного и того же ключа двумя разными узлами в кластере, поскольку отсутствует межмашинная блокировка. Для этого потребуется распределённая блокировка или придётся смириться с периодическим двойным перестроением на разных узлах как с менее масштабной версией той же проблемы.

Решение 3: не допускать одновременного истечения срока действия всего кэша
Даже при блокировке по ключу, проблема может возникать, если срок действия множества разных ключей истекает одновременно. Решение - добавить разброс (jitter):
var jitter = TimeSpan.FromSeconds(Random.Shared.Next(0, 10));

_cache.Set(key, report, TimeSpan.FromSeconds(60) + jitter);


Источник:
https://steven-giesel.com/blogPost/605274d2-719b-4b1f-b0d5-3ad9001d9b56/adding-static-getter-thanks-to-extensions
  • 👍 11
More from @netdeveloperdiary
  1. Sep 26, 2026День 2796. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Продолжение Начало Три…
  2. Sep 25, 2026День 2795. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Начало Проблема с позиц…
  3. Sep 24, 2026День 2794. #Оффтоп #Здоровье Сегодня будет необычный пост. Завтра в Москве стартует конфер…
  4. Sep 23, 2026День 2793. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
  5. Sep 22, 2026День 2792. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
  6. Sep 21, 2026🔍Тестовое собеседование с Senior C# разработчиком уже завтра 22 сентября(уже завтра!) в 1…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →