Cache na memória no ASP.NET Core

Por Rick Anderson, John Luo, e Steve Smith

O cache pode melhorar significativamente o desempenho e a escalabilidade de um aplicativo reduzindo o trabalho necessário para gerar conteúdo. O cache funciona melhor com dados que são alterados com pouca frequência e são caros de gerar. O cache faz uma cópia dos dados que podem ser retornados muito mais rapidamente do que da origem. Os aplicativos devem ser gravados e testados para nunca dependerem de dados armazenados em cache.

ASP.NET Core dá suporte a vários caches diferentes. O cache mais simples é baseado no IMemoryCache. IMemoryCache representa um cache armazenado na memória do servidor Web. Os aplicativos em execução em um farm de servidores (vários servidores) devem garantir que as sessões sejam autoadesivas ao usar o cache na memória. As sessões autoadesivas garantem que as solicitações de um cliente vão para o mesmo servidor. Por exemplo, os aplicativos Web do Azure usam o ARR (Roteamento de Solicitações de Aplicativo) para rotear todas as solicitações para o mesmo servidor.

Sessões não autoadesivas em um web farm exigem um cache distribuído para evitar problemas de consistência de cache. Para alguns aplicativos, um cache distribuído pode dar suporte a expansão maior do que um cache na memória. O uso de um cache distribuído descarrega a memória do cache para um processo externo.

O cache na memória pode armazenar qualquer objeto. A interface de cache distribuído é limitada a byte[]. Os itens de cache armazenados em cache na memória e distribuídos como pares chave-valor.

System.Runtime.Caching/MemoryCache

System.Runtime.Caching/MemoryCache (Pacote NuGet) pode ser usado com:

  • .NET Standard 2.0 ou posterior.
  • Qualquer implementação do .NET direcionada ao .NET Standard 2.0 ou posterior. Por exemplo, ASP.NET Core 3.1 ou posterior.
  • .NET Framework 4.5 ou posterior.

Microsoft.Extensions.Caching.Memory/IMemoryCache (descrito neste artigo) é recomendadoSystem.Runtime.Caching/MemoryCacheporque é melhor integrado ao ASP.NET Core. Por exemplo, IMemoryCache funciona nativamente com ASP.NET Core injeção de dependência.

Use System.Runtime.Caching/MemoryCache como uma ponte de compatibilidade ao portar código de ASP.NET 4.x para ASP.NET Core.

Diretrizes de uso

  • O código sempre deve ter uma opção de fallback para buscar dados e não depender de um valor armazenado em cache disponível.
  • O cache usa um recurso escasso, memória. Limitar o crescimento do cache:
    • Não insira entrada externa no cache. Por exemplo, não é recomendável usar a entrada arbitrária fornecida pelo usuário como uma chave de cache, pois a entrada pode consumir uma quantidade imprevisível de memória.
    • Use expirações para limitar o crescimento do cache.
    • Use SetSize, Size e SizeLimit para limitar o tamanho do cache. O runtime do ASP.NET Core não limita o tamanho do cache com base na pressão de memória. Cabe ao desenvolvedor limitar o tamanho do cache.

Usar IMemoryCache

Aviso

Usar um cache de memória compartilhada da Injeção de Dependência e chamar SetSize, Sizeou SizeLimit para limitar o tamanho do cache pode fazer com que o aplicativo falhe. Quando um limite de tamanho é definido em um cache, todas as entradas devem especificar um tamanho ao serem adicionadas. Isso pode levar a problemas, pois os desenvolvedores podem não ter controle total sobre o que usa o cache compartilhado. Ao usar SetSize, Size ou SizeLimit para limitar o cache, crie um singleton de cache para cache. Para obter mais informações e um exemplo, consulte Usar SetSize, Size e SizeLimit para limitar o tamanho do cache. Um cache compartilhado é compartilhado por outras estruturas ou bibliotecas.

O cache na memória é um serviço referenciado de um aplicativo usando a Injeção de Dependência. Solicite a IMemoryCache instância no construtor:

public class IndexModel : PageModel
{
    private readonly IMemoryCache _memoryCache;

    public IndexModel(IMemoryCache memoryCache) =>
        _memoryCache = memoryCache;

