缓存设计模式
大约 15 分钟约 4580 字
缓存设计模式
简介
缓存是提升系统性能的核心手段之一。通过在数据访问路径上增加高速存储层,减少对慢速数据源(数据库、API、文件系统)的访问次数。缓存设计模式定义了数据在缓存和数据源之间的读写策略,不同的模式适用于不同的业务场景。本文将系统讲解 Cache-Aside、Read-Through、Write-Through、Write-Behind、Refresh-Ahead 等核心模式,以及在 .NET 中使用 IMemoryCache 和 Redis 实现多级缓存的实践方案。
特点
缓存读写模式
Cache-Aside(旁路缓存)
/// <summary>
/// Cache-Aside — 最常用的缓存模式
/// 应用程序负责同时管理缓存和数据源
///
/// 读流程:
/// 1. 先查缓存
/// 2. 缓存命中 → 直接返回
/// 3. 缓存未命中 → 查数据源 → 写入缓存 → 返回
///
/// 写流程:
/// 1. 先更新数据源
/// 2. 再删除(或更新)缓存
/// </summary>
public class CacheAsidePattern
{
private readonly IMemoryCache _cache;
private readonly DbContext _dbContext;
private readonly TimeSpan _defaultExpiration = TimeSpan.FromMinutes(30);
public CacheAsidePattern(IMemoryCache cache, DbContext dbContext)
{
_cache = cache;
_dbContext = dbContext;
}
/// <summary>
/// Cache-Aside 读操作
/// </summary>
public async Task<Product?> GetProductAsync(int productId)
{
string cacheKey = $"product:{productId}";
// 1. 先查缓存
if (_cache.TryGetValue(cacheKey, out Product? cached))
{
return cached;
}
// 2. 缓存未命中,查数据库
var product = await _dbContext.Products
.FirstOrDefaultAsync(p => p.Id == productId);
// 3. 写入缓存(仅当数据存在时)
if (product != null)
{
var options = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(_defaultExpiration)
.SetSlidingExpiration(TimeSpan.FromMinutes(10))
.SetPriority(CacheItemPriority.Normal);
_cache.Set(cacheKey, product, options);
}
return product;
}
/// <summary>
/// Cache-Aside 写操作 — 先更新数据库,再删除缓存
/// </summary>
public async Task UpdateProductAsync(Product product)
{
// 1. 更新数据库
_dbContext.Products.Update(product);
await _dbContext.SaveChangesAsync();
// 2. 删除缓存(推荐删除而非更新)
string cacheKey = $"product:{product.Id}";
_cache.Remove(cacheKey);
}
/// <summary>
/// Cache-Aside 删除操作
/// </summary>
public async Task DeleteProductAsync(int productId)
{
// 1. 删除数据库记录
var product = await _dbContext.Products.FindAsync(productId);
if (product != null)
{
_dbContext.Products.Remove(product);
await _dbContext.SaveChangesAsync();
}
// 2. 删除缓存
_cache.Remove($"product:{productId}");
}
}Read-Through(读穿透)
/// <summary>
/// Read-Through — 缓存层自动从数据源加载
/// 应用程序只与缓存交互,缓存层负责从数据源读取
///
/// 优点:
/// - 应用代码简洁,不需要关心缓存逻辑
/// - 缓存策略集中管理
///
/// 缺点:
/// - 需要实现自定义的缓存提供者
/// </summary>
public class ReadThroughCacheProvider
{
private readonly IMemoryCache _cache;
private readonly Func<string, Task<object?>> _dataLoader;
private readonly TimeSpan _expiration;
public ReadThroughCacheProvider(
IMemoryCache cache,
Func<string, Task<object?>> dataLoader,
TimeSpan? expiration = null)
{
_cache = cache;
_dataLoader = dataLoader;
_expiration = expiration ?? TimeSpan.FromMinutes(30);
}
/// <summary>
/// Read-Through 读取 — 缓存未命中时自动加载
/// </summary>
public async Task<T?> GetAsync<T>(string key) where T : class
{
if (_cache.TryGetValue(key, out T? cached))
{
return cached;
}
// 缓存未命中,通过加载器从数据源读取
var data = await _dataLoader(key);
if (data is T result)
{
var options = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(_expiration);
_cache.Set(key, result, options);
return result;
}
return null;
}
}
/// <summary>
/// 使用 Read-Through 的服务层
/// </summary>
public class ProductReadThroughService
{
private readonly ReadThroughCacheProvider _cacheProvider;
public ProductReadThroughService(IMemoryCache cache, DbContext dbContext)
{
_cacheProvider = new ReadThroughCacheProvider(
cache,
async key =>
{
// 从 key 解析实体ID,查询数据库
if (key.StartsWith("product:"))
{
int id = int.Parse(key["product:".Length..]);
return await dbContext.Products.FindAsync(id);
}
return null;
},
TimeSpan.FromMinutes(20));
}
public Task<Product?> GetProductAsync(int id)
{
return _cacheProvider.GetAsync<Product>($"product:{id}");
}
}Write-Through(写穿透)
/// <summary>
/// Write-Through — 写数据时同时写缓存和数据源
///
/// 流程:
/// 1. 更新缓存
/// 2. 同步更新数据源
///
/// 优点:
/// - 缓存和数据源始终一致
/// - 读性能高(数据一定在缓存中)
///
/// 缺点:
/// - 写延迟增加(需要同时写两个地方)
/// </summary>
public class WriteThroughCache
{
private readonly IMemoryCache _cache;
private readonly DbContext _dbContext;
public WriteThroughCache(IMemoryCache cache, DbContext dbContext)
{
_cache = cache;
_dbContext = dbContext;
}
/// <summary>
/// Write-Through 写操作
/// </summary>
public async Task SetProductAsync(Product product)
{
string cacheKey = $"product:{product.Id}";
// 1. 先更新数据库
var existing = await _dbContext.Products.FindAsync(product.Id);
if (existing != null)
{
_dbContext.Entry(existing).CurrentValues.SetValues(product);
}
else
{
_dbContext.Products.Add(product);
}
await _dbContext.SaveChangesAsync();
// 2. 同步更新缓存
var options = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(30));
_cache.Set(cacheKey, product, options);
}
/// <summary>
/// Write-Through 读操作(数据一定在缓存中)
/// </summary>
public Product? GetProduct(int productId)
{
return _cache.Get<Product>($"product:{productId}");
}
}Write-Behind(异步写回)
/// <summary>
/// Write-Behind(Write-Behind/Write-Back)
/// 先更新缓存,异步批量写入数据源
///
/// 流程:
/// 1. 更新缓存
/// 2. 将写入操作放入队列
/// 3. 后台线程批量刷新到数据源
///
/// 优点:
/// - 写性能极高(只写缓存)
/// - 支持合并写操作(同一个 key 多次更新只写一次)
///
/// 缺点:
/// - 数据可能短暂不一致
/// - 宕机可能丢失未持久化的数据
/// </summary>
public class WriteBehindCache : IDisposable
{
private readonly IMemoryCache _cache;
private readonly DbContext _dbContext;
private readonly ConcurrentQueue<WriteOperation> _writeQueue = new();
private readonly CancellationTokenSource _cts = new();
private readonly Task _backgroundFlushTask;
public WriteBehindCache(IMemoryCache cache, DbContext dbContext)
{
_cache = cache;
_dbContext = dbContext;
// 启动后台刷新任务
_backgroundFlushTask = Task.Run(BackgroundFlushAsync);
}
/// <summary>
/// Write-Behind 写操作 — 只更新缓存,异步写数据库
/// </summary>
public void SetProduct(Product product)
{
string cacheKey = $"product:{product.Id}";
// 1. 立即更新缓存
var options = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(30));
_cache.Set(cacheKey, product, options);
// 2. 入队等待异步写入
_writeQueue.Enqueue(new WriteOperation
{
Key = cacheKey,
Data = product,
Timestamp = DateTime.UtcNow
});
}
/// <summary>
/// 读操作 — 直接从缓存读取
/// </summary>
public Product? GetProduct(int productId)
{
return _cache.Get<Product>($"product:{productId}");
}
/// <summary>
/// 后台批量刷新
/// </summary>
private async Task BackgroundFlushAsync()
{
while (!_cts.IsCancellationRequested)
{
try
{
// 收集一批写操作
var batch = new List<WriteOperation>();
while (_writeQueue.TryDequeue(out var op) && batch.Count < 100)
{
// 合并相同 key 的多次更新(只保留最新的)
var existing = batch.FirstOrDefault(b => b.Key == op.Key);
if (existing != null)
{
batch.Remove(existing);
}
batch.Add(op);
}
if (batch.Count > 0)
{
// 批量写入数据库
foreach (var op in batch)
{
if (op.Data is Product product)
{
var existing = await _dbContext.Products.FindAsync(product.Id);
if (existing != null)
{
_dbContext.Entry(existing).CurrentValues.SetValues(product);
}
else
{
_dbContext.Products.Add(product);
}
}
}
await _dbContext.SaveChangesAsync();
}
await Task.Delay(1000, _cts.Token); // 每秒刷新一次
}
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
Console.WriteLine($"Write-Behind flush error: {ex.Message}");
await Task.Delay(5000, _cts.Token);
}
}
}
public void Dispose()
{
_cts.Cancel();
_backgroundFlushTask.Wait(TimeSpan.FromSeconds(10));
_cts.Dispose();
}
}
public class WriteOperation
{
public string Key { get; set; } = "";
public object Data { get; set; } = "";
public DateTime Timestamp { get; set; }
}Refresh-Ahead(提前刷新)
/// <summary>
/// Refresh-Ahead — 在缓存过期前自动刷新
///
/// 流程:
/// 1. 设置较短的 TTL
/// 2. 在 TTL 到达前(如剩余 20% 时间)触发后台刷新
/// 3. 用户始终命中缓存,不会感受到加载延迟
///
/// 适用场景:热点数据,不能容忍缓存未命中
/// </summary>
public class RefreshAheadCache : IDisposable
{
private readonly IMemoryCache _cache;
private readonly ConcurrentDictionary<string, CacheEntry> _entries = new();
private readonly Func<string, Task<object?>> _refresher;
private readonly TimeSpan _ttl;
private readonly double _refreshRatio = 0.8; // TTL 过 80% 时刷新
private readonly CancellationTokenSource _cts = new();
private Task? _refreshTask;
public RefreshAheadCache(
IMemoryCache cache,
Func<string, Task<object?>> refresher,
TimeSpan ttl)
{
_cache = cache;
_refresher = refresher;
_ttl = ttl;
_refreshTask = Task.Run(RefreshLoopAsync);
}
/// <summary>
/// 注册需要提前刷新的 key
/// </summary>
public void Register(string key)
{
_entries[key] = new CacheEntry
{
Key = key,
CreatedAt = DateTime.UtcNow,
LastRefreshed = DateTime.UtcNow
};
// 初始加载
_ = RefreshKeyAsync(key);
}
/// <summary>
/// 读取数据(始终从缓存获取)
/// </summary>
public T? Get<T>(string key) where T : class
{
return _cache.Get<T>(key);
}
private async Task RefreshLoopAsync()
{
while (!_cts.IsCancellationRequested)
{
try
{
var now = DateTime.UtcNow;
var keysToRefresh = _entries.Values
.Where(e => (now - e.LastRefreshed).TotalMilliseconds
> _ttl.TotalMilliseconds * _refreshRatio)
.Select(e => e.Key)
.ToList();
foreach (var key in keysToRefresh)
{
await RefreshKeyAsync(key);
}
await Task.Delay(5000, _cts.Token);
}
catch (OperationCanceledException) { break; }
catch { }
}
}
private async Task RefreshKeyAsync(string key)
{
try
{
var data = await _refresher(key);
if (data != null)
{
_cache.Set(key, data, _ttl);
if (_entries.TryGetValue(key, out var entry))
{
entry.LastRefreshed = DateTime.UtcNow;
}
}
}
catch { /* 刷新失败不影响现有缓存 */ }
}
public void Dispose()
{
_cts.Cancel();
_refreshTask?.Wait(TimeSpan.FromSeconds(5));
_cts.Dispose();
}
}
public class CacheEntry
{
public string Key { get; set; } = "";
public DateTime CreatedAt { get; set; }
public DateTime LastRefreshed { get; set; }
}分布式缓存(Redis)
StackExchange.Redis 封装
/// <summary>
/// Redis 分布式缓存封装
/// NuGet: Install-Package StackExchange.Redis
/// </summary>
public class RedisCacheService : IDisposable
{
private readonly ConnectionMultiplexer _redis;
private readonly IDatabase _db;
private readonly ISerializer _serializer;
public RedisCacheService(string connectionString, ISerializer serializer)
{
_redis = ConnectionMultiplexer.Connect(connectionString);
_db = _redis.GetDatabase();
_serializer = serializer;
}
/// <summary>
/// 设置缓存
/// </summary>
public async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null)
{
string json = _serializer.Serialize(value);
await _db.StringSetAsync(key, json, expiration ?? TimeSpan.FromMinutes(30));
}
/// <summary>
/// 获取缓存
/// </summary>
public async Task<T?> GetAsync<T>(string key) where T : class
{
RedisValue value = await _db.StringGetAsync(key);
if (value.IsNullOrEmpty)
return null;
return _serializer.Deserialize<T>(value!);
}
/// <summary>
/// 删除缓存
/// </summary>
public async Task RemoveAsync(string key)
{
await _db.KeyDeleteAsync(key);
}
/// <summary>
/// 批量删除(按模式匹配)
/// </summary>
public async Task RemoveByPatternAsync(string pattern)
{
var endpoints = _redis.GetEndPoints();
var server = _redis.GetServer(endpoints[0]);
var keys = server.Keys(pattern: pattern);
foreach (var key in keys)
{
await _db.KeyDeleteAsync(key);
}
}
/// <summary>
/// 使用 Redis Hash 存储对象
/// </summary>
public async Task HashSetAsync(string key, Dictionary<string, string> fields, TimeSpan? expiration = null)
{
var entries = fields.Select(f => new HashEntry(f.Key, f.Value)).ToArray();
await _db.HashSetAsync(key, entries);
if (expiration.HasValue)
{
await _db.KeyExpireAsync(key, expiration);
}
}
/// <summary>
/// 获取 Hash 字段
/// </summary>
public async Task<string?> HashGetAsync(string key, string field)
{
var value = await _db.HashGetAsync(key, field);
return value.IsNullOrEmpty ? null : value.ToString();
}
/// <summary>
/// 分布式锁
/// </summary>
public async Task<bool> AcquireLockAsync(string key, TimeSpan expiry)
{
return await _db.StringSetAsync(
$"lock:{key}",
Environment.MachineName,
expiry,
When.NotExists);
}
public async Task ReleaseLockAsync(string key)
{
await _db.KeyDeleteAsync($"lock:{key}");
}
public void Dispose()
{
_redis?.Dispose();
}
}
public interface ISerializer
{
string Serialize<T>(T obj);
T Deserialize<T>(string data);
}
public class SystemTextJsonSerializer : ISerializer
{
private readonly JsonSerializerOptions _options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public string Serialize<T>(T obj) => JsonSerializer.Serialize(obj, _options);
public T Deserialize<T>(string data) => JsonSerializer.Deserialize<T>(data, _options)!;
}多级缓存
L1 本地 + L2 分布式
/// <summary>
/// 多级缓存 — L1 本地内存 + L2 Redis
///
/// 读取顺序:
/// L1 (IMemoryCache) → L2 (Redis) → 数据源
///
/// 写入策略:
/// 同时写入 L1 和 L2,删除时同时清除
/// </summary>
public class MultiLevelCacheService
{
private readonly IMemoryCache _l1Cache;
private readonly RedisCacheService _l2Cache;
private readonly TimeSpan _l1Expiration;
private readonly TimeSpan _l2Expiration;
public MultiLevelCacheService(
IMemoryCache l1Cache,
RedisCacheService l2Cache,
TimeSpan? l1Expiration = null,
TimeSpan? l2Expiration = null)
{
_l1Cache = l1Cache;
_l2Cache = l2Cache;
_l1Expiration = l1Expiration ?? TimeSpan.FromMinutes(5);
_l2Expiration = l2Expiration ?? TimeSpan.FromMinutes(30);
}
/// <summary>
/// 多级缓存读取
/// </summary>
public async Task<T?> GetAsync<T>(string key) where T : class
{
// L1 本地缓存
if (_l1Cache.TryGetValue(key, out T? l1Value))
{
return l1Value;
}
// L2 Redis 缓存
var l2Value = await _l2Cache.GetAsync<T>(key);
if (l2Value != null)
{
// 回填 L1
_l1Cache.Set(key, l2Value, _l1Expiration);
return l2Value;
}
return null;
}
/// <summary>
/// 多级缓存写入
/// </summary>
public async Task SetAsync<T>(string key, T value) where T : class
{
// 写入 L1
_l1Cache.Set(key, value, _l1Expiration);
// 写入 L2
await _l2Cache.SetAsync(key, value, _l2Expiration);
}
/// <summary>
/// 多级缓存删除
/// </summary>
public async Task RemoveAsync(string key)
{
// 删除 L1
_l1Cache.Remove(key);
// 删除 L2
await _l2Cache.RemoveAsync(key);
}
/// <summary>
/// 带数据加载器的多级缓存读取(Cache-Aside 模式)
/// </summary>
public async Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> loader) where T : class
{
// L1
if (_l1Cache.TryGetValue(key, out T? l1Value))
{
return l1Value;
}
// L2
var l2Value = await _l2Cache.GetAsync<T>(key);
if (l2Value != null)
{
_l1Cache.Set(key, l2Value, _l1Expiration);
return l2Value;
}
// 数据源
var data = await loader();
// 回填 L1 和 L2
_l1Cache.Set(key, data, _l1Expiration);
await _l2Cache.SetAsync(key, data, _l2Expiration);
return data;
}
}缓存失效策略
缓存穿透、雪崩、击穿防御
/// <summary>
/// 缓存三大问题的防御方案
/// </summary>
public class CacheDefense
{
private readonly IMemoryCache _cache;
private readonly RedisCacheService _redis;
private readonly SemaphoreSlim _semaphore = new(1, 1);
public CacheDefense(IMemoryCache cache, RedisCacheService redis)
{
_cache = cache;
_redis = redis;
}
/// <summary>
/// 防缓存穿透 — 使用布隆过滤器 + 空值缓存
/// 穿透:查询不存在的数据,缓存永远不命中,请求直达数据库
/// </summary>
public async Task<Product?> GetWithAntiPenetrationAsync(int productId)
{
string cacheKey = $"product:{productId}";
string nullKey = $"null:{productId}";
// 检查空值标记
if (_cache.TryGetValue(nullKey, out _))
{
return null; // 标记为不存在的数据,直接返回
}
// 正常缓存查询
var product = await _redis.GetAsync<Product>(cacheKey);
if (product != null) return product;
// 查数据库
product = await LoadFromDbAsync(productId);
if (product != null)
{
await _redis.SetAsync(cacheKey, product, TimeSpan.FromMinutes(30));
}
else
{
// 缓存空值(短 TTL),防止穿透
_cache.Set(nullKey, true, TimeSpan.FromMinutes(5));
}
return product;
}
/// <summary>
/// 防缓存击穿 — 使用互斥锁
/// 击穿:热点 key 过期瞬间,大量并发请求直达数据库
/// </summary>
public async Task<Product?> GetWithAntiBreakdownAsync(int productId)
{
string cacheKey = $"product:{productId}";
// 先查缓存
var product = await _redis.GetAsync<Product>(cacheKey);
if (product != null) return product;
// 获取互斥锁,只允许一个请求加载数据
bool locked = await _redis.AcquireLockAsync(cacheKey, TimeSpan.FromSeconds(10));
if (locked)
{
try
{
// Double-check:获取锁后再查一次缓存
product = await _redis.GetAsync<Product>(cacheKey);
if (product != null) return product;
// 查数据库并写入缓存
product = await LoadFromDbAsync(productId);
if (product != null)
{
await _redis.SetAsync(cacheKey, product, TimeSpan.FromMinutes(30));
}
return product;
}
finally
{
await _redis.ReleaseLockAsync(cacheKey);
}
}
else
{
// 等待并重试
await Task.Delay(100);
return await GetWithAntiBreakdownAsync(productId);
}
}
/// <summary>
/// 防缓存雪崩 — 随机过期时间
/// 雪崩:大量 key 同时过期,请求瞬间压垮数据库
/// </summary>
public async Task SetWithAntiAvalancheAsync(string key, object value)
{
// 基础过期时间 + 随机偏移
var baseExpiration = TimeSpan.FromMinutes(30);
var randomOffset = TimeSpan.FromSeconds(Random.Shared.Next(0, 300)); // 0-5分钟随机
await _redis.SetAsync(key, value, baseExpiration + randomOffset);
}
private Task<Product?> LoadFromDbAsync(int productId)
{
// 模拟数据库查询
return Task.FromResult<Product?>(null);
}
}缓存预热
启动时预加载热点数据
/// <summary>
/// 缓存预热 — 应用启动时或定时加载热点数据到缓存
/// </summary>
public class CacheWarmupService : IHostedService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<CacheWarmupService> _logger;
public CacheWarmupService(
IServiceProvider serviceProvider,
ILogger<CacheWarmupService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("开始缓存预热...");
using var scope = _serviceProvider.CreateScope();
var redis = scope.ServiceProvider.GetRequiredService<RedisCacheService>();
var dbContext = scope.ServiceProvider.GetRequiredService<DbContext>();
// 预热热点商品
var hotProducts = await dbContext.Set<Product>()
.Where(p => p.IsHot)
.Take(100)
.ToListAsync(cancellationToken);
foreach (var product in hotProducts)
{
string key = $"product:{product.Id}";
await redis.SetAsync(key, product, TimeSpan.FromHours(2));
}
_logger.LogInformation("缓存预热完成,已加载 {Count} 个热点商品", hotProducts.Count);
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
/// <summary>
/// 定时刷新缓存的后台服务
/// </summary>
public class CacheRefreshService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly TimeSpan _refreshInterval = TimeSpan.FromMinutes(10);
public CacheRefreshService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await RefreshHotDataAsync(stoppingToken);
}
catch (Exception ex)
{
Console.WriteLine($"缓存刷新失败: {ex.Message}");
}
await Task.Delay(_refreshInterval, stoppingToken);
}
}
private async Task RefreshHotDataAsync(CancellationToken ct)
{
using var scope = _serviceProvider.CreateScope();
var redis = scope.ServiceProvider.GetRequiredService<RedisCacheService>();
var dbContext = scope.ServiceProvider.GetRequiredService<DbContext>();
// 刷新需要定时更新的数据(如排行榜)
var ranking = await dbContext.Set<Product>()
.OrderByDescending(p => p.SalesCount)
.Take(50)
.Select(p => new { p.Id, p.Name, p.SalesCount })
.ToListAsync(ct);
await redis.SetAsync("ranking:top50", ranking, TimeSpan.FromMinutes(30));
}
}缓存监控
缓存命中率监控
/// <summary>
/// 缓存监控指标
/// </summary>
public class CacheMetrics
{
private long _totalRequests;
private long _cacheHits;
private long _cacheMisses;
private long _evictions;
public void RecordHit() => Interlocked.Increment(ref _cacheHits);
public void RecordMiss() => Interlocked.Increment(ref _cacheMisses);
public void RecordEviction() => Interlocked.Increment(ref _evictions);
public double HitRate =>
(_totalRequests = _cacheHits + _cacheMisses) > 0
? (double)_cacheHits / _totalRequests * 100
: 0;
public long TotalRequests => Volatile.Read(ref _totalRequests);
public long Hits => Volatile.Read(ref _cacheHits);
public long Misses => Volatile.Read(ref _cacheMisses);
public long Evictions => Volatile.Read(ref _evictions);
public override string ToString()
=> $"命中率: {HitRate:F1}% | 命中: {Hits} | 未命中: {Misses} | 驱逐: {Evictions}";
}
/// <summary>
/// 带监控的缓存装饰器
/// </summary>
public class MonitoredCacheService
{
private readonly RedisCacheService _inner;
public CacheMetrics Metrics { get; } = new();
public MonitoredCacheService(RedisCacheService inner)
{
_inner = inner;
}
public async Task<T?> GetAsync<T>(string key) where T : class
{
var result = await _inner.GetAsync<T>(key);
if (result != null)
Metrics.RecordHit();
else
Metrics.RecordMiss();
return result;
}
public Task SetAsync<T>(string key, T value, TimeSpan? expiration = null)
{
return _inner.SetAsync(key, value, expiration);
}
public Task RemoveAsync(string key)
{
Metrics.RecordEviction();
return _inner.RemoveAsync(key);
}
}优点
缺点
性能注意事项
总结
缓存设计模式是系统性能优化的核心手段。Cache-Aside 是最通用的模式;Read-Through 简化了应用代码;Write-Through 保证数据一致性;Write-Behind 提升写性能;Refresh-Ahead 消除读延迟。多级缓存(L1 本地 + L2 分布式)是生产环境的推荐方案。同时必须防御缓存穿透、雪崩和击穿三大问题。
关键知识点
- Cache-Aside:应用自行管理缓存读写,最灵活最常用
- Read-Through:缓存层自动加载数据,应用代码更简洁
- Write-Through:同步写缓存和数据源,一致性最强
- Write-Behind:异步写回,写性能最高但有数据丢失风险
- 多级缓存 L1+L2 平衡了性能和一致性
- 缓存穿透用空值缓存/布隆过滤器防御
- 缓存击穿用互斥锁防御
- 缓存雪崩用随机过期时间防御
常见误区
- 误区:缓存能解决所有性能问题
纠正:缓存只解决读性能问题,写性能需要其他方案 - 误区:缓存一定比直接查数据库快
纠正:对于极高频变更的数据,缓存可能反而增加开销 - 误区:TTL 越长越好
纠正:TTL 过长导致数据不一致,需要根据业务容忍度设定 - 误区:所有数据都需要缓存
纠正:只缓存访问频繁且变更不频繁的数据
进阶路线
- 初级:使用 IMemoryCache 实现 Cache-Aside 模式
- 中级:集成 Redis 分布式缓存,实现多级缓存
- 高级:实现 Write-Behind、Refresh-Ahead,防御三大缓存问题
- 专家级:缓存编排平台,自动预热、监控告警、动态策略调整
适用场景
- 电商商品详情页(读多写少)
- 用户会话管理
- 热点数据排行榜
- 配置信息缓存
- API 响应缓存
- 数据库查询结果缓存
落地建议
- 项目初期即引入 IMemoryCache 作为本地缓存
- 数据量增长后引入 Redis 作为分布式缓存
- 建立缓存 key 命名规范:
{业务域}:{实体}:{ID} - 监控缓存命中率,目标 > 80%
- 缓存预热作为应用启动的标配步骤
- 制定缓存降级方案(缓存不可用时直接查数据库)
排错清单
复盘问题
- Cache-Aside 模式下,先删缓存再更新数据库有什么风险?
- 如何在不停服的情况下切换缓存实现(Memory → Redis)?
- 分布式环境下如何保证多个节点的本地缓存一致性?
- 缓存命中率突然下降,可能的原因有哪些?
- 如何设计一个支持动态调整 TTL 的缓存系统?
- Write-Behind 模式下如何处理数据库写入失败?
- 如何评估一个查询是否适合加入缓存?
- 大量 key 同时过期的告警如何设计?
