Redis
 Computer >> コンピューター >  >> プログラミング >> Redis

.NET 6 Web APIで実装するAzure Redis Cache:ステップバイステップ完全ガイド

本記事では、Azure Redis Cacheの基礎知識と、.NET Core Web APIを使った実装方法について詳しく解説します。

目次

  • はじめに
  • キャッシュとは?
  • キャッシュの種類
  • Redis Cacheについて
  • Azure Redis Cacheのセットアップ
  • ステップバイステップでの実装

前提条件

  • Visual Studio 2022
  • Azureアカウント
  • .NET Core 6

はじめに

近年、ソフトウェア業界においてキャッシングは非常に注目されている技術です。アプリケーションのパフォーマンスとスケーラビリティを大幅に向上させることができるためです。GmailやFacebookなどのWebアプリケーションを利用すると、その応答性の高さと優れたユーザー体験を実感できるはずです。インターネットユーザー数は膨大であり、大量のネットワークトラフィックや高い需要が発生するアプリケーションでは、パフォーマンスと応答性を維持するためにさまざまな工夫が求められます。そこで有効な解決策となるのがキャッシングであり、これがキャッシュ技術が広く採用される理由です。

キャッシュとは?

キャッシュとは、頻繁にアクセスされるデータを一時的なストレージに保存しておくためのメモリ領域のことです。これによりパフォーマンスが劇的に向上し、不要なデータベースへのアクセスを回避できます。使用頻度の高いデータをキャッシュに格納しておくことで、システム全体の負荷を軽減できます。

.NET 6 Web APIで実装するAzure Redis Cache:ステップバイステップ完全ガイド

.NET 6 Web APIで実装するAzure Redis Cache:ステップバイステップ完全ガイド

上記の図には、キャッシュを使用しない場合と使用する場合の2つのシナリオが示されています。キャッシュを使用しない場合、ユーザーがデータを必要とするたびに毎回データベースへアクセスすることになります。特にすべてのユーザーに対して同じ静的データを返すようなケースでは、時間計算量が増大し、パフォーマンスが低下します。一方、キャッシュを使用する場合、全ユーザー共通の同一データであれば、最初の1人のユーザーだけがデータベースにアクセスしてデータを取得し、それをキャッシュメモリに保存します。以降のユーザーはキャッシュから直接データを取得できるため、無駄なデータベースアクセスが発生しません。

キャッシュの種類

.NET Coreがサポートしているキャッシングには、基本的に以下の2種類があります。

  1. インメモリキャッシング(In-Memory Caching)
  2. 分散キャッシング(Distributed Caching)

インメモリキャッシュを使用する場合、データはアプリケーションサーバーのメモリ内に保存されます。必要になったタイミングでそこからデータを取得し、任意の場所で利用します。一方、分散キャッシングでは、Redisをはじめとする多くのサードパーティ製の仕組みが利用可能です。本記事では、Redis Cacheについて詳しく掘り下げ、.NET Coreでの動作方法を見ていきます。

分散キャッシング

.NET 6 Web APIで実装するAzure Redis Cache:ステップバイステップ完全ガイド

  • 分散キャッシングでは、データが複数のサーバー間で保存・共有されます。
  • マルチテナントアプリケーションにおいて、複数サーバー間で負荷を分散管理することで、アプリケーションのスケーラビリティとパフォーマンスを容易に向上させることができます。
  • 仮に将来あるサーバーがクラッシュして再起動しても、複数のサーバーが稼働しているため、アプリケーションへの影響はありません。

Redisは現在多くの企業で採用されており、アプリケーションのパフォーマンスとスケーラビリティ向上のための最も人気のあるキャッシュソリューションです。ここからは、Redisとその活用方法を順番に見ていきましょう。

Redis Cache

  • Redisはオープンソース(BSDライセンス)のインメモリ型データ構造ストアで、データベースとしても利用できます。
  • 主に頻繁に使用されるデータや静的なデータをキャッシュ内に保存し、ユーザーの要件に応じて活用します。
  • List、Set、Hashing、Streamなど、データ保存に利用できる豊富なデータ構造が用意されています。

Azure Redis Cacheのセットアップ

ステップ1

Azureポータルにログインします。

ステップ2

マーケットプレイスで「Azure Cache for Redis」を検索して開きます。

.NET 6 Web APIで実装するAzure Redis Cache:ステップバイステップ完全ガイド

ステップ3

「作成」をクリックし、必要な情報を入力します。

.NET 6 Web APIで実装するAzure Redis Cache:ステップバイステップ完全ガイド

.NET 6 Web APIで実装するAzure Redis Cache:ステップバイステップ完全ガイド

.NET 6 Web APIで実装するAzure Redis Cache:ステップバイステップ完全ガイド