    // ...

O código a seguir usa TryGetValue para marcar se uma hora estiver no cache. Se uma hora não for armazenada em cache, uma nova entrada será criada e adicionada ao cache com Set:

public void OnGet()
{
    CurrentDateTime = DateTime.Now;

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = CurrentDateTime;

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
    }

    CacheCurrentDateTime = cacheValue;
}

No código anterior, a entrada de cache é configurada com uma expiração deslizante de três segundos. Se a entrada de cache não for acessada por mais de três segundos, ela será removida do cache. Cada vez que a entrada de cache é acessada, ela permanece no cache por mais 3 segundos. A CacheKeys classe faz parte do exemplo de download.

A hora atual e a hora armazenada em cache são exibidas:

<ul>
    <li>Current Time: @Model.CurrentDateTime</li>
    <li>Cached Time: @Model.CacheCurrentDateTime</li>
</ul>

O código a seguir usa o Set método de extensão para armazenar dados em cache por um tempo relativo sem MemoryCacheEntryOptions:

_memoryCache.Set(CacheKeys.Entry, DateTime.Now, TimeSpan.FromDays(1));

No código anterior, a entrada de cache é configurada com uma expiração relativa de um dia. A entrada de cache é removida do cache após um dia, mesmo que seja acessada dentro desse período de tempo limite.

O código a seguir usa GetOrCreate e GetOrCreateAsync para armazenar dados em cache.

public void OnGetCacheGetOrCreate()
{
    var cachedValue = _memoryCache.GetOrCreate(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return DateTime.Now;
        });

    // ...
}

