Redisキャッシュを.NET Core APIに統合してパフォーマンスとスケーラビリティを向上させる方法
はじめに
本記事では、.NET Coreにおけるキャッシュの仕組みと、その実装方法について詳しく解説します。以下のトピックを順番に見ていきましょう。
- キャッシュの概要
- キャッシュとは何か
- キャッシュの種類
- キャッシュの実装方法
近年、ソフトウェア業界においてキャッシングは非常に注目されている技術です。アプリケーションのパフォーマンスとスケーラビリティを大幅に向上させることができるからです。GmailやFacebookなどのWebアプリケーションを使うと、その応答性の高さと優れたユーザー体験を実感できるはずです。インターネット利用者数が増加し、アプリケーションへのトラフィックや需要が膨大になると、パフォーマンスと応答性を維持するためにさまざまな対策が必要になります。そこで有効なソリューションとして登場するのが「キャッシュ」なのです。
それでは、一つずつ見ていきましょう。
キャッシュとは?
キャッシュとは、頻繁にアクセスされるデータを一時的なストレージに保存するためのメモリ領域です。不要なデータベースへのアクセスを回避し、よく使われるデータをバッファに保持することで、パフォーマンスを劇的に向上させることができます。


上記の画像には、キャッシュを使用しない場合と使用する場合の2つのシナリオが示されています。キャッシュを使用しない場合、ユーザーがデータを要求するたびに毎回データベースへアクセスすることになり、処理時間が増大してパフォーマンスが低下します。特に、すべてのユーザーに対して同じ静的データを返すようなケースでは、各ユーザーが不必要にデータベースへアクセスすることになってしまいます。
一方、キャッシュを使用した場合はどうでしょうか。すべてのユーザーに対して同じ静的データを提供するケースでは、最初の1人のユーザーだけがデータベースにアクセスし、取得したデータをキャッシュメモリに保存します。その後、他のユーザーはキャッシュからデータを取得できるため、無駄なデータベースアクセスを避けることができます。
キャッシュの種類
.NET Coreがサポートするキャッシュには、主に以下の2種類があります。
- インメモリキャッシュ(In-Memory Caching)
- 分散キャッシュ(Distributed Caching)
インメモリキャッシュでは、データはアプリケーションサーバーのメモリ上に保存され、必要なときにそこからデータを取得して利用します。一方、分散キャッシュでは、Redisをはじめとする多くのサードパーティ製の仕組みを利用できます。本記事では、Redisキャッシュについて詳しく掘り下げ、.NET Coreでどのように動作するのかを見ていきます。
分散キャッシュ

- 分散キャッシュでは、データが複数のサーバー間で保存・共有されます。
- マルチテナントアプリケーションにおいて、複数サーバー間で負荷を分散することで、スケーラビリティとパフォーマンスを容易に向上させることができます。
- 仮に将来あるサーバーがクラッシュして再起動しても、必要に応じて複数のサーバーが稼働しているため、アプリケーションへの影響はありません。
Redisは最も人気のあるキャッシュ技術であり、現在多くの企業がアプリケーションのパフォーマンスとスケーラビリティ向上のために採用しています。それでは、Redisとその使い方を順番に見ていきましょう。
Redisキャッシュとは
- Redisはオープンソース(BSDライセンス)のインメモリ・データ構造ストアであり、データベースとしても利用できます。
- 基本的には、頻繁に使用されるデータや静的なデータをキャッシュ内に保存し、ユーザーの要件に応じて活用します。
- RedisにはList、Set、Hash、Streamなど、データ保存に利用できる多彩なデータ構造が用意されています。
Redisキャッシュのインストール
ステップ1. 以下のURLからRedisサーバーをダウンロードします。
https://github.com/microsoftarchive/redis/releases/tag/win-3.0.504
ステップ2. ZIPファイルを解凍し、Redis ServerとRedis CLIを開きます。

