Bagikan melalui


Cache di .NET

Dalam artikel ini, Anda akan mempelajari tentang berbagai mekanisme cache. Penyimpanan cache adalah tindakan menyimpan data dalam lapisan menengah, membuat pengambilan data berikutnya lebih cepat. Secara konseptual, caching adalah strategi optimasi performa dan pertimbangan desain. Cache dapat secara signifikan meningkatkan performa aplikasi dengan membuat data yang jarang berubah (atau mahal untuk diambil) lebih mudah diakses. Artikel ini memperkenalkan dua tipe caching utama, dan menyediakan kode sumber contoh untuk keduanya.

Penting

Ada dua MemoryCache class dalam .NET, satu di namespace System.Runtime.Caching dan yang lain di namespace Microsoft.Extensions.Caching:

Meskipun artikel ini berfokus pada penembolokan, artikel ini tidak menyertakan System.Runtime.Caching paket NuGet. Semua referensi ke MemoryCache berada dalam ruang nama Microsoft.Extensions.Caching.

Semua paket Microsoft.Extensions.* siap untuk injeksi dependensi (DI), dan antarmuka IMemoryCache serta IDistributedCache dapat digunakan sebagai layanan.

Penembolokan memori

Di bagian ini, Anda akan mempelajari tentang paket Microsoft.Extensions.Caching.Memory . Implementasi IMemoryCache saat ini adalah pembungkus yang membungkus ConcurrentDictionary<TKey,TValue>, mengekspos API yang kaya fitur. Entri dalam cache diwakili oleh ICacheEntry, dan dapat berupa .object Solusi cache dalam memori sangat bagus untuk aplikasi yang berjalan di satu server, di mana semua data yang di-cache menggunakan memori dalam proses aplikasi.

Petunjuk / Saran

Untuk skenario pencache-an multi-server, pertimbangkan pendekatan Pencache-an Terdistribusi sebagai alternatif untuk pencache-an dalam memori.

API cache dalam memori

Konsumen cache memiliki kontrol atas kedaluwarsa geser dan absolut:

Mengatur waktu kedaluwarsa akan menyebabkan entri dalam cache dikeluarkan jika tidak diakses dalam batas waktu kedaluwarsa yang ditentukan. Konsumen memiliki opsi tambahan untuk mengontrol entri cache, melalui MemoryCacheEntryOptions. Masing-masing ICacheEntry dipasangkan dengan MemoryCacheEntryOptions yang mengekspos fungsionalitas pengeluaran kedaluwarsa dengan IChangeToken, pengaturan prioritas dengan CacheItemPriority, dan mengontrol ICacheEntry.Size. Pertimbangkan metode ekstensi berikut:

Contoh cache dalam memori

Untuk menggunakan implementasi default IMemoryCache , panggil AddMemoryCache metode ekstensi untuk mendaftarkan semua layanan yang diperlukan dengan DI. Dalam sampel kode berikut, host generik digunakan untuk mengekspos fungsionalitas DI:

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
using IHost host = builder.Build();

Bergantung pada beban kerja .NET Anda, Anda mungkin mengakses IMemoryCache secara berbeda, misalnya injeksi konstruktor. Dalam sampel ini, Anda menggunakan IServiceProvider instans pada host dan memanggil metode ekstensi generik GetRequiredService<T>(IServiceProvider) :

IMemoryCache cache =
    host.Services.GetRequiredService<IMemoryCache>();

Dengan layanan penembolokan dalam memori terdaftar, dan diselesaikan melalui DI, Anda siap untuk memulai penembolokan. Sampel ini berulang melalui huruf dalam alfabet bahasa Inggris 'A' hingga 'Z'. record AlphabetLetter Tipe menyimpan referensi ke surat, dan menghasilkan pesan.