public async Task OnGetCacheGetOrCreateAsync()
{
    var cachedValue = await _memoryCache.GetOrCreateAsync(
        CacheKeys.Entry,
        cacheEntry =>
        {
            cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    // ...
}

O código a seguir chama Get para buscar o tempo armazenado em cache:

var cacheEntry = _memoryCache.Get<DateTime?>(CacheKeys.Entry);

O código a seguir obtém ou cria um item armazenado em cache com expiração absoluta:

var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.Entry,
    cacheEntry =>
    {
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

Um conjunto de itens armazenados em cache com apenas uma expiração deslizante corre o risco de nunca expirar. Se o item armazenado em cache for acessado repetidamente dentro do intervalo de expiração deslizante, o item nunca expirará. Combine uma expiração deslizante com uma expiração absoluta para garantir que o item expire. A expiração absoluta define um limite superior em quanto tempo o item pode ser armazenado em cache enquanto ainda permite que o item expire anteriormente se ele não for solicitado dentro do intervalo de expiração deslizante. Se o intervalo de expiração deslizante ou o tempo de expiração absoluto passar, o item será removido do cache.

O código a seguir obtém ou cria um item armazenado em cache com expiração deslizante e absoluta:

var cachedValue = _memoryCache.GetOrCreate(
    CacheKeys.CallbackEntry,
    cacheEntry =>
    {
        cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(3);
        cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

O código anterior garante que os dados não serão armazenados em cache por mais tempo do que o tempo absoluto.

GetOrCreate, GetOrCreateAsync e Get são métodos de extensão na CacheExtensions classe. Esses métodos estendem a funcionalidade de IMemoryCache.

MemoryCacheEntryOptions

O exemplo a seguir:

  • Define a prioridade do cache como CacheItemPriority.NeverRemove.
  • Define um PostEvictionDelegate que é chamado depois que a entrada é removida do cache. O retorno de chamada é executado em um thread diferente do código que remove o item do cache.
public void OnGetCacheRegisterPostEvictionCallback()
{
    var memoryCacheEntryOptions = new MemoryCacheEntryOptions()
        .SetPriority(CacheItemPriority.NeverRemove)
        .RegisterPostEvictionCallback(PostEvictionCallback, _memoryCache);

    _memoryCache.Set(CacheKeys.CallbackEntry, DateTime.Now, memoryCacheEntryOptions);
}

private static void PostEvictionCallback(
    object cacheKey, object cacheValue, EvictionReason evictionReason, object state)
{
    var memoryCache = (IMemoryCache)state;

    memoryCache.Set(
        CacheKeys.CallbackMessage,
        $"Entry {cacheKey} was evicted: {evictionReason}.");
}

Usar SetSize, Size e SizeLimit para limitar o tamanho do cache

Opcionalmente, uma instância MemoryCache pode especificar e impor um limite de tamanho. O limite de tamanho do cache não tem uma unidade de medida definida porque o cache não tem mecanismo para medir o tamanho das entradas. Se o limite de tamanho do cache estiver definido, todas as entradas deverão especificar o tamanho. O runtime do ASP.NET Core não limita o tamanho do cache com base na pressão de memória. Cabe ao desenvolvedor limitar o tamanho do cache. O tamanho especificado está em unidades escolhidas pelo desenvolvedor.

Por exemplo:

  • Se o aplicativo Web estivesse armazenando principalmente cadeias de caracteres em cache, cada tamanho de entrada de cache poderia ser o comprimento da cadeia de caracteres.
  • O aplicativo pode especificar o tamanho de todas as entradas como 1 e o limite de tamanho é a contagem de entradas.

Se SizeLimit não estiver definido, o cache crescerá sem associação. O runtime ASP.NET Core não corta o cache quando a memória do sistema está baixa. Os aplicativos devem ser projetados para:

  • Limitar o crescimento do cache.
  • Chame Compact ou Remove quando a memória disponível for limitada.

O código a seguir cria um tamanho MemoryCache fixo sem unidade acessível por injeção de dependência:

public class MyMemoryCache
{
    public MemoryCache Cache { get; } = new MemoryCache(
        new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
}

SizeLimit não tem unidades. As entradas armazenadas em cache devem especificar o tamanho em todas as unidades que considerarem mais apropriadas se o limite de tamanho do cache tiver sido definido. Todos os usuários de uma instância de cache devem usar o mesmo sistema de unidade. Uma entrada não será armazenada em cache se a soma dos tamanhos de entrada armazenados em cache exceder o valor especificado por SizeLimit. Se nenhum limite de tamanho de cache for definido, o tamanho do cache definido na entrada será ignorado.

O código a seguir MyMemoryCache registra com o contêiner de injeção de dependência:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddSingleton<MyMemoryCache>();

MyMemoryCache é criado como um cache de memória independente para componentes que estão cientes desse tamanho de cache limitado e sabem como definir o tamanho da entrada de cache adequadamente.

O tamanho da entrada de cache pode ser definido usando o SetSize método de extensão ou a propriedade Size:

if (!_myMemoryCache.Cache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        .SetSize(1);

    // cacheEntryOptions.Size = 1;

    _myMemoryCache.Cache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
}

No código anterior, as duas linhas realçadas obtêm o mesmo resultado de definir o tamanho da entrada de cache. SetSize é fornecido para conveniência ao encadear chamadas em new MemoryCacheOptions().

MemoryCache.Compact

MemoryCache.Compact tenta remover o percentual especificado do cache na seguinte ordem:

  • Todos os itens expirados.
  • Itens por prioridade. Os itens de prioridade mais baixos são removidos primeiro.
  • Objetos menos usados recentemente.
  • Itens com a expiração absoluta mais antiga.
  • Itens com a expiração deslizante mais antiga.

Itens fixados com prioridade NeverRemove nunca são removidos. O código a seguir remove um item de cache e chama Compact para remover 25% das entradas armazenadas em cache:

_myMemoryCache.Cache.Remove(CacheKeys.Entry);
_myMemoryCache.Cache.Compact(.25);

Para obter mais informações, consulte a origem compacta no GitHub.

Armazenar dependências em cache

O exemplo a seguir mostra como expirar uma entrada de cache se uma entrada dependente expirar. Um CancellationChangeToken é adicionado ao item armazenado em cache. Quando Cancel é chamado no CancellationTokenSource, ambas as entradas de cache são removidas:

public void OnGetCacheCreateDependent()
{
    var cancellationTokenSource = new CancellationTokenSource();

    _memoryCache.Set(
        CacheKeys.DependentCancellationTokenSource,
        cancellationTokenSource);

    using var parentCacheEntry = _memoryCache.CreateEntry(CacheKeys.Parent);

    parentCacheEntry.Value = DateTime.Now;

    _memoryCache.Set(
        CacheKeys.Child,
        DateTime.Now,
        new CancellationChangeToken(cancellationTokenSource.Token));
}

public void OnGetCacheRemoveDependent()
{
    var cancellationTokenSource = _memoryCache.Get<CancellationTokenSource>(
        CacheKeys.DependentCancellationTokenSource);

    cancellationTokenSource.Cancel();
}

O uso de um CancellationTokenSource permite que várias entradas de cache sejam removidas como um grupo. Com o using padrão no código acima, as entradas de cache criadas dentro do using escopo herdam gatilhos e configurações de expiração.

Observações adicionais

  • A expiração não acontece em segundo plano. Não há temporizador que verifique ativamente o cache em busca de itens expirados. Qualquer atividade no cache (Get, Set, Remove) pode disparar uma verificação em segundo plano para itens expirados. Um temporizador no CancellationTokenSource (CancelAfter) também remove a entrada e dispara uma verificação de itens expirados. O exemplo a seguir usa CancellationTokenSource(TimeSpan) para o token registrado. Quando esse token é acionado, ele remove a entrada imediatamente e dispara os retornos de chamada de remoção:

    if (!_memoryCache.TryGetValue(CacheKeys.Entry, out DateTime cacheValue))
    {
        cacheValue = DateTime.Now;
    
        var cancellationTokenSource = new CancellationTokenSource(
            TimeSpan.FromSeconds(10));
    
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .AddExpirationToken(
                new CancellationChangeToken(cancellationTokenSource.Token))
            .RegisterPostEvictionCallback((key, value, reason, state) =>
            {
                ((CancellationTokenSource)state).Dispose();
            }, cancellationTokenSource);
    
        _memoryCache.Set(CacheKeys.Entry, cacheValue, cacheEntryOptions);
    }
    
  • Ao usar um retorno de chamada para repovoar um item de cache:

    • Várias solicitações podem localizar o valor da chave armazenada em cache vazio porque o retorno de chamada não foi concluído.
    • Isso pode resultar em vários threads repovoando o item armazenado em cache.
  • Quando uma entrada de cache é usada para criar outra, o filho copia os tokens de expiração da entrada pai e as configurações de expiração baseadas em tempo. O filho não expira por remoção manual ou atualização da entrada pai.

  • Use PostEvictionCallbacks para definir as callbacks que serão disparadas depois que a entrada do cache for removida do cache.

  • Para a maioria dos aplicativos, IMemoryCache está habilitado. Por exemplo, chamar AddMvc, AddControllersWithViews, AddRazorPages, AddMvcCore().AddRazorViewEngine e muitos outros métodos Add{Service} em Program.cs, habilita IMemoryCache. Para aplicativos que não chamam um dos métodos anteriores Add{Service}, pode ser necessário chamar AddMemoryCache no Program.cs.

Atualização de cache em segundo plano

Use um serviço em segundo plano, como IHostedService para atualizar o cache. O serviço em segundo plano pode recomputar as entradas e atribuí-las ao cache somente quando estiverem prontas.

Recursos adicionais

Exibir ou baixar código de exemplo (como baixar)

Noções básicas de cache

O cache pode melhorar significativamente o desempenho e a escalabilidade de um aplicativo reduzindo o trabalho necessário para gerar conteúdo. O cache funciona melhor com dados que são alterados com pouca frequência e são caros de gerar. O cache faz uma cópia dos dados que podem ser retornados muito mais rapidamente do que da origem. Os aplicativos devem ser gravados e testados para nunca dependerem de dados armazenados em cache.

ASP.NET Core dá suporte a vários caches diferentes. O cache mais simples é baseado no IMemoryCache. IMemoryCache representa um cache armazenado na memória do servidor Web. Os aplicativos em execução em um farm de servidores (vários servidores) devem garantir que as sessões sejam autoadesivas ao usar o cache na memória. As sessões autoadesivas garantem que as solicitações subsequentes de um cliente acessem o mesmo servidor. Por exemplo, os aplicativos Web do Azure usam o ARR (Roteamento de Solicitação de Aplicativo Web) para rotear todas as solicitações subsequentes para o mesmo servidor.

Sessões não autoadesivas em um web farm exigem um cache distribuído para evitar problemas de consistência de cache. Para alguns aplicativos, um cache distribuído pode dar suporte a expansão maior do que um cache na memória. O uso de um cache distribuído descarrega a memória do cache para um processo externo.

O cache na memória pode armazenar qualquer objeto. A interface de cache distribuído é limitada a byte[]. Os itens de cache armazenados em cache na memória e distribuídos como pares chave-valor.

System.Runtime.Caching/MemoryCache

System.Runtime.Caching/MemoryCache (Pacote NuGet) pode ser usado com:

  • .NET Standard 2.0 ou posterior.
  • Qualquer implementação do .NET direcionada ao .NET Standard 2.0 ou posterior. Por exemplo, ASP.NET Core 3.1 ou posterior.
  • .NET Framework 4.5 ou posterior.

Microsoft.Extensions.Caching.Memory/IMemoryCache (descrito neste artigo) é recomendadoSystem.Runtime.Caching/MemoryCacheporque é melhor integrado ao ASP.NET Core. Por exemplo, IMemoryCache funciona nativamente com ASP.NET Core injeção de dependência.

Use System.Runtime.Caching/MemoryCache como uma ponte de compatibilidade ao portar código de ASP.NET 4.x para ASP.NET Core.

Diretrizes de uso

  • O código sempre deve ter uma opção de fallback para buscar dados e não depender de um valor armazenado em cache disponível.
  • O cache usa um recurso escasso, memória. Limitar o crescimento do cache:
    • Não use a entrada externa como chaves de cache.
    • Use expirações para limitar o crescimento do cache.
    • Use SetSize, Size e SizeLimit para limitar o tamanho do cache. O runtime do ASP.NET Core não limita o tamanho do cache com base na pressão de memória. Cabe ao desenvolvedor limitar o tamanho do cache.

Usar IMemoryCache

Aviso

Usar um cache de memória compartilhada da Injeção de Dependência e chamar SetSize, Sizeou SizeLimit para limitar o tamanho do cache pode fazer com que o aplicativo falhe. Quando um limite de tamanho é definido em um cache, todas as entradas devem especificar um tamanho ao serem adicionadas. Isso pode levar a problemas, pois os desenvolvedores podem não ter controle total sobre o que usa o cache compartilhado. Ao usar SetSize, Size ou SizeLimit para limitar o cache, crie um singleton de cache para cache. Para obter mais informações e um exemplo, consulte Usar SetSize, Size e SizeLimit para limitar o tamanho do cache. Um cache compartilhado é compartilhado por outras estruturas ou bibliotecas.

O cache na memória é um serviço referenciado de um aplicativo usando a Injeção de Dependência. Solicite a IMemoryCache instância no construtor:

public class HomeController : Controller
{
    private IMemoryCache _cache;

    public HomeController(IMemoryCache memoryCache)
    {
        _cache = memoryCache;
    }

O código a seguir usa TryGetValue para marcar se uma hora estiver no cache. Se uma hora não for armazenada em cache, uma nova entrada será criada e adicionada ao cache com Set. A CacheKeys classe faz parte do exemplo de download.

public static class CacheKeys
{
    public static string Entry => "_Entry";
    public static string CallbackEntry => "_Callback";
    public static string CallbackMessage => "_CallbackMessage";
    public static string Parent => "_Parent";
    public static string Child => "_Child";
    public static string DependentMessage => "_DependentMessage";
    public static string DependentCTS => "_DependentCTS";
    public static string Ticks => "_Ticks";
    public static string CancelMsg => "_CancelMsg";
    public static string CancelTokenSource => "_CancelTokenSource";
}
public IActionResult CacheTryGetValueSet()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Set cache options.
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Save data in cache.
        _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
    }

    return View("Cache", cacheEntry);
}

A hora atual e a hora armazenada em cache são exibidas:

@model DateTime?

<div>
    <h2>Actions</h2>
    <ul>
        <li><a asp-controller="Home" asp-action="CacheTryGetValueSet">TryGetValue and Set</a></li>
        <li><a asp-controller="Home" asp-action="CacheGet">Get</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreate">GetOrCreate</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAsynchronous">CacheGetOrCreateAsynchronous</a></li>
        <li><a asp-controller="Home" asp-action="CacheRemove">Remove</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbs">CacheGetOrCreateAbs</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAbsSliding">CacheGetOrCreateAbsSliding</a></li>

    </ul>
</div>

<h3>Current Time: @DateTime.Now.TimeOfDay.ToString()</h3>
<h3>Cached Time: @(Model == null ? "No cached entry found" : Model.Value.TimeOfDay.ToString())</h3>

O código a seguir usa o Set método de extensão para armazenar dados em cache por um tempo relativo sem criar o objeto MemoryCacheEntryOptions:

public IActionResult SetCacheRelativeExpiration()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Save data in cache and set the relative expiration time to one day
        _cache.Set(CacheKeys.Entry, cacheEntry, TimeSpan.FromDays(1));
    }

    return View("Cache", cacheEntry);
}

O valor armazenado DateTime em cache permanece no cache enquanto há solicitações dentro do período de tempo limite.

O código a seguir usa GetOrCreate e GetOrCreateAsync para armazenar dados em cache.

public IActionResult CacheGetOrCreate()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(3);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

public async Task<IActionResult> CacheGetOrCreateAsynchronous()
{
    var cacheEntry = await
        _cache.GetOrCreateAsync(CacheKeys.Entry, entry =>
        {
            entry.SlidingExpiration = TimeSpan.FromSeconds(3);
            return Task.FromResult(DateTime.Now);
        });

    return View("Cache", cacheEntry);
}

O código a seguir chama Get para buscar o tempo armazenado em cache:

public IActionResult CacheGet()
{
    var cacheEntry = _cache.Get<DateTime?>(CacheKeys.Entry);
    return View("Cache", cacheEntry);
}

O código a seguir obtém ou cria um item armazenado em cache com expiração absoluta:

public IActionResult CacheGetOrCreateAbs()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

Um conjunto de itens armazenados em cache com apenas uma expiração deslizante corre o risco de nunca expirar. Se o item armazenado em cache for acessado repetidamente dentro do intervalo de expiração deslizante, o item nunca expirará. Combine uma expiração deslizante com uma expiração absoluta para garantir que o item expire. A expiração absoluta define um limite superior em quanto tempo o item pode ser armazenado em cache enquanto ainda permite que o item expire anteriormente se ele não for solicitado dentro do intervalo de expiração deslizante. Se o intervalo de expiração deslizante ou o tempo de expiração absoluto passar, o item será removido do cache.

O código a seguir obtém ou cria um item armazenado em cache com expiração deslizante e absoluta:

public IActionResult CacheGetOrCreateAbsSliding()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SetSlidingExpiration(TimeSpan.FromSeconds(3));
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

O código anterior garante que os dados não serão armazenados em cache por mais tempo do que o tempo absoluto.

GetOrCreate, GetOrCreateAsync e Get são métodos de extensão na CacheExtensions classe. Esses métodos estendem a funcionalidade de IMemoryCache.

MemoryCacheEntryOptions

O exemplo a seguir:

  • Define um tempo de expiração deslizante. As solicitações que acessam esse item armazenado em cache redefinirão o relógio de expiração deslizante.
  • Define a prioridade do cache como CacheItemPriority.NeverRemove.
  • Define um PostEvictionDelegate que será chamado depois que a entrada for removida do cache. O retorno de chamada é executado em um thread diferente do código que remove o item do cache.
public IActionResult CreateCallbackEntry()
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        // Pin to cache.
        .SetPriority(CacheItemPriority.NeverRemove)
        // Add eviction callback
        .RegisterPostEvictionCallback(callback: EvictionCallback, state: this);

    _cache.Set(CacheKeys.CallbackEntry, DateTime.Now, cacheEntryOptions);

    return RedirectToAction("GetCallbackEntry");
}

public IActionResult GetCallbackEntry()
{
    return View("Callback", new CallbackViewModel
    {
        CachedTime = _cache.Get<DateTime?>(CacheKeys.CallbackEntry),
        Message = _cache.Get<string>(CacheKeys.CallbackMessage)
    });
}

public IActionResult RemoveCallbackEntry()
{
    _cache.Remove(CacheKeys.CallbackEntry);
    return RedirectToAction("GetCallbackEntry");
}

private static void EvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.CallbackMessage, message);
}