.NET 6 Web APIで実装するAzure Redis Cache:ステップバイステップ完全ガイド

ステップ4

作成したキャッシュの「アクセスキー」セクションに移動し、.NET Core Web APIで必要になるプライマリ接続文字列をコピーします。

.NET 6 Web APIで実装するAzure Redis Cache:ステップバイステップ完全ガイド

ステップバイステップでの実装

ステップ1

Visual Studioを開き、新しい.NET Core Web APIプロジェクトを作成します。

.NET 6 Web APIで実装するAzure Redis Cache:ステップバイステップ完全ガイド

ステップ2

新しいプロジェクトを構成します。

ステップ3

追加情報を入力します。

ステップ4

プロジェクト構造を確認します。

ステップ5

商品詳細クラス(ProductDetails)を作成します。

namespace AzureRedisCacheDemo.Models {
 public class ProductDetails {
 public int Id { get; set; }
 public string ProductName { get; set; }
 public string ProductDescription { get; set; }
 public int ProductPrice { get; set; }
 public int ProductStock { get; set; }
 }
}

ステップ6

次に、Dataフォルダ内にDbContextクラスを追加します。

using AzureRedisCacheDemo.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
namespace AzureRedisCacheDemo.Data {
 public class DbContextClass: DbContext {
 public DbContextClass(DbContextOptions< DbContextClass > options): base(options) {}
 public DbSet< ProductDetails > Products { get; set; }
 }
}

ステップ7

続いて、初期データを挿入するために使用するSeed Dataクラスを追加します。

using AzureRedisCacheDemo.Models;
using Microsoft.EntityFrameworkCore;
namespace AzureRedisCacheDemo.Data
{
 public class SeedData
 {
 public static void Initialize(IServiceProvider serviceProvider)
 {
 using (var context = new DbContextClass(
 serviceProvider.GetRequiredService<DbContextOptions<DbContextClass>>()))
 {
 if (context.Products.Any())
 {
 return;
 }
 context.Products.AddRange(
 new ProductDetails
 {
 Id = 1,
 ProductName = "IPhone",
 ProductDescription = "IPhone 14",
 ProductPrice = 120000,
 ProductStock = 100
 },
 new ProductDetails
 {
 Id = 2,
 ProductName = "Samsung TV",
 ProductDescription = "Smart TV",
 ProductPrice = 400000,
 ProductStock = 120
 });
 context.SaveChanges();
 }
 }
 }
}

ステップ8

appsettings.jsonファイルにAzure Redis Cacheの接続文字列を設定します。

{
 "Logging": {
 "LogLevel": {
 "Default": "Information",
 "Microsoft.AspNetCore": "Warning"
 }
 },
 "AllowedHosts": "*",
 "RedisURL": "<valuefromportal>"
}

ステップ9

接続処理に使用するConfiguration ManagerクラスとConnection HelperクラスをHelperフォルダ内に作成します。

Configuration Manager

namespace AzureRedisCacheDemo.Helper {
 static class ConfigurationManager {
 public static IConfiguration AppSetting { get; }
 static ConfigurationManager() {
 AppSetting = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build();
 }
 }
}

Connection Helper

using StackExchange.Redis;
namespace AzureRedisCacheDemo.Helper {
 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;
 }
 }
 }
}

ステップ10

次に、Repositories内にIProductServiceインターフェースを追加します。

using AzureRedisCacheDemo.Models;
namespace AzureRedisCacheDemo.Repositories {
 public interface IProductService {
 public Task< List< ProductDetails >> ProductListAsync();
 public Task< ProductDetails > GetProductDetailByIdAsync(int productId);
 public Task< bool > AddProductAsync(ProductDetails productDetails);
 public Task< bool > UpdateProductAsync(ProductDetails productDetails);
 public Task< bool > DeleteProductAsync(int productId);
 }
}

ステップ11

続いて、ProductServiceクラスを作成し、先ほどのIProductServiceインターフェースを実装します。

