Bagikan melalui


Penembolokan di .NET

Dalam artikel ini, Anda akan mempelajari tentang berbagai mekanisme penembolokan. Penembolokan adalah tindakan menyimpan data dalam lapisan menengah, membuat pengambilan data berikutnya lebih cepat. Secara konseptual, penembolokan adalah strategi pengoptimalan performa dan pertimbangan desain. Penembolokan dapat secara signifikan meningkatkan performa aplikasi dengan membuat data yang jarang berubah (atau mahal untuk diambil) lebih tersedia. Artikel ini memperkenalkan dua jenis penembolokan utama, dan menyediakan kode sumber sampel untuk keduanya:

Penting

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

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

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

Penembolokan dalam memori

Di bagian ini, Anda akan mempelajari tentang paket Microsoft.Extensions.Caching.Memory . Implementasi IMemoryCache saat ini adalah pembungkus di ConcurrentDictionary<TKey,TValue>sekitar , mengekspos API 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 menyewa memori dalam proses aplikasi.

Tip

Untuk skenario penembolokan multi-server, pertimbangkan pendekatan Penembolokan terdistribusi sebagai alternatif untuk penembolokan dalam memori.

API penembolokan dalam memori

Konsumen cache memiliki kontrol atas kedaluwarsa geser dan absolut:

Mengatur kedaluwarsa akan menyebabkan entri dalam cache dikeluarkan jika tidak diakses dalam penjatahan waktu kedaluwarsa. 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 mungkin mengakses IMemoryCache secara berbeda, seperti 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 Jenis menyimpan referensi ke huruf, dan menghasilkan pesan.

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

Tip

Pengubah file akses digunakan pada AlphabetLetter jenis , karena ditentukan dalam dan hanya 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, melewati saat ini letter.
  • Setelah semua huruf diproses, baris kosong ditulis ke konsol.

Untuk menambahkan item ke panggilan cache salah Createsatu , atau Set API:

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 ke IterateAlphabetAsync dan ditunggu.
  • Dibantah Func<char, Task> asyncFunc dengan lambda.
  • MemoryCacheEntryOptions instans dengan kedaluwarsa absolut relatif terhadap sekarang.
  • Panggilan balik pasca-pengeluaran didaftarkan.
  • Objek AlphabetLetter dibuat instans, dan diteruskan bersama Set dengan letter dan options.
  • Huruf ditulis ke konsol sebagai di-cache.
  • Akhirnya, dikembalikan Task.Delay .

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

Panggilan balik pengeluaran pasca menulis detail 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;

cache Jika berisi letter kunci, dan value adalah instans dari AlphabetLetter kunci 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 nilai dan MillisecondsAbsoluteExpiration untuk mengamati perubahan perilaku pada kedaluwarsa dan pengeluaran 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.

Penembolokan Layanan Pekerja

Salah satu strategi umum untuk penembolokan data, adalah memperbarui cache secara independen dari layanan data yang mengonsumsi. 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 AddHostedService<THostedService>(IServiceCollection) metode ekstensi. 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 penembolokan dalam memori, layanan terdaftar sebagai 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 :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 tanggung jawab tunggal.

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:GetPhotosAsync
    • Func<Photo, bool> filter Menentukan parameter, dan mengembalikan IAsyncEnumerable<Photo>.
    • Memanggil dan menunggu _cacheSignal.WaitAsync() rilis, ini memastikan bahwa cache diisi sebelum mengakses cache.
    • _cache.GetOrCreateAsync()Memanggil , secara asinkron mendapatkan 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.

Konsumen layanan ini gratis untuk memanggil GetPhotosAsync metode, dan menangani foto yang sesuai. Tidak HttpClient diperlukan karena cache berisi foto.

Sinyal asinkron didasarkan pada instans yang dienkapsulasi SemaphoreSlim , dalam singleton yang dibatasi jenis generik. Bergantung CacheSignal<T> pada instans :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 .SemaphoreSlim CacheSignal<T> Karena terdaftar sebagai singleton, ini dapat digunakan di semua masa pakai layanan dengan jenis generik apa pun—dalam hal ini, Photo. Ini bertanggung jawab untuk menandakan penyemaian 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.
  • didefinisikan _updateInterval selama tiga jam.
  • Metode:ExecuteAsync
    • Perulangan saat aplikasi sedang berjalan.
    • Membuat permintaan HTTP ke "https://jsonplaceholder.typicode.com/photos", dan memetakan respons sebagai array Photo objek.
    • Array foto ditempatkan di bawah IMemoryCache"Photos" kunci.
    • Disebut _cacheSignal.Release() , merilis setiap konsumen yang menunggu sinyal.
    • Panggilan ke Task.Delay ditunggu, mengingat interval pembaruan.
    • Setelah tertunda selama tiga jam, cache kembali diperbarui.

Konsumen dalam proses yang sama dapat meminta IMemoryCache foto, tetapi CacheWorker bertanggung jawab untuk memperbarui cache.

Penembolokan terdistribusi

Dalam beberapa skenario, cache terdistribusi diperlukan—demikian halnya dengan beberapa server aplikasi. Cache terdistribusi mendukung peluasan skala yang lebih tinggi daripada pendekatan penembolokan dalam memori. Menggunakan cache terdistribusi membongkar memori cache ke proses eksternal, tetapi memerlukan I/O jaringan ekstra dan memperkenalkan sedikit lebih banyak latensi (bahkan jika nominal).

Abstraksi penembolokan terdistribusi adalah bagian Microsoft.Extensions.Caching.Memory dari paket NuGet, dan bahkan AddDistributedMemoryCache ada metode ekstensi.

Perhatian

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

Pertimbangkan salah satu implementasi yang IDistributedCache tersedia dari paket berikut:

API penembolokan terdistribusi

API penembolokan terdistribusi sedikit lebih primitif daripada rekan API penembolokan dalam memori mereka. Pasangan kunci-nilai sedikit lebih mendasar. Kunci penembolokan dalam memori didasarkan pada object, sedangkan kunci terdistribusi adalah string. Dengan penembolokan dalam memori, nilainya dapat berupa generik yang ditik dengan kuat, sedangkan nilai dalam penembolokan terdistribusi dipertahankan sebagai byte[]. Itu bukan berarti bahwa berbagai implementasi tidak mengekspos nilai generik yang ditik dengan kuat tetapi itu akan menjadi detail 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 penembolokan dalam memori, entri cache dapat memiliki opsi untuk membantu menyempurnakan keberadaannya 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 dapatkan API:

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[]

Membaca 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:

Tip

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, lebih disukai daripada tidak menggunakan API asinkron.

Lihat juga