.NET Core APIによるRedisキャッシュの実装
ステップ1. .NET Core APIのWebアプリケーションを作成します。
ステップ2. アプリケーションに必要な以下のNuGetパッケージをインストールします。
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.Design
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
- Swashbuckle.AspNetCore
- StackExchange.Redis
ステップ3. Modelフォルダを作成し、その中にProductクラスを以下の内容で作成します。
namespace RedisCacheDemo.Model
{
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public string ProductDescription { get; set; }
public int Stock { get; set; }
}
}
ステップ4. 次に、データベース操作用のDbContextClassクラスを以下のように作成します。
using Microsoft.EntityFrameworkCore;
using RedisCacheDemo.Model;
namespace RedisCacheDemo.Data {
public class DbContextClass: DbContext {
public DbContextClass(DbContextOptions < DbContextClass > options): base(options) {}
public DbSet < Product > Products {
get;
set;
}
}
}
ステップ5. 続いて、Redisキャッシュ関連の処理を行うためのICacheServiceインターフェースとCacheServiceクラスを作成します。
using System;
namespace RedisCacheDemo.Cache
{
public interface ICacheService
{
/// <summary>
/// キーを指定してデータを取得する
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <returns></returns>
T GetData<T>(string key);
/// <summary>
/// 値とキーの有効期限を指定してデータを保存する
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="expirationTime"></param>
/// <returns></returns>
bool SetData<T>(string key, T value, DateTimeOffset expirationTime);
/// <summary>
/// データを削除する
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
object RemoveData(string key);
}
}
using Newtonsoft.Json;
using StackExchange.Redis;
using System;
namespace RedisCacheDemo.Cache {
public class CacheService: ICacheService {
private IDatabase _db;
public CacheService() {
ConfigureRedis();
}
private void ConfigureRedis() {
_db = ConnectionHelper.Connection.GetDatabase();
}
public T GetData < T > (string key) {
var value = _db.StringGet(key);
if (!string.IsNullOrEmpty(value)) {
return JsonConvert.DeserializeObject < T > (value);
}
return default;
}
public bool SetData < T > (string key, T value, DateTimeOffset expirationTime) {
TimeSpan expiryTime = expirationTime.DateTime.Subtract(DateTime.Now);
var isSet = _db.StringSet(key, JsonConvert.SerializeObject(value), expiryTime);
return isSet;
}
public object RemoveData(string key) {
bool _isKeyExist = _db.KeyExists(key);
if (_isKeyExist == true) {
return _db.KeyDelete(key);
}
return false;
}
}
}
ステップ6. ProductControllerクラスを作成し、以下のようなメソッドを定義します。
using Microsoft.AspNetCore.Mvc;
using RedisCacheDemo.Cache;
using RedisCacheDemo.Data;
using RedisCacheDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace RedisCacheDemo.Controllers {
[Route("api/[controller]")]
[ApiController]
public class ProductController: ControllerBase {
private readonly DbContextClass _dbContext;
private readonly ICacheService _cacheService;
public ProductController(DbContextClass dbContext, ICacheService cacheService) {
_dbContext = dbContext;
_cacheService = cacheService;
}
[HttpGet("products")]
public IEnumerable < Product > Get() {
var cacheData = _cacheService.GetData < IEnumerable < Product >> ("product");
if (cacheData != null) {
return cacheData;
}
var expirationTime = DateTimeOffset.Now.AddMinutes(5.0);
cacheData = _dbContext.Products.ToList();
_cacheService.SetData < IEnumerable < Product >> ("product", cacheData, expirationTime);
return cacheData;
}
[HttpGet("product")]
public Product Get(int id) {
Product filteredData;
var cacheData = _cacheService.GetData < IEnumerable < Product >> ("product");
if (cacheData != null) {
filteredData = cacheData.Where(x => x.ProductId == id).FirstOrDefault();
return filteredData;
}
filteredData = _dbContext.Products.Where(x => x.ProductId == id).FirstOrDefault();
return filteredData;
}
[HttpPost("addproduct")]
public async Task < Product > Post(Product value) {
var obj = await _dbContext.Products.AddAsync(value);
_cacheService.RemoveData("product");
_dbContext.SaveChanges();
return obj.Entity;
}
[HttpPut("updateproduct")]
public void Put(Product product) {
_dbContext.Products.Update(product);
_cacheService.RemoveData("product");
_dbContext.SaveChanges();
}
[HttpDelete("deleteproduct")]
public void Delete(int Id) {
var filteredData = _dbContext.Products.Where(x => x.ProductId == Id).FirstOrDefault();
_dbContext.Remove(filteredData);
_cacheService.RemoveData("product");
_dbContext.SaveChanges();
}
}
}
ステップ7. appsettings.jsonにSQL Serverの接続文字列とRedisのURLを追加します。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"RedisURL": "127.0.0.1:6379",
"ConnectionStrings": {
"DefaultConnection": "Data Source=Server;Initial Catalog=RedisCache;User Id=sa;Password=***;"
}
}
ステップ8. 次に、StartupクラスのConfigureServicesメソッド内でICacheServiceを登録し、APIエンドポイントをテストするためのSwagger関連の設定も追加します。
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using RedisCacheDemo.Cache;
using RedisCacheDemo.Data;
namespace RedisCacheDemo {
public class Startup {
public Startup(IConfiguration configuration) {
Configuration = configuration;
}
public IConfiguration Configuration {
get;
}
// このメソッドはランタイムによって呼び出されます。サービスをコンテナに追加するために使用します。
public void ConfigureServices(IServiceCollection services) {
services.AddControllers();
services.AddScoped < ICacheService, CacheService > ();
services.AddDbContext < DbContextClass > (options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddSwaggerGen(c => {
c.SwaggerDoc("v1", new OpenApiInfo {
Title = "RedisCacheDemo", Version = "v1"
});
});
}
// このメソッドはランタイムによって呼び出されます。HTTPリクエストパイプラインを構成するために使用します。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "RedisCacheDemo v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
});
}
}
}
ステップ9. アプリ設定を読み込むためのConfigurationManagerクラスを作成します。
using Microsoft.Extensions.Configuration;
using System.IO;
namespace RedisCacheDemo {
static class ConfigurationManager {
public static IConfiguration AppSetting {
get;
}
static ConfigurationManager() {
AppSetting = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build();
}
}
}
ステップ10. 次に、Redis接続用のConnectionHelperクラスを作成します。
using StackExchange.Redis;
using System;
namespace RedisCacheDemo.Cache {
public class ConnectionHelper {
static ConnectionHelper() {
ConnectionHelper.lazyConnection = new Lazy < ConnectionMultiplexer > (() => {
return ConnectionMultiplexer.Connect(ConfigurationManager.AppSetting["RedisURL"]);
});
}
private static Lazy < ConnectionMultiplexer > lazyConnection;
public static ConnectionMultiplexer Connection {
get {
return lazyConnection.Value;
}
}
}
}
ステップ11. パッケージマネージャーコンソールで以下のコマンドを実行し、マイグレーションとデータベース更新を行ってDBを作成します。
add-migration “FirstMigration”
update-database
このコマンドを実行すると、マイグレーションに関連するファイルが生成され、appsettings.jsonの接続文字列で指定したSQL Server内にデータベースが作成されます。
ステップ12. 最後に、アプリケーションを実行し、Swagger UIからデータを追加して、productsおよびproductエンドポイントでキャッシュがどのように機能するかを確認しましょう。
ここでは、コントローラーのproductsとproductエンドポイントにキャッシュを組み込んでいます。ユーザーが全製品データを取得しようとすると、まずRedisキャッシュ内に該当データが存在するかどうかが確認されます。キャッシュにデータがあれば、そのデータがユーザーに返されます。キャッシュに存在しない場合は、データベースからデータを取得し、その結果をキャッシュに保存します。これにより、次回以降のリクエストではキャッシュからデータが返され、不要なデータベースアクセスを回避できます。
また、製品IDを指定してデータを取得する場合も同様です。コントローラーの2番目のエンドポイント(product)では、まずキャッシュから全製品データを取得し、その後製品IDでフィルタリングします。該当データがキャッシュに存在すればキャッシュからユーザーに返され、存在しなければデータベースから取得してフィルタリング後に返します。
さらに、ProductControllerのupdate、delete、postエンドポイントでは、removeメソッドを使ってキャッシュから「product」キーのデータを削除しています。このように、メモリキャッシュにはさまざまな活用シーンがあり、ニーズや要件に応じて柔軟に利用できます。本記事では、Redisキャッシュの基本と、.NET Coreにおける動作の基礎を紹介しました。
注意すべきシナリオ:同時アクセスへの対策
キャッシュを利用する際に注意が必要なシナリオがあります。例えば、2人のユーザーが同時にアプリケーションを利用している場合、以下のような状況が発生します。
- 1人目のユーザーが全製品データの取得リクエストを送信すると、まずキャッシュ内にデータが存在するかが確認されます。データが存在しない場合は、データベースからデータを取得し、キャッシュに保存します。
- その間に、2人目のユーザーが製品詳細の取得リクエストを送信すると、1人目のリクエストが完了する前にこのリクエストもデータベースにアクセスしてしまうため、2人目のユーザーもデータベースにアクセスすることになります。
- この問題を解決するには、以下のようにロック機構(Lock Mechanism)を利用します。
まず、クラスの先頭にロック用のプライベートオブジェクトを作成します。
private static object _lock = new object()
次に、Getメソッドを以下のように修正します。
public IEnumerable < Product > Get() {
var cacheData = _cacheService.GetData < IEnumerable < Product >> ("product");
if (cacheData != null) {
return cacheData;
}
lock(_lock) {
var expirationTime = DateTimeOffset.Now.AddMinutes(5.0);
cacheData = _dbContext.Products.ToList();
_cacheService.SetData < IEnumerable < Product >> ("product", cacheData, expirationTime);
}
return cacheData;
}
このコードでは、まずキャッシュ内にデータが存在するかを確認し、存在すればそのまま返します。次に、Redisキャッシュに値が存在しない場合はロックを適用します。ロックを獲得したリクエストがクリティカルセクションに入り、データベースから製品情報を取得してキャッシュに保存した後、データを返します。では、1人目のユーザーのリクエストが完了する前に2人目のユーザーがリクエストを送信した場合はどうなるでしょうか。この場合、2番目のリクエストはキューで待機し、1人目のリクエストが完了した時点で処理されます。
また、Redis CLIを使えば、Redis内に既に存在するキーの詳細を以下のように確認できます。