Usar SetSize, Size e SizeLimit para limitar o tamanho do cache

Opcionalmente, uma instância MemoryCache pode especificar e impor um limite de tamanho. O limite de tamanho do cache não tem uma unidade de medida definida porque o cache não tem mecanismo para medir o tamanho das entradas. Se o limite de tamanho do cache estiver definido, todas as entradas deverão especificar o tamanho. O runtime ASP.NET Core não limita o tamanho do cache com base na pressão de memória. Cabe ao desenvolvedor limitar o tamanho do cache. O tamanho especificado está em unidades escolhidas pelo desenvolvedor.

Por exemplo:

  • Se o aplicativo Web estivesse armazenando principalmente cadeias de caracteres em cache, cada tamanho de entrada de cache poderia ser o comprimento da cadeia de caracteres.
  • O aplicativo pode especificar o tamanho de todas as entradas como 1 e o limite de tamanho é a contagem de entradas.

Se SizeLimit não estiver definido, o cache crescerá sem associação. O runtime ASP.NET Core não corta o cache quando a memória do sistema está baixa. Os aplicativos devem ser projetados para:

  • Limitar o crescimento do cache.
  • Chamar Compact ou Remove quando a memória disponível for limitada:

O código a seguir cria um tamanho MemoryCache fixo sem unidade acessível por injeção de dependência:

// using Microsoft.Extensions.Caching.Memory;
public class MyMemoryCache 
{
    public MemoryCache Cache { get; private set; }
    public MyMemoryCache()
    {
        Cache = new MemoryCache(new MemoryCacheOptions
        {
            SizeLimit = 1024
        });
    }
}

SizeLimit não tem unidades. As entradas armazenadas em cache devem especificar o tamanho em todas as unidades que considerarem mais apropriadas se o limite de tamanho do cache tiver sido definido. Todos os usuários de uma instância de cache devem usar o mesmo sistema de unidade. Uma entrada não será armazenada em cache se a soma dos tamanhos de entrada armazenados em cache exceder o valor especificado por SizeLimit. Se nenhum limite de tamanho de cache for definido, o tamanho do cache definido na entrada será ignorado.

O código a seguir MyMemoryCache registra com o contêiner de injeção de dependência.

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddSingleton<MyMemoryCache>();
}

MyMemoryCache é criado como um cache de memória independente para componentes que estão cientes desse tamanho de cache limitado e sabem como definir o tamanho da entrada de cache adequadamente.

O código a seguir usa MyMemoryCache:

public class SetSize : PageModel
{
    private MemoryCache _cache;
    public static readonly string MyKey = "_MyKey";

    public SetSize(MyMemoryCache memoryCache)
    {
        _cache = memoryCache.Cache;
    }

