トレーニング
モジュール
.NET Aspire プロジェクトでキャッシュを使用してパフォーマンスを向上させる - Training
このモジュールでは、.NET Aspire クラウドネイティブ アプリ内のキャッシュと、それらを使用してマイクロサービスのパフォーマンスを最適化する方法について学習します。
このブラウザーはサポートされなくなりました。
Microsoft Edge にアップグレードすると、最新の機能、セキュリティ更新プログラム、およびテクニカル サポートを利用できます。
この記事では、さまざまなキャッシュ メカニズムについて説明します。 キャッシュとは、中間層にデータを格納することです。これにより、後続のデータ取得が高速になります。 概念的には、キャッシュはパフォーマンスの最適化戦略と設計上の考慮事項です。 キャッシュを使用すると、変更頻度が低い (または取得に負荷がかかる) データがすぐに使えるようになるため、アプリのパフォーマンスを大幅に向上させることができます。 この記事では、2 つの主要なキャッシュの種類について説明し、両方のサンプル ソース コードを示します。
重要
.NET 内には 2 つの MemoryCache
クラスがあります。1 つは System.Runtime.Caching
名前空間に、もう 1 つは Microsoft.Extensions.Caching
名前空間にあります。
この記事ではキャッシュに焦点を当てていますが、System.Runtime.Caching
NuGet パッケージは含まれていません。 MemoryCache
へのすべての参照は、Microsoft.Extensions.Caching
名前空間内にあります。
すべての Microsoft.Extensions.*
パッケージは依存関係の挿入 (DI) の準備が整っており、IMemoryCache と IDistributedCache の両方のインターフェイスをサービスとして使用できます。
このセクションでは、Microsoft.Extensions.Caching.Memory パッケージについて学習します。 IMemoryCache の現在の実装は、ConcurrentDictionary<TKey,TValue> のラッパーであり、機能豊富な API を公開しています。 キャッシュ内のエントリは、ICacheEntry によって表され、どの object
も指定できます。 メモリ内キャッシュ ソリューションは、1 台のサーバーで実行されるアプリに適しています。ここでは、すべてのキャッシュされたデータがアプリのプロセス内のメモリをレンタルします。
ヒント
マルチサーバー キャッシュのシナリオでは、メモリ内キャッシュの代わりに分散キャッシュによる方法を検討してください。
キャッシュのコンシューマーは、スライド式と絶対の両方の有効期限を制御できます。
有効期限を設定すると、有効期限内にアクセスされなかったキャッシュ内のエントリは "削除" されます。 コンシューマーには、MemoryCacheEntryOptions を使用してキャッシュ エントリを制御するための追加のオプションがあります。 それぞれの ICacheEntry が MemoryCacheEntryOptions とペアになっています。これにより IChangeToken で有効期限削除機能が、CacheItemPriority で優先順位設定が、ICacheEntry.Size で制御が公開されます。 次の拡張メソッドについて考えてみましょう。
既定の IMemoryCache 実装を使用するには、AddMemoryCache 拡張メソッドを呼び出して必要なすべてのサービスを DI に登録します。 次のコード サンプルでは、汎用ホストを使用して 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();
.NET ワークロードによっては、コンストラクターの挿入などで、異なる方法で IMemoryCache
にアクセスする場合があります。 このサンプルでは、host
の IServiceProvider
インスタンスを使用し、汎用の GetRequiredService<T>(IServiceProvider) 拡張メソッドを呼び出します。
IMemoryCache cache =
host.Services.GetRequiredService<IMemoryCache>();
メモリ内キャッシュ サービスが登録され、DI によって解決されたら、キャッシュを開始する準備ができています。 このサンプルでは、英語のアルファベットの 'A' から 'Z' の文字を反復処理します。 record AlphabetLetter
型は文字への参照を保持し、メッセージを生成します。
file record AlphabetLetter(char Letter)
{
internal string Message =>
$"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}
ヒント
file
アクセス修飾子は、Program.cs ファイル内で定義され、Program.cs ファイルからのみアクセスされるため、AlphabetLetter
型で使用されます。 詳細については、「file (C# リファレンス)」を参照してください。 完全なソース コードについては、「Program.cs」セクションを参照してください。
このサンプルには、アルファベット文字を反復処理するヘルパー関数が含まれています。
static async ValueTask IterateAlphabetAsync(
Func<char, Task> asyncFunc)
{
for (char letter = 'A'; letter <= 'Z'; ++letter)
{
await asyncFunc(letter);
}
Console.WriteLine();
}
前述の C# コードでは:
Func<char, Task> asyncFunc
は、反復ごとに待機し、現在の letter
を渡します。キャッシュに項目を追加するには、Create
または 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;
前述の C# コードでは:
addLettersToCacheTask
は IterateAlphabetAsync
に委任され、待機されます。Func<char, Task> asyncFunc
がラムダ式と競合しています。MemoryCacheEntryOptions
は、現在に対する絶対有効期限でインスタンス化されます。AlphabetLetter
オブジェクトがインスタンス化され、letter
と options
と共に Set に渡されます。アルファベットの各文字について、キャッシュ エントリが有効期限、および削除後のコールバックと共に書き込まれます。
削除後のコールバックにより、コンソールに削除された値の詳細が書き込まれます。
static void OnPostEviction(
object key, object? letter, EvictionReason reason, object? state)
{
if (letter is AlphabetLetter alphabetLetter)
{
Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
}
};
キャッシュが作成されたので、IterateAlphabetAsync
へのもう 1 つの呼び出しが待機状態になりますが、今回は 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
に letter
キーが含まれていて、value
が AlphabetLetter
のインスタンスである場合は、コンソールに書き込まれます。 letter
キーがキャッシュにない場合は削除され、削除後のコールバックが呼び出されました。
IMemoryCache
には、非同期の GetOrCreateAsync
を含む、便利な拡張メソッドが数多く用意されています。
サンプル アプリのソース コード全体が最上位のプログラムであり、次の 2 つの 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.";
}
MillisecondsDelayAfterAdd
と MillisecondsAbsoluteExpiration
の値を自由に調整して、キャッシュされたエントリの有効期限と削除の動作の変化を確認してください。 このコードを実行した場合の出力例を次に示します。 .NET イベントには非決定的な性質があるため、異なる出力になる場合があります。
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.
絶対有効期限 (MemoryCacheEntryOptions.AbsoluteExpirationRelativeToNow) が設定されているため、キャッシュされたすべての項目が最終的に削除されます。
データをキャッシュするための一般的な戦略の 1 つが、使用するデータ サービスとは別にキャッシュを更新することです。 BackgroundService は他のアプリケーション コードから独立して (またはバックグラウンドで) 実行されるため、"Worker サービス" テンプレートが優れた例です。 IHostedService の実装をホストするアプリケーションの実行が開始されると、対応する実装 (この場合 BackgroundService
または "worker") が同じプロセスで実行を開始します。 これらのホステッド サービスは、AddHostedService<THostedService>(IServiceCollection) 拡張メソッドを介してシングルトンとして DI に登録されます。 他のサービスは、任意のサービス有効期間で DI に登録できます。
重要
サービスの有効期間を理解しておくことは、非常に重要です。 AddMemoryCache を呼び出して、すべてのメモリ内キャッシュ サービスを登録すると、サービスはシングルトンとして登録されます。
HTTP 経由でアクセスできるサードパーティの API に依存する写真サービスを開発しているとしましょう。 この写真データの変更は頻繁ではありませんが、珍しくもありません。 それぞれの写真は、単純な record
で表現されます。
namespace CachingExamples.Memory;
public readonly record struct Photo(
int AlbumId,
int Id,
string Title,
string Url,
string ThumbnailUrl);
次の例では、いくつかのサービスが DI に登録されています。 各サービスが 1 つの役割を持っていること。
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();
前述の C# コードでは:
HttpClient
インスタンスは、AddHttpClient<TClient>(IServiceCollection) を使用して CacheWorker
クラスに登録されています。CacheWorker
クラスは AddHostedService<THostedService>(IServiceCollection) に登録されています。PhotoService
クラスは AddScoped<TService>(IServiceCollection) に登録されています。CacheSignal<T>
クラスは AddSingleton に登録されています。host
はビルダーからインスタンス化され、非同期的に開始されます。PhotoService
は、指定された条件 (または 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();
}
}
}
前述の C# コードでは:
IMemoryCache
、CacheSignal<Photo>
、および ILogger
が必要です。GetPhotosAsync
メソッド: Func<Photo, bool> filter
パラメーターを定義し、IAsyncEnumerable<Photo>
を返します。_cacheSignal.WaitAsync()
を呼び出して、解放されるまで待機します。これにより、キャッシュにアクセスする前にキャッシュが設定されます。_cache.GetOrCreateAsync()
を呼び出し、キャッシュ内のすべての写真を非同期に取得します。factory
引数は警告をログし、空の写真配列を返しますが、これが発生することはありません。yield return
で具体化されます。このサービスのコンシューマーは自由に GetPhotosAsync
メソッドを呼び出し、それに応じて写真を処理することができます。 キャッシュには写真が含まれているため、HttpClient
は不要です。
非同期シグナルは、ジェネリック型の制約付きシングルトン内のカプセル化された SemaphoreSlim インスタンスに基づきます。 CacheSignal<T>
は、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();
}
前の C# コードでは、デコレーター パターンを使用して SemaphoreSlim
のインスタンスがラップされます。 CacheSignal<T>
はシングルトンとして登録されます。そのため、任意のジェネリック型 (この場合は Photo
) を使って、すべてのサービスの有効期間にわたって使用できます。 キャッシュのシード処理をシグナル処理する必要があります。
CacheWorker
は 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;
}
}
}
}
前述の C# コードでは:
ILogger
、HttpClient
、および IMemoryCache
が必要です。_updateInterval
は 3 時間定義されます。ExecuteAsync
メソッド: "https://jsonplaceholder.typicode.com/photos"
に対して HTTP 要求を行い、応答を Photo
オブジェクトの配列としてマップします。"Photos"
キーの下の IMemoryCache
に配置されます。_cacheSignal.Release()
が呼び出され、シグナルを待機していたコンシューマーが解放されます。同じプロセスのコンシューマーは、IMemoryCache
に写真を要求できますが、CacheWorker
がキャッシュの更新を担当します。
一部のシナリオでは、分散キャッシュが必要です。たとえば、複数のアプリ サーバーの場合です。 分散キャッシュは、メモリ内キャッシュ アプローチよりも高いスケールアウトをサポートします。 分散キャッシュを使用すると、キャッシュ メモリが外部プロセスにオフロードされますが、追加のネットワーク I/O が必要であり、(標準の場合でも) 待機時間が少し長くなります。
分散キャッシュの抽象化は、Microsoft.Extensions.Caching.Memory
NuGet パッケージの一部であり、AddDistributedMemoryCache
拡張メソッドも含まれています。
注意事項
AddDistributedMemoryCache は、開発やテストのシナリオでのみ使用する必要があり、実行可能な実稼働の実装ではありません。
次のパッケージから、IDistributedCache
の使用可能な実装を検討してください。
Microsoft.Extensions.Caching.SqlServer
Microsoft.Extensions.Caching.StackExchangeRedis
NCache.Microsoft.Extensions.Caching.OpenSource
分散キャッシュ API は、メモリ内キャッシュ API の対応するものよりも少しプリミティブです。 キーと値のペアの方がより基本的です。 メモリ内キャッシュ キーは object
に基づいており、分散キーは string
です。 メモリ内キャッシュでは、値は任意の型指定されたジェネリックにできます。一方、分散キャッシュの値は byte[]
として保持されます。 異なる実装で厳密に型指定されたジェネリック値が公開されないわけではありませんが、その場合は実装の詳細になります。
分散キャッシュに値を作成するには、設定 API のいずれかを呼び出します。
メモリ内キャッシュの例の AlphabetLetter
レコードを使用して、オブジェクトを JSON にシリアル化し、string
を 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);
メモリ内キャッシュと同様に、キャッシュ エントリには、キャッシュ内の存在を微調整するのに役立つオプション (この場合は DistributedCacheEntryOptions) があります。
値を作成するための便利な拡張メソッドがいくつかあります。これは、byte[]
へのオブジェクトの string
表現のエンコードを回避するのに役立ちます。
分散キャッシュから値を読み取るには、取得 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);
}
キャッシュ エントリをキャッシュから読み取った後は、byte[]
から UTF8 でエンコードされた string
表現を取得できます
値を読み取るための便利な拡張メソッドがいくつかあります。これは、オブジェクトの string
表現への byte[]
のエンコードを回避するのに役立ちます。
分散キャッシュ内の値を 1 回の API 呼び出しで更新する方法はありません。代わりに、値のスライド式有効期限を次のいずれかの更新 API でリセットできます。
実際の値を更新する必要がある場合は、値を削除してから再追加する必要があります。
分散キャッシュの値を削除するには、削除 API のいずれかを呼び出します。
ヒント
前述の API には同期バージョンがありますが、分散キャッシュの実装はネットワーク I/O に依存しているという事実を考慮してください。 このため、非同期 API は使用しないよりは頻繁に使用する方が推奨されます。
.NET に関するフィードバック
.NET はオープンソース プロジェクトです。 フィードバックを提供するにはリンクを選択します。
トレーニング
モジュール
.NET Aspire プロジェクトでキャッシュを使用してパフォーマンスを向上させる - Training
このモジュールでは、.NET Aspire クラウドネイティブ アプリ内のキャッシュと、それらを使用してマイクロサービスのパフォーマンスを最適化する方法について学習します。