ご覧のとおり、Redisキャッシュ内に存在するキーに関する情報を取得できるコマンドが多数用意されています。
以上が、.NET CoreにおけるRedisキャッシュの解説でした。この記事の内容が皆さんのお役に立てば幸いです。
Happy Coding!
-
Netlify Edge FunctionsとUpstashサーバーレスRedisで始めるエッジアプリ開発
Netlifyは最近、Denoランタイム上でエッジロケーションにてコードを実行できる「Edge Functions」を発表しました。これにより、グローバルに低レイテンシでコードを実行することが可能になります。この記事では、Netlify Edge Functionsを使ったシンプルなアプリを構築し、データストアとしてUpstash Redisへアクセスする方法を紹介します。 Upstash RedisがNetlify Edge Functionsと相性抜群である理由は、以下の点にあります。 Upstash Redisには「Global」データベースタイプがあり、Redisレプリカが世界中に分
-
Upstash RedisキャッシュでPrisma ORMのパフォーマンスを向上させる方法
前回のブログ記事では、キャッシュのメリット、一般的なキャッシュ手法、そしてUpstash Redisを使ってSQLデータをキャッシュする方法を、さまざまなコードサンプルとともに解説しました。 Upstash RedisによるSQLクエリのキャッシュについて、その詳細とメリットを確認したところで、今回はさらに一歩進めて実際の実装を見ていきましょう。そのためにまず、広く使われているORMであるPrismaを取り上げ、Upstash Redisによるキャッシュがデータベースクエリに対してどのように機能するかを確認します。 この記事では、Upstash RedisとPrismaを統合し、キャッシュアサ