    [TempData]
    public string DateTime_Now { get; set; }

    public IActionResult OnGet()
    {
        if (!_cache.TryGetValue(MyKey, out string cacheEntry))
        {
            // Key not in cache, so get data.
            cacheEntry = DateTime.Now.TimeOfDay.ToString();

            var cacheEntryOptions = new MemoryCacheEntryOptions()
                // Set cache entry size by extension method.
                .SetSize(1)
                // Keep in cache for this time, reset time if accessed.
                .SetSlidingExpiration(TimeSpan.FromSeconds(3));

            // Set cache entry size via property.
            // cacheEntryOptions.Size = 1;

            // Save data in cache.
            _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
        }

        DateTime_Now = cacheEntry;

        return RedirectToPage("./Index");
    }
}

O tamanho da entrada de cache pode ser definido por Size ou pelos métodos de extensão SetSize:

public IActionResult OnGet()
{
    if (!_cache.TryGetValue(MyKey, out string cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now.TimeOfDay.ToString();

        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Set cache entry size by extension method.
            .SetSize(1)
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Set cache entry size via property.
        // cacheEntryOptions.Size = 1;

        // Save data in cache.
        _cache.Set(MyKey, cacheEntry, cacheEntryOptions);
    }

    DateTime_Now = cacheEntry;

    return RedirectToPage("./Index");
}

MemoryCache.Compact

MemoryCache.Compact tenta remover o percentual especificado do cache na seguinte ordem:

  • Todos os itens expirados.
  • Itens por prioridade. Os itens de prioridade mais baixos são removidos primeiro.
  • Objetos menos usados recentemente.
  • Itens com a expiração absoluta mais antiga.
  • Itens com a expiração deslizante mais antiga.

Itens fixados com prioridade NeverRemove nunca são removidos. O código a seguir remove um item de cache e chama Compact:

_cache.Remove(MyKey);

// Remove 33% of cached items.
_cache.Compact(.33);   
cache_size = _cache.Count;

Para obter mais informações, consulte a Origem compacta no GitHub.

Armazenar dependências em cache

O exemplo a seguir mostra como expirar uma entrada de cache se uma entrada dependente expirar. Um CancellationChangeToken é adicionado ao item armazenado em cache. Quando Cancel é chamado no CancellationTokenSource, ambas as entradas de cache são removidas.

public IActionResult CreateDependentEntries()
{
    var cts = new CancellationTokenSource();
    _cache.Set(CacheKeys.DependentCTS, cts);

    using (var entry = _cache.CreateEntry(CacheKeys.Parent))
    {
        // expire this entry if the dependant entry expires.
        entry.Value = DateTime.Now;
        entry.RegisterPostEvictionCallback(DependentEvictionCallback, this);

        _cache.Set(CacheKeys.Child,
            DateTime.Now,
            new CancellationChangeToken(cts.Token));
    }

    return RedirectToAction("GetDependentEntries");
}

public IActionResult GetDependentEntries()
{
    return View("Dependent", new DependentViewModel
    {
        ParentCachedTime = _cache.Get<DateTime?>(CacheKeys.Parent),
        ChildCachedTime = _cache.Get<DateTime?>(CacheKeys.Child),
        Message = _cache.Get<string>(CacheKeys.DependentMessage)
    });
}

public IActionResult RemoveChildEntry()
{
    _cache.Get<CancellationTokenSource>(CacheKeys.DependentCTS).Cancel();
    return RedirectToAction("GetDependentEntries");
}

private static void DependentEvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Parent entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.DependentMessage, message);
}