using AzureRedisCacheDemo.Data;
using AzureRedisCacheDemo.Models;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace AzureRedisCacheDemo.Repositories {
 public class ProductService: IProductService {
 private readonly DbContextClass dbContextClass;
 public ProductService(DbContextClass dbContextClass) {
 this.dbContextClass = dbContextClass;
 }
 public async Task< List< ProductDetails >> ProductListAsync() {
 return await dbContextClass.Products.ToListAsync();
 }
 public async Task< ProductDetails > GetProductDetailByIdAsync(int productId) {
 return await dbContextClass.Products.Where(ele => ele.Id == productId).FirstOrDefaultAsync();
 }
 public async Task< bool > AddProductAsync(ProductDetails productDetails) {
 await dbContextClass.Products.AddAsync(productDetails);
 var result = await dbContextClass.SaveChangesAsync();
 if (result > 0) {
 return true;
 } else {
 return false;
 }
 }
 public async Task< bool > UpdateProductAsync(ProductDetails productDetails) {
 var isProduct = ProductDetailsExists(productDetails.Id);
 if (isProduct) {
 dbContextClass.Products.Update(productDetails);
 var result = await dbContextClass.SaveChangesAsync();
 if (result > 0) {
 return true;
 } else {
 return false;
 }
 }
 return false;
 }
 public async Task< bool > DeleteProductAsync(int productId) {
 var findProductData = dbContextClass.Products.Where(_ => _.Id == productId).FirstOrDefault();
 if (findProductData != null) {
 dbContextClass.Products.Remove(findProductData);
 var result = await dbContextClass.SaveChangesAsync();
 if (result > 0) {
 return true;
 } else {
 return false;
 }
 }
 return false;
 }
 private bool ProductDetailsExists(int productId) {
 return dbContextClass.Products.Any(e => e.Id == productId);
 }
 }
}

ステップ12

IRedisCacheインターフェースを作成します。

namespace AzureRedisCacheDemo.Repositories.AzureRedisCache {
 public interface IRedisCache {
 T GetCacheData< T >(string key);
 bool SetCacheData< T >(string key, T value, DateTimeOffset expirationTime);
 object RemoveData(string key);
 }
}

ステップ13

その後、RedisCacheクラスを作成し、先ほど定義したインターフェースのメソッドを実装します。

using AzureRedisCacheDemo.Helper;
using Newtonsoft.Json;
using StackExchange.Redis;
namespace AzureRedisCacheDemo.Repositories.AzureRedisCache
{
 public class RedisCache : IRedisCache
 {
 private IDatabase _db;
 public RedisCache()
 {
 ConfigureRedis();
 }
 private void ConfigureRedis()
 {
 _db = ConnectionHelper.Connection.GetDatabase();
 }
 public T GetCacheData<T>(string key)
 {
 var value = _db.StringGet(key);
 if (!string.IsNullOrEmpty(value))
 {
 return JsonConvert.DeserializeObject<T>(value);
 }
 return default;
 }
 public object RemoveData(string key)
 {
 bool _isKeyExist = _db.KeyExists(key);
 if (_isKeyExist == true)
 {
 return _db.KeyDelete(key);
 }
 return false;
 }
 public bool SetCacheData<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;
 }
 }
}

ステップ14

新しいProducts Controllerを作成します。

using AzureRedisCacheDemo.Models;
using AzureRedisCacheDemo.Repositories;
using AzureRedisCacheDemo.Repositories.AzureRedisCache;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace AzureRedisCacheDemo.Controllers
{
 [Route("api/[controller]")]
 [ApiController]
 public class ProductsController : ControllerBase
 {
 private readonly IProductService _productService;
 private readonly IRedisCache _redisCache;
 public ProductsController(IProductService productService, IRedisCache redisCache)
 {
 _productService = productService;
 _redisCache = redisCache;
 }
 /// <summary>
 /// 商品一覧を取得
 /// </summary>
 [HttpGet]
 public async Task<ActionResult<List<ProductDetails>>> ProductListAsync()
 {
 var cacheData = _redisCache.GetCacheData<List<ProductDetails>>("product");
 if (cacheData != null)
 {
 return new List<ProductDetails>(cacheData);
 }
 var productList = await _productService.ProductListAsync();
 if(productList != null)
 {
 var expirationTime = DateTimeOffset.Now.AddMinutes(5.0);
 _redisCache.SetCacheData<List<ProductDetails>>("product", productList, expirationTime);
 return Ok(productList);
 }
 else
 {
 return NoContent();
 }
 }
 /// <summary>
 /// IDで商品を取得
 /// </summary>
 [HttpGet("{productId}")]
 public async Task<ActionResult<ProductDetails>> GetProductDetailsByIdAsync(int productId)
 {
 var cacheData = _redisCache.GetCacheData<List<ProductDetails>>("product");
 if (cacheData != null)
 {
 ProductDetails filteredData = cacheData.Where(x => x.Id == productId).FirstOrDefault();
 return new ActionResult<ProductDetails>(filteredData);
 }
 var productDetails = await _productService.GetProductDetailByIdAsync(productId);
 if(productDetails != null)
 {
 return Ok(productDetails);
 }
 else
 {
 return NotFound();
 }
 }
 /// <summary>
 /// 新しい商品を追加
 /// </summary>
 [HttpPost]
 public async Task<IActionResult> AddProductAsync(ProductDetails productDetails)
 {
 var isProductInserted = await _productService.AddProductAsync(productDetails);
 _redisCache.RemoveData("product");
 if (isProductInserted)
 {
 return Ok(isProductInserted);
 }
 else
 {
 return BadRequest();
 }
 }
 /// <summary>
 /// 商品情報を更新
 /// </summary>
 [HttpPut]
 public async Task<IActionResult> UpdateProductAsync(ProductDetails productDetails)
 {
 var isProductUpdated = await _productService.UpdateProductAsync(productDetails);
 _redisCache.RemoveData("product");
 if (isProductUpdated)
 {
 return Ok(isProductUpdated);
 }
 else
 {
 return BadRequest();
 }
 }
 /// <summary>
 /// IDで商品を削除
 /// </summary>
 [HttpDelete]
 public async Task<IActionResult> DeleteProductAsync(int productId)
 {
 var isProductDeleted = await _productService.DeleteProductAsync(productId);
 _redisCache.RemoveData("product");
 if (isProductDeleted)
 {
 return Ok(isProductDeleted);
 }
 else
 {
 return BadRequest();
 }
 }
 }
}