file record AlphabetLetter(char Letter)
{
    internal string Message =>
        $"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}

Petunjuk / Saran

Pengubah file akses digunakan pada tipe AlphabetLetter, karena didefinisikan di dalam dan hanya dapat diakses dari file Program.cs. Untuk informasi selengkapnya, lihat file (Referensi C#). Untuk melihat kode sumber lengkap, lihat bagian Program.cs .

Sampel mencakup fungsi pembantu yang melakukan iterasi melalui huruf alfabet:

static async ValueTask IterateAlphabetAsync(
    Func<char, Task> asyncFunc)
{
    for (char letter = 'A'; letter <= 'Z'; ++letter)
    {
        await asyncFunc(letter);
    }

    Console.WriteLine();
}

Dalam kode C# sebelumnya:

  • Func<char, Task> asyncFunc ditunggu pada setiap iterasi, dengan melewatkan letter saat ini.
  • Setelah semua huruf diproses, baris kosong ditulis ke konsol.

Untuk menambahkan item ke dalam cache, panggil salah satu API di antara Create atau Set.

var addLettersToCacheTask = IterateAlphabetAsync(letter =>
{
    MemoryCacheEntryOptions options = new()
    {
        AbsoluteExpirationRelativeToNow =
            TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
    };

    _ = options.RegisterPostEvictionCallback(OnPostEviction);

    AlphabetLetter alphabetLetter =
        cache.Set(
            letter, new AlphabetLetter(letter), options);

    Console.WriteLine($"{alphabetLetter.Letter} was cached.");

    return Task.Delay(
        TimeSpan.FromMilliseconds(MillisecondsDelayAfterAdd));
});
await addLettersToCacheTask;

Dalam kode C# sebelumnya:

  • Variabel addLettersToCacheTask mendelegasikan tugas ke IterateAlphabetAsync dan kemudian menunggu.
  • Func<char, Task> asyncFunc diberi argumen lambda.
  • MemoryCacheEntryOptions diinisialisasi dengan kedaluwarsa absolut yang dihitung dari sekarang.
  • Panggilan balik pasca-pengeluaran didaftarkan.
  • Objek AlphabetLetter diinstansiasi, dan diteruskan ke Set beserta letter dan options.
  • Pesan ditulis ke konsol dan disimpan dalam cache.
  • Akhirnya, Task.Delay dikembalikan.

Untuk setiap huruf dalam alfabet, entri cache ditulis dengan kedaluwarsa, dan panggilan balik pasca pengeluaran.

Panggilan balik setelah pengeluaran menuliskan rincian nilai yang dikeluarkan ke konsol.

static void OnPostEviction(
    object key, object? letter, EvictionReason reason, object? state)
{
    if (letter is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
    }
};

Sekarang setelah cache diisi, panggilan lain ke IterateAlphabetAsync ditunggu, tetapi kali ini Anda akan memanggil IMemoryCache.TryGetValue:

var readLettersFromCacheTask = IterateAlphabetAsync(letter =>
{
    if (cache.TryGetValue(letter, out object? value) &&
        value is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{letter} is still in cache. {alphabetLetter.Message}");
    }

    return Task.CompletedTask;
});
await readLettersFromCacheTask;

Jika cache berisi kunci letter, dan value adalah instans dari AlphabetLetter, maka value tersebut ditulis ke konsol. letter Ketika kunci tidak ada di cache, kunci tersebut dikeluarkan dan panggilan balik pasca pengeluarannya dipanggil.

Metode ekstensi tambahan

Hadir IMemoryCache dengan banyak metode ekstensi berbasis kenyamanan, termasuk asinkron GetOrCreateAsync:

Satukan semuanya

Seluruh kode sumber aplikasi sampel adalah program tingkat atas dan memerlukan dua paket NuGet:

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
using IHost host = builder.Build();

IMemoryCache cache =
    host.Services.GetRequiredService<IMemoryCache>();

const int MillisecondsDelayAfterAdd = 50;
const int MillisecondsAbsoluteExpiration = 750;

static void OnPostEviction(
    object key, object? letter, EvictionReason reason, object? state)
{
    if (letter is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
    }
};

static async ValueTask IterateAlphabetAsync(
    Func<char, Task> asyncFunc)
{
    for (char letter = 'A'; letter <= 'Z'; ++letter)
    {
        await asyncFunc(letter);
    }

    Console.WriteLine();
}

var addLettersToCacheTask = IterateAlphabetAsync(letter =>
{
    MemoryCacheEntryOptions options = new()
    {
        AbsoluteExpirationRelativeToNow =
            TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
    };

    _ = options.RegisterPostEvictionCallback(OnPostEviction);

    AlphabetLetter alphabetLetter =
        cache.Set(
            letter, new AlphabetLetter(letter), options);

    Console.WriteLine($"{alphabetLetter.Letter} was cached.");

    return Task.Delay(
        TimeSpan.FromMilliseconds(MillisecondsDelayAfterAdd));
});
await addLettersToCacheTask;

var readLettersFromCacheTask = IterateAlphabetAsync(letter =>
{
    if (cache.TryGetValue(letter, out object? value) &&
        value is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{letter} is still in cache. {alphabetLetter.Message}");
    }

    return Task.CompletedTask;
});
await readLettersFromCacheTask;

await host.RunAsync();

file record AlphabetLetter(char Letter)
{
    internal string Message =>
        $"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}

Jangan ragu untuk menyesuaikan MillisecondsDelayAfterAdd dan MillisecondsAbsoluteExpiration nilai untuk mengamati perubahan perilaku pada kedaluwarsa dan penghapusan entri yang di-cache. Berikut ini adalah contoh output dari menjalankan kode ini. Karena sifat non-deterministik peristiwa .NET, output Anda mungkin berbeda.

A was cached.
B was cached.
C was cached.
D was cached.
E was cached.
F was cached.
G was cached.
H was cached.
I was cached.
J was cached.
K was cached.
L was cached.
M was cached.
N was cached.
O was cached.
P was cached.
Q was cached.
R was cached.
S was cached.
T was cached.
U was cached.
V was cached.
W was cached.
X was cached.
Y was cached.
Z was cached.

A was evicted for Expired.
C was evicted for Expired.
B was evicted for Expired.
E was evicted for Expired.
D was evicted for Expired.
F was evicted for Expired.
H was evicted for Expired.
K was evicted for Expired.
L was evicted for Expired.
J was evicted for Expired.
G was evicted for Expired.
M was evicted for Expired.
N was evicted for Expired.
I was evicted for Expired.
P was evicted for Expired.
R was evicted for Expired.
O was evicted for Expired.
Q was evicted for Expired.
S is still in cache. The 'S' character is the 19 letter in the English alphabet.
T is still in cache. The 'T' character is the 20 letter in the English alphabet.
U is still in cache. The 'U' character is the 21 letter in the English alphabet.
V is still in cache. The 'V' character is the 22 letter in the English alphabet.
W is still in cache. The 'W' character is the 23 letter in the English alphabet.
X is still in cache. The 'X' character is the 24 letter in the English alphabet.
Y is still in cache. The 'Y' character is the 25 letter in the English alphabet.
Z is still in cache. The 'Z' character is the 26 letter in the English alphabet.

Karena kedaluwarsa absolut (MemoryCacheEntryOptions.AbsoluteExpirationRelativeToNow) diatur, semua item yang di-cache pada akhirnya akan dikeluarkan.

Caching Layanan Pekerja

Salah satu strategi umum untuk penyimpanan sementara data adalah memperbarui cache secara independen dari layanan data yang menggunakan. Templat Layanan Pekerja adalah contoh yang bagus, karena BackgroundService berjalan independen (atau di latar belakang) dari kode aplikasi lainnya. Ketika aplikasi mulai berjalan yang menghosting implementasi IHostedService, implementasi yang sesuai (dalam hal BackgroundService ini atau "pekerja") mulai berjalan dalam proses yang sama. Layanan yang dihosting ini terdaftar di DI sebagai singleton, melalui metode perpanjangan AddHostedService<THostedService>(IServiceCollection). Layanan lain dapat didaftarkan dengan DI dengan masa pakai layanan apa pun.

Penting

Masa pakai layanan sangat penting untuk dipahami. Ketika Anda memanggil AddMemoryCache untuk mendaftarkan semua layanan penyimpanan dalam memori, layanan-layanan tersebut terdaftar sebagai entitas tunggal (singleton).

Skenario layanan foto

Bayangkan Anda mengembangkan layanan foto yang bergantung pada API pihak ketiga yang dapat diakses melalui HTTP. Data foto ini tidak terlalu sering berubah, tetapi ada banyak sekali. Setiap foto diwakili oleh simbol sederhana: record

namespace CachingExamples.Memory;

public readonly record struct Photo(
    int AlbumId,
    int Id,
    string Title,
    string Url,
    string ThumbnailUrl);

Dalam contoh berikut, Anda akan melihat beberapa layanan yang terdaftar di DI. Setiap layanan memiliki satu tanggung jawab.

using CachingExamples.Memory;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<CacheWorker>();
builder.Services.AddHostedService<CacheWorker>();
builder.Services.AddScoped<PhotoService>();
builder.Services.AddSingleton(typeof(CacheSignal<>));

using IHost host = builder.Build();

await host.StartAsync();

Dalam kode C# sebelumnya:

PhotoService bertanggung jawab untuk mendapatkan foto yang cocok dengan kriteria yang diberikan (atau filter):

using Microsoft.Extensions.Caching.Memory;

namespace CachingExamples.Memory;

public sealed class PhotoService(
        IMemoryCache cache,
        CacheSignal<Photo> cacheSignal,
        ILogger<PhotoService> logger)
{
    public async IAsyncEnumerable<Photo> GetPhotosAsync(Func<Photo, bool>? filter = default)
    {
        try
        {
            await cacheSignal.WaitAsync();

            Photo[] photos =
                (await cache.GetOrCreateAsync(
                    "Photos", _ =>
                    {
                        logger.LogWarning("This should never happen!");

                        return Task.FromResult(Array.Empty<Photo>());
                    }))!;

            // If no filter is provided, use a pass-thru.
            filter ??= _ => true;

            foreach (Photo photo in photos)
            {
                if (!default(Photo).Equals(photo) && filter(photo))
                {
                    yield return photo;
                }
            }
        }
        finally
        {
            cacheSignal.Release();
        }
    }
}

Dalam kode C# sebelumnya:

  • Konstruktor memerlukan IMemoryCache, , CacheSignal<Photo>dan ILogger.
  • Metode tersebut:GetPhotosAsync
    • Mendefinisikan parameter Func<Photo, bool> filter, dan mengembalikan IAsyncEnumerable<Photo>.
    • Memanggil dan menunggu _cacheSignal.WaitAsync() untuk dirilis, ini memastikan bahwa cache diisi sebelum mengakses cache.
    • Secara asinkron memanggil _cache.GetOrCreateAsync(), mengambil semua foto di cache.
    • Argumen factory mencatat peringatan, dan mengembalikan array foto kosong - ini seharusnya tidak pernah terjadi.
    • Setiap foto dalam cache diulang, difilter, dan diwujudkan dengan yield return.
    • Akhirnya, sinyal cache diatur ulang.

Pengguna layanan ini bebas untuk memanggil metode GetPhotosAsync, dan menangani foto secara sesuai. Tidak HttpClient diperlukan karena cache berisi foto.

Sinyal asinkron didasarkan pada instans SemaphoreSlim yang dienkapsulasi, dalam singleton dengan tipe generik yang dibatasi. CacheSignal<T> bergantung pada sebuah instance dari SemaphoreSlim.

namespace CachingExamples.Memory;

public sealed class CacheSignal<T>
{
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    /// <summary>
    /// Exposes a <see cref="Task"/> that represents the asynchronous wait operation.
    /// When signaled (consumer calls <see cref="Release"/>), the 
    /// <see cref="Task.Status"/> is set as <see cref="TaskStatus.RanToCompletion"/>.
    /// </summary>
    public Task WaitAsync() => _semaphore.WaitAsync();

    /// <summary>
    /// Exposes the ability to signal the release of the <see cref="WaitAsync"/>'s operation.
    /// Callers who were waiting, will be able to continue.
    /// </summary>
    public void Release() => _semaphore.Release();
}

Dalam kode C# sebelumnya, pola dekorator digunakan untuk membungkus instans dari SemaphoreSlim. Karena CacheSignal<T> terdaftar sebagai singleton, CacheSignal<T> dapat digunakan di seluruh masa hidup layanan dengan tipe generik apa pun—dalam hal ini, . Ini bertanggung jawab untuk menandakan pengisian awal cache.

CacheWorker adalah subkelas dari BackgroundService:

using System.Net.Http.Json;
using Microsoft.Extensions.Caching.Memory;

namespace CachingExamples.Memory;

public sealed class CacheWorker(
    ILogger<CacheWorker> logger,
    HttpClient httpClient,
    CacheSignal<Photo> cacheSignal,
    IMemoryCache cache) : BackgroundService
{
    private readonly TimeSpan _updateInterval = TimeSpan.FromHours(3);

    private bool _isCacheInitialized = false;

    private const string Url = "https://jsonplaceholder.typicode.com/photos";

    public override async Task StartAsync(CancellationToken cancellationToken)
    {
        await cacheSignal.WaitAsync();
        await base.StartAsync(cancellationToken);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            logger.LogInformation("Updating cache.");

            try
            {
                Photo[]? photos =
                    await httpClient.GetFromJsonAsync<Photo[]>(
                        Url, stoppingToken);

                if (photos is { Length: > 0 })
                {
                    cache.Set("Photos", photos);
                    logger.LogInformation(
                        "Cache updated with {Count:#,#} photos.", photos.Length);
                }
                else
                {
                    logger.LogWarning(
                        "Unable to fetch photos to update cache.");
                }
            }
            finally
            {
                if (!_isCacheInitialized)
                {
                    cacheSignal.Release();
                    _isCacheInitialized = true;
                }
            }

            try
            {
                logger.LogInformation(
                    "Will attempt to update the cache in {Hours} hours from now.",
                    _updateInterval.Hours);

                await Task.Delay(_updateInterval, stoppingToken);
            }
            catch (OperationCanceledException)
            {
                logger.LogWarning("Cancellation acknowledged: shutting down.");
                break;
            }
        }
    }
}

Dalam kode C# sebelumnya:

  • Konstruktor memerlukan ILogger, , HttpClientdan IMemoryCache.
  • _updateInterval didefinisikan selama tiga jam.
  • Metode tersebut:ExecuteAsync
    • Perulangan ketika aplikasi sedang berjalan.
    • Membuat permintaan HTTP ke "https://jsonplaceholder.typicode.com/photos", dan memetakan respons sebagai array Photo objek.
    • Array foto ditempatkan di dalam IMemoryCache dengan kunci "Photos".
    • _cacheSignal.Release() berfungsi untuk melepaskan setiap konsumen yang menunggu sinyal.
    • Panggilan ke Task.Delay ditunggu, mengingat interval pembaruan.
    • Setelah tertunda selama tiga jam, cache kembali diperbarui.

Pengguna dalam proses yang berlangsung dapat meminta foto-foto dari IMemoryCache, tetapi CacheWorker bertanggung jawab untuk memperbarui cache.

Cache terdistribusi

Dalam beberapa skenario, cache terdistribusi diperlukan—demikian halnya dengan beberapa server aplikasi. Cache terdistribusi mendukung skala yang lebih besar daripada pendekatan cache dalam memori. Menggunakan cache terdistribusi memindahkan memori cache ke proses eksternal atau server terpisah, tetapi memerlukan input/output jaringan tambahan dan menambah sedikit latensi, meskipun nominal.

Abstraksi penembolokan terdistribusi termasuk dalam paket Microsoft.Extensions.Caching.Memory NuGet, dan bahkan AddDistributedMemoryCache terdapat metode ekstensi.

Perhatian

AddDistributedMemoryCache hanya digunakan dalam skenario pengembangan dan/atau pengujian, dan bukan implementasi yang cocok untuk produksi.

Pertimbangkan salah satu implementasi yang IDistributedCache tersedia dari paket berikut:

API pencache-an terdistribusi

API caching terdistribusi sedikit lebih primitif dibandingkan dengan API caching dalam memori. Pasangan kunci-nilai sedikit lebih mendasar. Kunci cache dalam memori didasarkan pada object, sedangkan kunci yang didistribusikan adalah string. Dengan cache dalam memori, nilainya dapat berupa generik bertipe kuat, sedangkan nilai dalam cache terdistribusi dipertahankan sebagai byte[]. Bukan berarti bahwa berbagai implementasi tidak mengekspos nilai generik berjenis tipe, tetapi itu adalah rincian implementasi.

Membuat nilai

Untuk membuat nilai dalam cache terdistribusi, panggil salah satu API yang ditetapkan:

AlphabetLetter Dengan menggunakan rekaman dari contoh cache dalam memori, Anda dapat membuat serialisasi objek ke JSON lalu mengodekan string sebagai byte[]:

DistributedCacheEntryOptions options = new()
{
    AbsoluteExpirationRelativeToNow =
        TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
};

AlphabetLetter alphabetLetter = new(letter);
string json = JsonSerializer.Serialize(alphabetLetter);
byte[] bytes = Encoding.UTF8.GetBytes(json);

await cache.SetAsync(letter.ToString(), bytes, options);

Sama seperti penyimpanan sementara dalam memori, entri cache dapat memiliki opsi untuk menyempurnakan eksistensinya di cache—dalam hal ini, DistributedCacheEntryOptions.

Membuat metode ekstensi

Ada beberapa metode ekstensi berbasis kenyamanan untuk membuat nilai, yang membantu menghindari string pengodean representasi objek menjadi byte[]:

Membaca nilai

Untuk membaca nilai dari cache terdistribusi, panggil salah satu API get:

AlphabetLetter? alphabetLetter = null;
byte[]? bytes = await cache.GetAsync(letter.ToString());
if (bytes is { Length: > 0 })
{
    string json = Encoding.UTF8.GetString(bytes);
    alphabetLetter = JsonSerializer.Deserialize<AlphabetLetter>(json);
}

Setelah entri cache dibaca dari cache, Anda bisa mendapatkan representasi yang dikodekan string UTF8 dari byte[]

Memahami metode ekstensi

Ada beberapa metode ekstensi berbasis kenyamanan untuk membaca nilai, yang membantu menghindari decoding byte[] menjadi string representasi objek:

Memperbarui nilai

Tidak ada cara untuk memperbarui nilai dalam cache terdistribusi dengan satu panggilan API, sebagai gantinya, nilai dapat mengatur ulang kedaluwarsa geser mereka dengan salah satu API refresh:

Jika nilai aktual perlu diperbarui, Anda harus menghapus nilai lalu menambahkannya kembali.

Hapus nilai

Untuk menghapus nilai dalam cache terdistribusi, panggil salah satu API hapus:

Petunjuk / Saran

Meskipun ada versi sinkron dari API yang disebutkan di atas, harap pertimbangkan fakta bahwa implementasi cache terdistribusi bergantung pada I/O jaringan. Untuk alasan ini, API asinkron lebih sering dipilih.

Lihat juga