O uso de um CancellationTokenSource permite que várias entradas de cache sejam removidas como um grupo. Com o using padrão no código acima, as entradas de cache criadas dentro do using bloco herdarão gatilhos e configurações de expiração.

Observações adicionais

  • A expiração não acontece em segundo plano. Não há temporizador que verifique ativamente o cache em busca de itens expirados. Qualquer atividade no cache (Get, Set, Remove) pode disparar uma verificação em segundo plano para itens expirados. Um temporizador no CancellationTokenSource (CancelAfter) também remove a entrada e dispara uma verificação de itens expirados. O exemplo a seguir usa CancellationTokenSource(TimeSpan) para o token registrado. Quando esse token é acionado, ele remove a entrada imediatamente e dispara os retornos de chamada de remoção:

    public IActionResult CacheAutoExpiringTryGetValueSet()
    {
        DateTime cacheEntry;
    
        if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
        {
            cacheEntry = DateTime.Now;
    
            var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
    
            var cacheEntryOptions = new MemoryCacheEntryOptions()
                .AddExpirationToken(new CancellationChangeToken(cts.Token));
    
            _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
        }
    
        return View("Cache", cacheEntry);
    }
    
  • Ao usar um retorno de chamada para repovoar um item de cache:

    • Várias solicitações podem localizar o valor da chave armazenada em cache vazio porque o retorno de chamada não foi concluído.
    • Isso pode resultar em vários threads repovoando o item armazenado em cache.
  • Quando uma entrada de cache é usada para criar outra, o filho copia os tokens de expiração da entrada pai e as configurações de expiração baseadas em tempo. O filho não expira por remoção manual ou atualização da entrada pai.

  • Use PostEvictionCallbacks para definir as callbacks que serão disparadas depois que a entrada do cache for removida do cache. No código de exemplo, CancellationTokenSource.Dispose() é chamado para liberar os recursos não gerenciados usados pelo CancellationTokenSource. No entanto, o CancellationTokenSource não é descartado imediatamente porque ainda está sendo usado pela entrada de cache. O CancellationToken é passado para MemoryCacheEntryOptions a fim de criar uma entrada de cache que expira após um determinado tempo. Portanto, Dispose não deve ser chamado até que a entrada de cache seja removida ou expirada. O código de exemplo chama o método RegisterPostEvictionCallback para registrar um retorno de chamada que será invocado quando a entrada de cache for removida e descarta o CancellationTokenSource nesse retorno de chamada.

  • Para a maioria dos aplicativos, IMemoryCache está habilitado. Por exemplo, chamar AddMvc, AddControllersWithViews, AddRazorPages, AddMvcCore().AddRazorViewEngine e muitos outros métodos Add{Service} em ConfigureServices, habilita IMemoryCache. Para aplicativos que não estão chamando um dos métodos anteriores Add{Service}, pode ser necessário chamar AddMemoryCache em ConfigureServices.

Atualização de cache em segundo plano

Use um serviço em segundo plano, como IHostedService para atualizar o cache. O serviço em segundo plano pode recomputar as entradas e atribuí-las ao cache somente quando estiverem prontas.

Recursos adicionais