ステップ15

Programクラス内にいくつかのサービスを登録します。

using AzureRedisCacheDemo.Data;
using AzureRedisCacheDemo.Models;
using AzureRedisCacheDemo.Repositories;
using AzureRedisCacheDemo.Repositories.AzureRedisCache;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using System;
var builder = WebApplication.CreateBuilder(args);
// コンテナにサービスを追加します。
builder.Services.AddScoped< IProductService, ProductService >();
builder.Services.AddDbContext< DbContextClass >(o => o.UseInMemoryDatabase("RedisCacheDemo"));
builder.Services.AddScoped< IRedisCache, RedisCache >();
builder.Services.AddControllers();
// Swagger/OpenAPI の設定の詳細については、https://aka.ms/aspnetcore/swashbuckle を参照してください。
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
using(var scope = app.Services.CreateScope()) {
 var services = scope.ServiceProvider;
 var context = services.GetRequiredService< DbContextClass >();
 SeedData.Initialize(services);
}
// HTTPリクエストパイプラインを設定します。
if (app.Environment.IsDevelopment()) {
 app.UseSwagger();
 app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

ステップ16

最後にアプリケーションを実行すると、APIの各エンドポイントを確認できるSwagger UIが表示されます。

ステップ17

商品取得のエンドポイントを叩いた後、Azureポータル内でRedis CLIを開いてみてください。初回アクセス時の商品リストがキャッシュに保存されていることを確認できます。

この実装では、まずキャッシュにデータが存在するかどうかを確認します。存在しない場合はデータベースからデータを取得し、同時にキャッシュにも保存します。この処理はすでにコントローラー内のコードとして実装済みです。次回以降のリクエストでは、キャッシュから直接データが取得される仕組みです。コントローラー内にデバッガーを仕込めば、実際の動作フローをより深く理解できるでしょう。

GitHubリポジトリ

https://github.com/Jaydeep-007/AzureRedisCacheDemo/tree/master/AzureRedisCacheDemo

まとめ

本記事では、キャッシュの基本概念とAzure上での設定方法、さらに.NET Core Web APIを使ったステップバイステップの実装手順について解説しました。キャッシュを適切に活用することで、アプリケーションのパフォーマンスとスケーラビリティを大きく改善できますので、ぜひ実際のプロジェクトでも試してみてください。

  1. Redisで実現するインメモリキャッシュ入門:高速かつ信頼性の高いデータ取得をマスターする

    高速なレスポンスが求められるWebアプリやAPIを開発する際、キャッシュは成功を左右する重要な要素となります。 キャッシュがない場合、サーバーは同じデータを何度も取得するために時間を浪費してしまいます。データベース、サードパーティAPI、あるいは低速なストレージシステムへのアクセスがその典型例です。 しかし、そのデータをメモリ上に保存すれば、同じ情報をミリ秒単位で提供できるようになります。そこで活躍するのがRedisです。 Redisは、データをRAMに保存し瞬時に取り出せる高速かつ柔軟なツールです。ダッシュボードの構築、SNS投稿の自動化、ユーザーセッションの管理など、あらゆる場面でシステ

  2. RedisDays San Francisco 2022 開催レポート:開発者体験を変えるRedisの最新イノベーション

    RedisDays San Franciscoは、Redis開発者コミュニティに捧げられた1日限りのイベントです。ゲストスピーカーたちは、Redisのリアルタイムデータイノベーションが、開発者体験をシンプルにすることで、いかにアプリ開発を高速化できるかを紹介しました。新製品の発表、製品アップデート、ステップバイステップのウォークスルーなど、アプリ開発をより簡単かつ迅速に行うために必要なツールを開発者コミュニティに届けることに特化したイベントとなりました。それでは早速、内容を見ていきましょう。 Redis Stack 登場 「リアルタイム対応は、今や消費者と企業の双方が当たり前に期待すること