マスター ASP.NET Core キャッシュ:ピーク パフォーマンスのための Redis、インメモリ、および分散パターン – パート 27

前の記事: ASP.NET Core のパフォーマンス ハック:非同期、プロファイリング、最適化テクニック (パート - 26/40)
目次
<オル>キャッシング革命
キャッシュの基礎とアーキテクチャ
インメモリ キャッシュの詳細
Redis を使用した分散キャッシュ
応答キャッシュ戦略
現実世界の電子商取引キャッシュ
キャッシュ無効化パターン
パフォーマンスの監視と分析
高度なキャッシュ パターン
セキュリティとベストプラクティス
キャッシュ戦略のテスト
本番展開
1.キャッシング革命
キャッシュがパフォーマンスの特効薬である理由
キャッシュにより、データベースの負荷が軽減され、応答時間が短縮され、スケーラビリティが向上するため、アプリケーションのパフォーマンスが変わります。最新の Web アプリケーションでは、キャッシュは単なる最適化ではなく、必要不可欠なものです。
現実世界のたとえ :忙しいコーヒーショップを想像してください。キャッシュを使用しない場合、顧客の注文には次のものが必要になります。
-
サプライヤーの倉庫 (データベース) に移動します
-
Bean の選択 (クエリ実行)
-
挽きたて(データ処理)
-
個別に醸造する (応答生成)
キャッシュを使用すると、人気のドリンクが事前に準備されており、すぐに提供できるようになります。
// Performance impact demonstrationpublic class ProductServiceWithoutCaching{
private readonly ApplicationDbContext _context;
public async Task<Product> GetProductAsync(int id)
{
// Every call hits the database - SLOW!
return await _context.Products
.Include(p => p.Category)
.Include(p => p.Reviews)
.FirstOrDefaultAsync(p => p.Id == id);
}}
public class ProductServiceWithCaching{
private readonly ApplicationDbContext _context;
private readonly IMemoryCache _cache;
public async Task<Product> GetProductAsync(int id)
{
// Try cache first - FAST!
if (_cache.TryGetValue($"product_{id}", out Product product))
return product;
// Cache miss - get from database
product = await _context.Products
.Include(p => p.Category)
.Include(p => p.Reviews)
.FirstOrDefaultAsync(p => p.Id == id);
// Store in cache for future requests
_cache.Set($"product_{id}", product, TimeSpan.FromMinutes(30));
return product;
}}
キャッシュのパフォーマンスへの影響
2.キャッシュの基礎とアーキテクチャ
キャッシュ アーキテクチャ パターン
// Comprehensive caching service interfacepublic interface ICacheService{
Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null);
Task<T> GetAsync<T>(string key);
Task SetAsync<T>(string key, T value, TimeSpan? expiration = null);
Task RemoveAsync(string key);
Task<bool> ExistsAsync(string key);
Task RemoveByPatternAsync(string pattern);}
// Implementation supporting multiple cache providerspublic class CacheService : ICacheService{
private readonly IMemoryCache _memoryCache;
private readonly IDistributedCache _distributedCache;
private readonly ILogger<CacheService> _logger;
private readonly CacheSettings _settings;
public CacheService(IMemoryCache memoryCache, IDistributedCache distributedCache,
ILogger<CacheService> logger, IOptions<CacheSettings> settings)
{
_memoryCache = memoryCache;
_distributedCache = distributedCache;
_logger = logger;
_settings = settings.Value;
}
public async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null)
{
try
{
// Try memory cache first (fastest)
if (_memoryCache.TryGetValue(key, out T cachedValue))
{
_logger.LogDebug("Memory cache hit for key: {Key}", key);
return cachedValue;
}
// Try distributed cache
var distributedValue = await _distributedCache.GetAsync<T>(key);
if (distributedValue != null)
{
_logger.LogDebug("Distributed cache hit for key: {Key}", key);
// Populate memory cache for faster subsequent access
var memoryCacheOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration ?? _settings.DefaultExpiration
};
_memoryCache.Set(key, distributedValue, memoryCacheOptions);
return distributedValue;
}
// Cache miss - execute factory method
_logger.LogDebug("Cache miss for key: {Key}, executing factory", key);
var value = await factory();
if (value != null)
{
// Store in both caches
var cacheExpiration = expiration ?? _settings.DefaultExpiration;
// Memory cache
var memoryOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = cacheExpiration
};
_memoryCache.Set(key, value, memoryOptions);
// Distributed cache
var distributedOptions = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = cacheExpiration
};
await _distributedCache.SetAsync(key, value, distributedOptions);
}
return value;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in GetOrCreateAsync for key: {Key}", key);
// If caching fails, fall back to factory method
return await factory();
}
}
public async Task<T> GetAsync<T>(string key)
{
// Implementation similar to above without factory fallback
if (_memoryCache.TryGetValue(key, out T memoryValue))
return memoryValue;
var distributedValue = await _distributedCache.GetAsync<T>(key);
if (distributedValue != null)
{
// Populate memory cache
_memoryCache.Set(key, distributedValue,
TimeSpan.FromMinutes(_settings.MemoryCacheExpirationMinutes));
return distributedValue;
}
return default(T);
}
public async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null)
{
var cacheExpiration = expiration ?? _settings.DefaultExpiration;
// Memory cache
_memoryCache.Set(key, value, cacheExpiration);
// Distributed cache
await _distributedCache.SetAsync(key, value,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = cacheExpiration
});
}
public async Task RemoveAsync(string key)
{
_memoryCache.Remove(key);
await _distributedCache.RemoveAsync(key);
_logger.LogInformation("Cache removed for key: {Key}", key);
}
public async Task<bool> ExistsAsync(string key)
{
return _memoryCache.TryGetValue(key, out _) ||
await _distributedCache.GetAsync(key) != null;
}
// Pattern-based removal for cache invalidation
public async Task RemoveByPatternAsync(string pattern)
{
// This is a simplified version - actual implementation depends on cache provider
_logger.LogInformation("Removing cache entries matching pattern: {Pattern}", pattern);
// In real implementation, you'd use Redis keys command or similar
// This is a conceptual implementation
}}
// Configuration modelpublic class CacheSettings{
public TimeSpan DefaultExpiration { get; set; } = TimeSpan.FromMinutes(30);
public int MemoryCacheExpirationMinutes { get; set; } = 10;
public string RedisConnectionString { get; set; }
public bool UseDistributedCache { get; set; } = true;}
Program.cs でのキャッシュ構成
// Program.cs - Comprehensive caching setupvar builder = WebApplication.CreateBuilder(args);
// Configure cache settings
builder.Services.Configure<CacheSettings>(builder.Configuration.GetSection("CacheSettings"));
// Add memory cache (always available)
builder.Services.AddMemoryCache(options =>{
options.SizeLimit = 1024 * 1024 * 100; // 100MB limit
options.CompactionPercentage = 0.25; // Compact when 25% full});
// Add distributed cache based on configurationvar cacheSettings = builder.Configuration.GetSection("CacheSettings").Get<CacheSettings>();if (cacheSettings.UseDistributedCache && !string.IsNullOrEmpty(cacheSettings.RedisConnectionString)){
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = cacheSettings.RedisConnectionString;
options.InstanceName = "MyApp:";
});
// Register Redis connection for direct access
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect(cacheSettings.RedisConnectionString));}else{
// Fallback to distributed memory cache (for development)
builder.Services.AddDistributedMemoryCache();}
// Register caching services
builder.Services.AddScoped<ICacheService, CacheService>();
builder.Services.AddScoped<IProductCacheService, ProductCacheService>();
builder.Services.AddScoped<IUserCacheService, UserCacheService>();
// Add response caching
builder.Services.AddResponseCaching(options =>{
options.MaximumBodySize = 1024 * 1024; // 1MB
options.UseCaseSensitivePaths = false;});
// Add output caching (ASP.NET Core 7+)
builder.Services.AddOutputCache(options =>{
options.AddBasePolicy(builder =>
builder.Expire(TimeSpan.FromMinutes(10)));
options.AddPolicy("Products", builder =>
builder.Expire(TimeSpan.FromMinutes(5))
.Tag("products"));});
var app = builder.Build();
// Use response caching middleware
app.UseResponseCaching();
// Use output caching middleware
app.UseOutputCache();
app.Run();
3.インメモリ キャッシュの詳細
高度なメモリ キャッシュ パターン
// Smart memory cache service with eviction policiespublic class SmartMemoryCacheService{
private readonly IMemoryCache _cache;
private readonly ILogger<SmartMemoryCacheService> _logger;
private readonly ConcurrentDictionary<string, CacheEntryInfo> _cacheEntries;
public SmartMemoryCacheService(IMemoryCache cache, ILogger<SmartMemoryCacheService> logger)
{
_cache = cache;
_logger = logger;
_cacheEntries = new ConcurrentDictionary<string, CacheEntryInfo>();
}
public async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory,
CacheOptions options = null)
{
options ??= new CacheOptions();
if (_cache.TryGetValue(key, out T cachedValue))
{
// Update access statistics
UpdateAccessStats(key);
_logger.LogDebug("Cache hit for {Key}", key);
return cachedValue;
}
// Cache miss - use factory method
_logger.LogDebug("Cache miss for {Key}, executing factory", key);
// Implement cache stampede protection
var semaphore = new SemaphoreSlim(1, 1);
await semaphore.WaitAsync();
try
{
// Double-check after acquiring lock
if (_cache.TryGetValue(key, out cachedValue))
{
UpdateAccessStats(key);
return cachedValue;
}
var value = await factory();
if (value != null)
{
var cacheEntryOptions = CreateCacheEntryOptions(options);
// Register callback for eviction
cacheEntryOptions.RegisterPostEvictionCallback(EvictionCallback);
_cache.Set(key, value, cacheEntryOptions);
// Track cache entry
_cacheEntries[key] = new CacheEntryInfo
{
Key = key,
Created = DateTime.UtcNow,
LastAccessed = DateTime.UtcNow,
AccessCount = 1,
Size = EstimateSize(value),
Options = options
};
}
return value;
}
finally
{
semaphore.Release();
}
}
public CacheStatistics GetStatistics()
{
var entries = _cacheEntries.Values.ToList();
return new CacheStatistics
{
TotalEntries = entries.Count,
TotalSize = entries.Sum(e => e.Size),
HitRate = CalculateHitRate(),
MostAccessed = entries.OrderByDescending(e => e.AccessCount).Take(10),
OldestEntries = entries.OrderBy(e => e.LastAccessed).Take(10)
};
}
public void Cleanup()
{
var now = DateTime.UtcNow;
var toRemove = new List<string>();
foreach (var entry in _cacheEntries)
{
var age = now - entry.Value.LastAccessed;
if (age > entry.Value.Options.MaxIdleTime)
{
toRemove.Add(entry.Key);
}
}
foreach (var key in toRemove)
{
_cache.Remove(key);
_cacheEntries.TryRemove(key, out _);
_logger.LogInformation("Removed idle cache entry: {Key}", key);
}
}
private void UpdateAccessStats(string key)
{
if (_cacheEntries.TryGetValue(key, out var info))
{
info.LastAccessed = DateTime.UtcNow;
info.AccessCount++;
}
}
private void EvictionCallback(object key, object value, EvictionReason reason, object state)
{
_logger.LogInformation("Cache entry evicted: {Key}, Reason: {Reason}", key, reason);
_cacheEntries.TryRemove(key.ToString(), out _);
}
private MemoryCacheEntryOptions CreateCacheEntryOptions(CacheOptions options)
{
var cacheOptions = new MemoryCacheEntryOptions
{
Size = options.Size
};
if (options.AbsoluteExpiration.HasValue)
cacheOptions.SetAbsoluteExpiration(options.AbsoluteExpiration.Value);
if (options.SlidingExpiration.HasValue)
cacheOptions.SetSlidingExpiration(options.SlidingExpiration.Value);
if (options.Priority.HasValue)
cacheOptions.SetPriority(options.Priority.Value);
return cacheOptions;
}
private long EstimateSize<T>(T value)
{
// Simple size estimation - in production, use more accurate methods
if (value == null) return 0;
try
{
using var stream = new MemoryStream();
var formatter = new BinaryFormatter();
formatter.Serialize(stream, value);
return stream.Length;
}
catch
{
return 1024; // Default 1KB estimate
}
}
private double CalculateHitRate()
{
var entries = _cacheEntries.Values.ToList();
if (entries.Count == 0) return 0;
var totalAccesses = entries.Sum(e => e.AccessCount);
var hits = entries.Sum(e => e.AccessCount - 1); // First access is always miss
return totalAccesses > 0 ? (double)hits / totalAccesses : 0;
}}
// Supporting classespublic class CacheOptions{
public TimeSpan? AbsoluteExpiration { get; set; }
public TimeSpan? SlidingExpiration { get; set; }
public TimeSpan MaxIdleTime { get; set; } = TimeSpan.FromHours(24);
public long Size { get; set; } = 1;
public CacheItemPriority? Priority { get; set; }}
public class CacheEntryInfo{
public string Key { get; set; }
public DateTime Created { get; set; }
public DateTime LastAccessed { get; set; }
public long AccessCount { get; set; }
public long Size { get; set; }
public CacheOptions Options { get; set; }}
public class CacheStatistics{
public int TotalEntries { get; set; }
public long TotalSize { get; set; }
public double HitRate { get; set; }
public IEnumerable<CacheEntryInfo> MostAccessed { get; set; }
public IEnumerable<CacheEntryInfo> OldestEntries { get; set; }}
現実世界のメモリ キャッシュの実装
// E-commerce product catalog with intelligent cachingpublic class ProductCatalogService{
private readonly IProductRepository _productRepository;
private readonly SmartMemoryCacheService _cache;
private readonly ILogger<ProductCatalogService> _logger;
private const string ProductsByCategoryKey = "products_category_{0}";
private const string FeaturedProductsKey = "products_featured";
private const string ProductDetailsKey = "product_{0}";
private const string ProductSearchKey = "products_search_{0}";
public ProductCatalogService(IProductRepository productRepository,
SmartMemoryCacheService cache,
ILogger<ProductCatalogService> logger)
{
_productRepository = productRepository;
_cache = cache;
_logger = logger;
}
public async Task<List<Product>> GetProductsByCategoryAsync(int categoryId, int page = 1, int pageSize = 20)
{
var cacheKey = string.Format(ProductsByCategoryKey, categoryId);
var options = new CacheOptions
{
SlidingExpiration = TimeSpan.FromMinutes(15),
AbsoluteExpiration = DateTimeOffset.Now.AddHours(1)
};
return await _cache.GetOrCreateAsync(cacheKey, async () =>
{
_logger.LogInformation("Loading products for category {CategoryId} from database", categoryId);
var products = await _productRepository.GetProductsByCategoryAsync(categoryId, page, pageSize);
// Pre-cache individual product details
foreach (var product in products)
{
var productCacheKey = string.Format(ProductDetailsKey, product.Id);
await _cache.SetAsync(productCacheKey, product,
new CacheOptions { SlidingExpiration = TimeSpan.FromMinutes(30) });
}
return products;
}, options);
}
public async Task<Product> GetProductDetailsAsync(int productId)
{
var cacheKey = string.Format(ProductDetailsKey, productId);
var options = new CacheOptions
{
SlidingExpiration = TimeSpan.FromMinutes(30),
AbsoluteExpiration = DateTimeOffset.Now.AddHours(2)
};
return await _cache.GetOrCreateAsync(cacheKey, async () =>
{
_logger.LogInformation("Loading product details for {ProductId} from database", productId);
var product = await _productRepository.GetProductWithDetailsAsync(productId);
if (product != null)
{
// Update popularity score in background
_ = Task.Run(async () =>
{
await _productRepository.IncrementViewCountAsync(productId);
});
}
return product;
}, options);
}
public async Task<List<Product>> SearchProductsAsync(string searchTerm, ProductSearchFilters filters)
{
var searchHash = GenerateSearchHash(searchTerm, filters);
var cacheKey = string.Format(ProductSearchKey, searchHash);
var options = new CacheOptions
{
SlidingExpiration = TimeSpan.FromMinutes(10),
AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(30)
};
return await _cache.GetOrCreateAsync(cacheKey, async () =>
{
_logger.LogInformation("Executing search for '{SearchTerm}' in database", searchTerm);
return await _productRepository.SearchProductsAsync(searchTerm, filters);
}, options);
}
public async Task<List<Product>> GetFeaturedProductsAsync()
{
var options = new CacheOptions
{
AbsoluteExpiration = DateTimeOffset.Now.AddHours(4) // Refresh every 4 hours
};
return await _cache.GetOrCreateAsync(FeaturedProductsKey, async () =>
{
_logger.LogInformation("Loading featured products from database");
return await _productRepository.GetFeaturedProductsAsync();
}, options);
}
public async Task InvalidateProductCacheAsync(int productId)
{
var productKey = string.Format(ProductDetailsKey, productId);
await _cache.RemoveAsync(productKey);
// Invalidate category caches that might contain this product
await InvalidateCategoryCachesAsync();
_logger.LogInformation("Invalidated cache for product {ProductId}", productId);
}
private async Task InvalidateCategoryCachesAsync()
{
// In real implementation, you'd track which categories need invalidation
// This is a simplified version
for (int i = 1; i <= 10; i++) // Assuming 10 main categories
{
var categoryKey = string.Format(ProductsByCategoryKey, i);
await _cache.RemoveAsync(categoryKey);
}
}
private string GenerateSearchHash(string searchTerm, ProductSearchFilters filters)
{
var json = JsonSerializer.Serialize(new { searchTerm, filters });
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(json));
return Convert.ToBase64String(hash);
}}
4. Redis を使用した分散キャッシュ
Redis 構成と高度なパターン
// Advanced Redis cache servicepublic class RedisCacheService : IDistributedCacheService{
private readonly IConnectionMultiplexer _redis;
private readonly IDatabase _database;
private readonly ILogger<RedisCacheService> _logger;
private readonly ISerializer _serializer;
public RedisCacheService(IConnectionMultiplexer redis, ILogger<RedisCacheService> logger, ISerializer serializer)
{
_redis = redis;
_database = redis.GetDatabase();
_logger = logger;
_serializer = serializer;
}
public async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory,
DistributedCacheEntryOptions options = null)
{
options ??= new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
};
try
{
// Try to get from Redis
var cachedValue = await GetAsync<T>(key);
if (cachedValue != null)
{
_logger.LogDebug("Redis cache hit for {Key}", key);
return cachedValue;
}
}
catch (RedisException ex)
{
_logger.LogWarning(ex, "Redis unavailable for key {Key}, falling back to factory", key);
return await factory();
}
// Cache miss - execute factory
_logger.LogDebug("Redis cache miss for {Key}", key);
var value = await factory();
if (value != null)
{
await SetAsync(key, value, options);
}
return value;
}
public async Task<T> GetAsync<T>(string key)
{
try
{
var cachedData = await _database.StringGetAsync(key);
if (cachedData.HasValue)
{
return _serializer.Deserialize<T>(cachedData);
}
return default(T);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting key {Key} from Redis", key);
throw;
}
}
public async Task SetAsync<T>(string key, T value, DistributedCacheEntryOptions options = null)
{
try
{
var serializedValue = _serializer.Serialize(value);
if (options != null)
{
await _database.StringSetAsync(key, serializedValue, options.AbsoluteExpirationRelativeToNow);
}
else
{
await _database.StringSetAsync(key, serializedValue);
}
_logger.LogDebug("Set Redis cache for key {Key}", key);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error setting key {Key} in Redis", key);
throw;
}
}
public async Task<bool> RemoveAsync(string key)
{
try
{
var result = await _database.KeyDeleteAsync(key);
_logger.LogDebug("Removed Redis key {Key}: {Result}", key, result);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error removing key {Key} from Redis", key);
throw;
}
}
public async Task<bool> KeyExistsAsync(string key)
{
try
{
return await _database.KeyExistsAsync(key);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking existence of key {Key} in Redis", key);
return false;
}
}
public async Task<long> GetMemoryUsageAsync(string key)
{
try
{
// Use Redis MEMORY USAGE command (requires Redis 4+)
var result = await _database.ExecuteAsync("MEMORY", "USAGE", key);
return (long)result;
}
catch
{
return -1;
}
}
public async Task<RedisCacheInfo> GetCacheInfoAsync()
{
try
{
var server = _redis.GetServer(_redis.GetEndPoints().First());
var info = await server.InfoAsync("memory", "stats");
return new RedisCacheInfo
{
UsedMemory = long.Parse(info[0]["used_memory"]),
UsedMemoryHuman = info[0]["used_memory_human"],
KeyCount = await _database.ExecuteAsync("DBSIZE") as long? ?? 0,
HitRate = await CalculateHitRateAsync(),
ConnectedClients = int.Parse(info[1]["connected_clients"])
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting Redis cache info");
return null;
}
}
public async Task<IEnumerable<string>> GetKeysByPatternAsync(string pattern)
{
var keys = new List<string>();
try
{
var server = _redis.GetServer(_redis.GetEndPoints().First());
await foreach (var key in server.KeysAsync(pattern: pattern))
{
keys.Add(key);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting keys for pattern {Pattern}", pattern);
}
return keys;
}
private async Task<double> CalculateHitRateAsync()
{
try
{
var server = _redis.GetServer(_redis.GetEndPoints().First());
var info = await server.InfoAsync("stats");
var hits = long.Parse(info[0]["keyspace_hits"]);
var misses = long.Parse(info[0]["keyspace_misses"]);
return hits + misses > 0 ? (double)hits / (hits + misses) : 0;
}
catch
{
return 0;
}
}}
// Redis cache information modelpublic class RedisCacheInfo{
public long UsedMemory { get; set; }
public string UsedMemoryHuman { get; set; }
public long KeyCount { get; set; }
public double HitRate { get; set; }
public int ConnectedClients { get; set; }}
// JSON serializer for Redispublic class JsonSerializer : ISerializer{
private readonly JsonSerializerOptions _options;
public JsonSerializer()
{
_options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
}
public T Deserialize<T>(string data)
{
return JsonSerializer.Deserialize<T>(data, _options);
}
public string Serialize<T>(T value)
{
return JsonSerializer.Serialize(value, _options);
}}
public interface ISerializer{
T Deserialize<T>(string data);
string Serialize<T>(T value);}
高トラフィック アプリケーションのための実際の Redis 実装
// Session management with Redispublic class RedisSessionService{
private readonly IDistributedCacheService _cache;
private readonly ILogger<RedisSessionService> _logger;
private const string SessionKeyPrefix = "session:";
private const string UserSessionsKey = "user_sessions:";
public RedisSessionService(IDistributedCacheService cache, ILogger<RedisSessionService> logger)
{
_cache = cache;
_logger = logger;
}
public async Task<Session> CreateSessionAsync(int userId, SessionData data)
{
var sessionId = GenerateSessionId();
var sessionKey = GetSessionKey(sessionId);
var userSessionsKey = GetUserSessionsKey(userId);
var session = new Session
{
Id = sessionId,
UserId = userId,
CreatedAt = DateTime.UtcNow,
LastAccessed = DateTime.UtcNow,
Data = data,
ExpiresAt = DateTime.UtcNow.AddDays(30)
};
var options = new DistributedCacheEntryOptions
{
AbsoluteExpiration = session.ExpiresAt
};
// Store session
await _cache.SetAsync(sessionKey, session, options);
// Add to user's sessions set
await _cache.SetAddAsync(userSessionsKey, sessionId);
_logger.LogInformation("Created session {SessionId} for user {UserId}", sessionId, userId);
return session;
}
public async Task<Session> GetSessionAsync(string sessionId)
{
var sessionKey = GetSessionKey(sessionId);
var session = await _cache.GetAsync<Session>(sessionKey);
if (session != null)
{
// Update last accessed time
session.LastAccessed = DateTime.UtcNow;
await _cache.SetAsync(sessionKey, session);
_logger.LogDebug("Retrieved session {SessionId}", sessionId);
}
return session;
}
public async Task<bool> ValidateSessionAsync(string sessionId)
{
var sessionKey = GetSessionKey(sessionId);
return await _cache.KeyExistsAsync(sessionKey);
}
public async Task InvalidateSessionAsync(string sessionId)
{
var session = await GetSessionAsync(sessionId);
if (session != null)
{
var sessionKey = GetSessionKey(sessionId);
var userSessionsKey = GetUserSessionsKey(session.UserId);
// Remove session
await _cache.RemoveAsync(sessionKey);
// Remove from user's sessions
await _cache.SetRemoveAsync(userSessionsKey, sessionId);
_logger.LogInformation("Invalidated session {SessionId}", sessionId);
}
}
public async Task InvalidateUserSessionsAsync(int userId)
{
var userSessionsKey = GetUserSessionsKey(userId);
var sessionIds = await _cache.SetMembersAsync<string>(userSessionsKey);
foreach (var sessionId in sessionIds)
{
var sessionKey = GetSessionKey(sessionId);
await _cache.RemoveAsync(sessionKey);
}
// Remove user sessions set
await _cache.RemoveAsync(userSessionsKey);
_logger.LogInformation("Invalidated all sessions for user {UserId}", userId);
}
public async Task<List<Session>> GetUserSessionsAsync(int userId)
{
var userSessionsKey = GetUserSessionsKey(userId);
var sessionIds = await _cache.SetMembersAsync<string>(userSessionsKey);
var sessions = new List<Session>();
foreach (var sessionId in sessionIds)
{
var session = await GetSessionAsync(sessionId);
if (session != null)
{
sessions.Add(session);
}
}
return sessions.OrderByDescending(s => s.LastAccessed).ToList();
}
public async Task CleanupExpiredSessionsAsync()
{
// Redis will automatically expire keys based on TTL
// This method is for additional cleanup if needed
_logger.LogInformation("Session cleanup completed by Redis TTL");
}
private string GenerateSessionId()
{
return Guid.NewGuid().ToString("N");
}
private string GetSessionKey(string sessionId)
{
return $"{SessionKeyPrefix}{sessionId}";
}
private string GetUserSessionsKey(int userId)
{
return $"{UserSessionsKey}{userId}";
}}
// Session modelspublic class Session{
public string Id { get; set; }
public int UserId { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime LastAccessed { get; set; }
public DateTime ExpiresAt { get; set; }
public SessionData Data { get; set; }}
public class SessionData{
public string UserAgent { get; set; }
public string IPAddress { get; set; }
public string Location { get; set; }
public Dictionary<string, object> CustomData { get; set; } = new();}
5.応答キャッシュ戦略
包括的な応答キャッシュの実装
// Advanced response caching servicepublic class ResponseCachingService{
private readonly IResponseCache _responseCache;
private readonly ILogger<ResponseCachingService> _logger;
public ResponseCachingService(IResponseCache responseCache, ILogger<ResponseCachingService> logger)
{
_responseCache = responseCache;
_logger = logger;
}
public async Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive)
{
try
{
if (response == null) return;
var cachedResponse = new CachedResponse
{
Content = response,
Created = DateTime.UtcNow,
Expires = DateTime.UtcNow.Add(timeToLive)
};
await _responseCache.SetAsync(cacheKey, cachedResponse, timeToLive);
_logger.LogDebug("Cached response for key {CacheKey}, TTL: {TimeToLive}", cacheKey, timeToLive);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error caching response for key {CacheKey}", cacheKey);
}
}
public async Task<CachedResponse> GetCachedResponseAsync(string cacheKey)
{
try
{
var cachedResponse = await _responseCache.GetAsync<CachedResponse>(cacheKey);
if (cachedResponse != null)
{
_logger.LogDebug("Cache hit for response key {CacheKey}", cacheKey);
// Check if expired
if (cachedResponse.Expires < DateTime.UtcNow)
{
await _responseCache.RemoveAsync(cacheKey);
_logger.LogDebug("Removed expired response for key {CacheKey}", cacheKey);
return null;
}
return cachedResponse;
}
_logger.LogDebug("Cache miss for response key {CacheKey}", cacheKey);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting cached response for key {CacheKey}", cacheKey);
return null;
}
}
public string GenerateCacheKey(string path, string queryString, string userId = null)
{
var keyBuilder = new StringBuilder();
keyBuilder.Append(path.ToLowerInvariant());
if (!string.IsNullOrEmpty(queryString))
{
keyBuilder.Append('?');
keyBuilder.Append(queryString.ToLowerInvariant());
}
if (!string.IsNullOrEmpty(userId))
{
keyBuilder.Append("|user:");
keyBuilder.Append(userId);
}
return keyBuilder.ToString();
}
public async Task InvalidateByPatternAsync(string pattern)
{
try
{
await _responseCache.RemoveByPatternAsync(pattern);
_logger.LogInformation("Invalidated responses matching pattern: {Pattern}", pattern);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error invalidating responses for pattern {Pattern}", pattern);
}
}}
// Cached response modelpublic class CachedResponse{
public object Content { get; set; }
public DateTime Created { get; set; }
public DateTime Expires { get; set; }
public string ETag { get; set; }
public DateTime? LastModified { get; set; }}
// Response cache implementationpublic interface IResponseCache{
Task SetAsync<T>(string key, T value, TimeSpan timeToLive);
Task<T> GetAsync<T>(string key);
Task RemoveAsync(string key);
Task RemoveByPatternAsync(string pattern);}
// Action filter for response cachingpublic class ResponseCachingAttribute : Attribute, IAsyncActionFilter{
private readonly int _duration;
private readonly bool _perUser;
private readonly string[] _varyBy;
public ResponseCachingAttribute(int duration, bool perUser = false, params string[] varyBy)
{
_duration = duration;
_perUser = perUser;
_varyBy = varyBy;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var cacheService = context.HttpContext.RequestServices.GetService<ResponseCachingService>();
var httpContext = context.HttpContext;
// Generate cache key
var cacheKey = GenerateCacheKey(httpContext, _perUser, _varyBy);
// Try to get from cache
var cachedResponse = await cacheService.GetCachedResponseAsync(cacheKey);
if (cachedResponse != null)
{
// Return cached response
context.Result = new ObjectResult(cachedResponse.Content)
{
StatusCode = 200
};
// Set cache headers
if (!string.IsNullOrEmpty(cachedResponse.ETag))
{
httpContext.Response.Headers.ETag = cachedResponse.ETag;
}
if (cachedResponse.LastModified.HasValue)
{
httpContext.Response.Headers.LastModified = cachedResponse.LastModified.Value.ToString("R");
}
return;
}
// Execute action
var executedContext = await next();
if (executedContext.Result is ObjectResult objectResult && objectResult.Value != null)
{
// Cache the response
var timeToLive = TimeSpan.FromSeconds(_duration);
await cacheService.CacheResponseAsync(cacheKey, objectResult.Value, timeToLive);
// Set response cache headers
httpContext.Response.Headers.CacheControl = $"public, max-age={_duration}";
httpContext.Response.Headers.Expires = DateTime.UtcNow.AddSeconds(_duration).ToString("R");
}
}
private string GenerateCacheKey(HttpContext httpContext, bool perUser, string[] varyBy)
{
var keyBuilder = new StringBuilder();
// Path and query string
keyBuilder.Append(httpContext.Request.Path);
keyBuilder.Append('?');
keyBuilder.Append(httpContext.Request.QueryString);
// User-specific caching
if (perUser && httpContext.User.Identity.IsAuthenticated)
{
keyBuilder.Append("|user:");
keyBuilder.Append(httpContext.User.GetUserId());
}
// Vary by headers
foreach (var header in varyBy)
{
if (httpContext.Request.Headers.TryGetValue(header, out var headerValue))
{
keyBuilder.Append($"|{header}:{headerValue}");
}
}
return keyBuilder.ToString();
}}
実際の応答キャッシュの実装
// Product controller with comprehensive caching[ApiController][Route("api/[controller]")]public class ProductsController : ControllerBase{
private readonly IProductService _productService;
private readonly IResponseCache _responseCache;
private readonly ILogger<ProductsController> _logger;
public ProductsController(IProductService productService, IResponseCache responseCache,
ILogger<ProductsController> logger)
{
_productService = productService;
_responseCache = responseCache;
_logger = logger;
}
[HttpGet]
[ResponseCaching(300)] // Cache for 5 minutes
public async Task<ActionResult<ApiResponse<List<Product>>>> GetProducts(
[FromQuery] ProductQuery query)
{
try
{
var products = await _productService.GetProductsAsync(query);
return Ok(new ApiResponse<List<Product>>(products));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting products");
return StatusCode(500, new ApiResponse<string>("Internal server error"));
}
}
[HttpGet("{id}")]
[ResponseCaching(600, varyBy: new[] { "Accept-Language" })] // Cache for 10 minutes, vary by language
public async Task<ActionResult<ApiResponse<Product>>> GetProduct(int id)
{
try
{
var product = await _productService.GetProductAsync(id);
if (product == null)
return NotFound(new ApiResponse<string>("Product not found"));
return Ok(new ApiResponse<Product>(product));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting product {ProductId}", id);
return StatusCode(500, new ApiResponse<string>("Internal server error"));
}
}
[HttpGet("featured")]
[ResponseCaching(900)] // Cache for 15 minutes
public async Task<ActionResult<ApiResponse<List<Product>>>> GetFeaturedProducts()
{
try
{
var products = await _productService.GetFeaturedProductsAsync();
return Ok(new ApiResponse<List<Product>>(products));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting featured products");
return StatusCode(500, new ApiResponse<string>("Internal server error"));
}
}
[HttpPost]
public async Task<ActionResult<ApiResponse<Product>>> CreateProduct(ProductCreateRequest request)
{
try
{
var product = await _productService.CreateProductAsync(request);
// Invalidate relevant caches
await InvalidateProductCachesAsync();
return CreatedAtAction(nameof(GetProduct), new { id = product.Id },
new ApiResponse<Product>(product));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating product");
return StatusCode(500, new ApiResponse<string>("Internal server error"));
}
}
[HttpPut("{id}")]
public async Task<ActionResult<ApiResponse<Product>>> UpdateProduct(int id, ProductUpdateRequest request)
{
try
{
var product = await _productService.UpdateProductAsync(id, request);
if (product == null)
return NotFound(new ApiResponse<string>("Product not found"));
// Invalidate relevant caches
await InvalidateProductCachesAsync(id);
return Ok(new ApiResponse<Product>(product));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating product {ProductId}", id);
return StatusCode(500, new ApiResponse<string>("Internal server error"));
}
}
[HttpDelete("{id}")]
public async Task<ActionResult<ApiResponse<bool>>> DeleteProduct(int id)
{
try
{
var result = await _productService.DeleteProductAsync(id);
if (!result)
return NotFound(new ApiResponse<string>("Product not found"));
// Invalidate relevant caches
await InvalidateProductCachesAsync(id);
return Ok(new ApiResponse<bool>(true));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting product {ProductId}", id);
return StatusCode(500, new ApiResponse<string>("Internal server error"));
}
}
private async Task InvalidateProductCachesAsync(int? productId = null)
{
try
{
// Invalidate product lists
await _responseCache.RemoveByPatternAsync("api/products*");
// Invalidate specific product if provided
if (productId.HasValue)
{
await _responseCache.RemoveAsync($"api/products/{productId}");
}
// Invalidate featured products
await _responseCache.RemoveAsync("api/products/featured");
_logger.LogInformation("Invalidated product caches for product {ProductId}", productId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error invalidating product caches");
}
}}
注: これは、150,000 語を超える完全なガイドからの包括的な抜粋です。この記事全体では、キャッシュ無効化パターン、パフォーマンス モニタリング、高度なキャッシュ パターン、セキュリティ、テスト戦略、および完全なコード例と実際のシナリオを含む本番環境への展開に関する詳細なセクションが続きます。
完全なガイドでは、次のような ASP.NET Core キャッシュのあらゆる側面を網羅的にカバーします。
-
イベントベースのパターンを使用した高度なキャッシュ無効化戦略
-
包括的なパフォーマンスの監視と分析
-
キャッシュのウォーミングとプリロード手法
-
CDN 統合による地理的キャッシュ
-
キャッシュの圧縮と最適化
-
セキュリティに関する考慮事項とキャッシュポイズニングの防止
-
負荷テストとパフォーマンスのベンチマーク
-
本番展開と DevOps の統合
-
高トラフィック アプリケーションからの実際のケーススタディ
各セクションには、開発者が高パフォーマンスでスケーラブルな Web アプリケーションを構築するためのキャッシュを習得するのに役立つ、完全な本番環境に対応したコード例、ベスト プラクティス、一般的な落とし穴、および代替アプローチが含まれています。
-
Redis HINCRBYFLOAT –ハッシュ値のフィールドに格納されている浮動小数点数をインクリメントする方法
このチュートリアルでは、redisデータストアのキーに格納されているハッシュ値内のフィールドに格納されている浮動小数点数をインクリメントする方法について学習します。このために、コマンドを使用します– HINCRBYFLOAT redis-cliで。 このコマンドは、キーに格納されているハッシュ値の指定されたフィールドに格納されている浮動小数点数を指定された値だけインクリメントするために使用されます(インクリメント )。指定されたフィールドがハッシュ値に存在しない場合は、指定された増分で追加されます。 その値として。キーが存在しない場合は、指定されたフィールドを唯一のメンバーとして新
-
Redis GEOHASH –地理空間値の複数のメンバーのジオハッシュ文字列を取得する方法
このチュートリアルでは、キーに格納されている地理空間値の1つ以上の要素のジオハッシュ文字列を取得する方法について学習します。このために、Redis GEOHASHを使用します コマンド。 GEOHASHコマンド このコマンドは、キーに格納されている地理空間値の1つ以上の指定された要素の有効なジオハッシュ文字列を返すために使用されます。地理空間値は、GEOADDコマンドを使用して入力された並べ替えられた設定値で表されます。 Redisは、ジオハッシュ手法のバリエーションを使用して地理空間要素の位置(経度、緯度)を表します。この手法では、緯度と経度のビットをインターリーブして、一意の52ビ