缓存策略(内存 + 分布式)
大约 9 分钟约 2729 字
缓存策略(内存 + 分布式)
简介
缓存是提升应用性能最有效的手段之一。ASP.NET Core 提供了内存缓存(IMemoryCache)和分布式缓存(IDistributedCache)两种方案。内存缓存适合单机场景,分布式缓存适合多实例部署。掌握缓存策略、过期机制和缓存穿透/雪崩防护,可以显著降低数据库压力。
特点
内存缓存
IMemoryCache
/// <summary>
/// 内存缓存基础用法
/// </summary>
// 注册服务
builder.Services.AddMemoryCache();
// 注入使用
public class UserService
{
private readonly IMemoryCache _cache;
public UserService(IMemoryCache cache)
{
_cache = cache;
}
public User? GetUser(int id)
{
string key = $"user:{id}";
// GetOrCreate — 获取或创建缓存
return _cache.GetOrCreate(key, entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
entry.SlidingExpiration = TimeSpan.FromMinutes(10);
entry.RegisterPostEvictionCallback((key, value, reason, state) =>
{
Console.WriteLine($"缓存被移除:{key},原因:{reason}");
});
return _db.Users.Find(id);
});
}
// 手动控制
public void SetUser(User user)
{
var options = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromHours(1))
.SetSlidingExpiration(TimeSpan.FromMinutes(15))
.SetPriority(CacheItemPriority.High)
.SetSize(1);
_cache.Set($"user:{user.Id}", user, options);
}
public void RemoveUser(int id) => _cache.Remove($"user:{id}");
}缓存策略模式
/// <summary>
/// 常见缓存策略
/// </summary>
// 1. Cache-Aside(旁路缓存)— 最常用
public async Task<Product?> GetProductAsync(int id)
{
var key = $"product:{id}";
if (!_cache.TryGetValue(key, out Product? product))
{
product = await _db.Products.FindAsync(id);
if (product != null)
{
_cache.Set(key, product, TimeSpan.FromMinutes(30));
}
}
return product;
}
// 2. 防止缓存穿透 — 缓存空值
public async Task<User?> GetUserSafeAsync(int id)
{
var key = $"user:safe:{id}";
return await _cache.GetOrCreateAsync(key, async entry =>
{
var user = await _db.Users.FindAsync(id);
if (user == null)
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); // 空值短缓存
}
else
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
}
return user;
});
}
// 3. 防止缓存雪崩 — 随机过期
public void SetWithJitter(string key, object value, TimeSpan baseExpiration)
{
var jitter = TimeSpan.FromSeconds(Random.Shared.Next(0, 120));
_cache.Set(key, value, baseExpiration + jitter);
}分布式缓存
Redis 分布式缓存
/// <summary>
/// IDistributedCache + Redis
/// </summary>
// 注册 Redis 分布式缓存
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
options.InstanceName = "MyApp:";
});
// 使用
public class ProductService
{
private readonly IDistributedCache _cache;
public ProductService(IDistributedCache cache)
{
_cache = cache;
}
public async Task<Product?> GetAsync(int id)
{
var key = $"product:{id}";
var bytes = await _cache.GetAsync(key);
if (bytes != null)
{
return JsonSerializer.Deserialize<Product>(bytes);
}
var product = await _db.Products.FindAsync(id);
if (product != null)
{
var json = JsonSerializer.SerializeToUtf8Bytes(product);
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
SlidingExpiration = TimeSpan.FromMinutes(10)
};
await _cache.SetAsync(key, json, options);
}
return product;
}
}缓存帮助类
/// <summary>
/// 分布式缓存扩展方法
/// </summary>
public static class DistributedCacheExtensions
{
public static async Task<T?> GetOrCreateAsync<T>(
this IDistributedCache cache, string key,
Func<Task<T>> factory, TimeSpan? expiration = null)
{
var bytes = await cache.GetAsync(key);
if (bytes != null)
{
return JsonSerializer.Deserialize<T>(bytes);
}
var value = await factory();
if (value != null)
{
var json = JsonSerializer.SerializeToUtf8Bytes(value);
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromMinutes(30)
};
await cache.SetAsync(key, json, options);
}
return value;
}
}
// 使用
var product = await _cache.GetOrCreateAsync($"product:{id}",
() => _db.Products.FindAsync(id), TimeSpan.FromHours(1));ResponseCache
HTTP 响应缓存
/// <summary>
/// ResponseCache — 服务端响应缓存
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
// 服务端缓存(独占),Duration 秒
[ResponseCache(Duration = 60, VaryByQueryKeys = new[] { "category" })]
[HttpGet]
public async Task<List<Product>> GetProducts(string? category)
{
return await _productService.GetAllAsync(category);
}
// 客户端缓存(通过 Cache-Control 头)
[ResponseCache(Duration = 300, Location = ResponseCacheLocation.Client)]
[HttpGet("{id}")]
public async Task<Product> GetProduct(int id)
{
return await _productService.GetByIdAsync(id);
}
// 禁用缓存
[ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
[HttpGet("realtime")]
public async Task<object> GetRealtimeData()
{
return await _realtimeService.GetLatestAsync();
}
}缓存高级模式
多级缓存策略
/// <summary>
/// 多级缓存 — L1 内存缓存 + L2 Redis 分布式缓存
/// </summary>
public class MultiLevelCacheService
{
private readonly IMemoryCache _memoryCache;
private readonly IDistributedCache _distributedCache;
private readonly ILogger<MultiLevelCacheService> _logger;
public MultiLevelCacheService(
IMemoryCache memoryCache,
IDistributedCache distributedCache,
ILogger<MultiLevelCacheService> logger)
{
_memoryCache = memoryCache;
_distributedCache = distributedCache;
_logger = logger;
}
public async Task<T?> GetAsync<T>(string key, Func<Task<T>> factory,
TimeSpan? memoryExpiration = null,
TimeSpan? distributedExpiration = null)
{
var memExpiry = memoryExpiration ?? TimeSpan.FromSeconds(60);
var distExpiry = distributedExpiration ?? TimeSpan.FromMinutes(30);
// L1:内存缓存
if (_memoryCache.TryGetValue(key, out T? cached))
{
_logger.LogDebug("L1 cache hit: {Key}", key);
return cached;
}
// L2:分布式缓存
var bytes = await _distributedCache.GetAsync(key);
if (bytes != null)
{
_logger.LogDebug("L2 cache hit: {Key}", key);
var value = JsonSerializer.Deserialize<T>(bytes);
_memoryCache.Set(key, value, memExpiry); // 回填 L1
return value;
}
// 回源加载
_logger.LogInformation("Cache miss: {Key}, loading from source", key);
var result = await factory();
if (result != null)
{
// 回填 L1 和 L2
_memoryCache.Set(key, result, memExpiry);
var json = JsonSerializer.SerializeToUtf8Bytes(result);
await _distributedCache.SetAsync(key, json, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = distExpiry
});
}
return result;
}
// 主动失效
public async Task RemoveAsync(string key)
{
_memoryCache.Remove(key);
await _distributedCache.RemoveAsync(key);
_logger.LogInformation("Cache invalidated: {Key}", key);
}
}缓存预热
/// <summary>
/// 缓存预热 — 服务启动时加载热点数据
/// </summary>
public class CacheWarmupService : IHostedService
{
private readonly MultiLevelCacheService _cache;
private readonly IProductRepository _repository;
private readonly ILogger<CacheWarmupService> _logger;
public CacheWarmupService(
MultiLevelCacheService cache,
IProductRepository repository,
ILogger<CacheWarmupService> logger)
{
_cache = cache;
_repository = repository;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("开始缓存预热...");
try
{
// 预加载热门分类
var categories = await _repository.GetTopCategoriesAsync(20);
foreach (var category in categories)
{
await _cache.GetAsync($"category:{category.Id}",
() => _repository.GetCategoryWithProductsAsync(category.Id));
}
// 预加载系统配置
var configs = await _repository.GetAllConfigsAsync();
foreach (var config in configs)
{
await _cache.GetAsync($"config:{config.Key}",
() => Task.FromResult(config.Value));
}
_logger.LogInformation("缓存预热完成,共加载 {Count} 项", categories.Count + configs.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "缓存预热失败");
// 预热失败不应阻止服务启动
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
// 注册
builder.Services.AddHostedService<CacheWarmupService>();缓存一致性策略
/// <summary>
/// 缓存一致性 — 数据更新时同步失效缓存
/// </summary>
public class ProductServiceWithCache
{
private readonly MultiLevelCacheService _cache;
private readonly AppDbContext _db;
public ProductServiceWithCache(MultiLevelCacheService cache, AppDbContext db)
{
_cache = cache;
_db = db;
}
public async Task<Product?> GetByIdAsync(int id)
{
return await _cache.GetAsync($"product:{id}",
() => _db.Products.FindAsync(id).AsTask());
}
public async Task<Product> UpdateAsync(int id, UpdateProductRequest request)
{
var product = await _db.Products.FindAsync(id)
?? throw new NotFoundException("Product", id);
product.Name = request.Name;
product.Price = request.Price;
await _db.SaveChangesAsync();
// 更新后失效相关缓存
await _cache.RemoveAsync($"product:{id}");
await _cache.RemoveAsync("products:all");
await _cache.RemoveAsync($"products:category:{product.CategoryId}");
return product;
}
public async Task DeleteAsync(int id)
{
var product = await _db.Products.FindAsync(id)
?? throw new NotFoundException("Product", id);
_db.Products.Remove(product);
await _db.SaveChangesAsync();
// 删除后失效相关缓存
await _cache.RemoveAsync($"product:{id}");
await _cache.RemoveAsync("products:all");
await _cache.RemoveAsync($"products:category:{product.CategoryId}");
}
}
// 基于消息的缓存失效(跨服务场景)
public class CacheInvalidationConsumer : IHostedService
{
private readonly MultiLevelCacheService _cache;
private readonly ISubscriber _subscriber; // Redis Pub/Sub
public async Task StartAsync(CancellationToken cancellationToken)
{
await _subscriber.SubscribeAsync("cache:invalidate", (channel, message) =>
{
var key = message.ToString();
_logger.LogInformation("Received cache invalidation: {Key}", key);
_cache.RemoveAsync(key).GetAwaiter().GetResult();
});
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}内存缓存高级配置
// 内存缓存容量控制
builder.Services.AddMemoryCache(options =>
{
options.SizeLimit = 100 * 1024 * 1024; // 100MB 缓存上限
options.CompactionPercentage = 0.25; // 压缩 25% 空间
options.ExpirationScanFrequency = TimeSpan.FromMinutes(5);
});
// 带大小控制的缓存条目
public class CachedUserService
{
private readonly IMemoryCache _cache;
public CachedUserService(IMemoryCache cache)
{
_cache = cache;
}
public User? GetUser(int id)
{
var options = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
SlidingExpiration = TimeSpan.FromMinutes(10),
Size = 1024, // 每个条目占 1KB
Priority = CacheItemPriority.Normal
};
// 注册缓存被移除时的回调
options.RegisterPostEvictionCallback((key, value, reason, state) =>
{
Console.WriteLine($"Cache evicted: {key}, Reason: {reason}");
});
return _cache.GetOrCreate($"user:{id}", entry =>
{
entry.SetOptions(options);
return _db.Users.Find(id);
});
}
}
// 缓存优先级
// CacheItemPriority.Low — 最先被淘汰
// CacheItemPriority.Normal — 默认
// CacheItemPriority.High — 优先保留
// CacheItemPriority.NeverRemove — 除非过期否则不移除分布式缓存连接池优化
// Redis 连接优化
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379,allowAdmin=true,connectTimeout=5000,syncTimeout=5000";
options.InstanceName = "MyApp:";
// 连接池配置(通过 ConnectionMultiplexer)
// 默认使用 StackExchange.Redis 内部连接池
// 单个连接多路复用,无需手动管理连接池
});
// 使用 ConnectionMultiplexer 自定义配置
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var config = ConfigurationOptions.Parse("localhost:6379");
config.AbortOnConnectFail = false; // 连接失败不中止
config.ConnectTimeout = 5000; // 连接超时 5 秒
config.SyncTimeout = 5000; // 同步操作超时 5 秒
config.AsyncTimeout = 5000; // 异步操作超时 5 秒
config.ConnectRetry = 3; // 重试次数
config.ReconnectRetryPolicy = new ExponentialRetry(5000); // 断线重连
return ConnectionMultiplexer.Connect(config);
});
// 缓存 Key 设计规范
// ✅ 好的 Key 设计
// product:123
// user:456:profile
// orders:user:789:page:1
// config:site:theme
// ❌ 避免的 Key 设计
// 随机 UUID 作为 Key(无法主动失效)
// 超长字符串作为 Key(浪费内存)
// 嵌套层级过深(超过 3 层)优点
缺点
总结
缓存选择:单机用 IMemoryCache,多实例用 Redis 分布式缓存。核心策略:Cache-Aside 最常用,空值缓存防穿透,随机过期防雪崩。ResponseCache 适合 API 响应缓存。缓存不是银弹,需要根据业务场景选择合适的过期时间和淘汰策略。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 缓存与开关类主题都在处理“配置/数据与运行时行为之间的解耦”。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
- 明确 Key 设计、过期策略、回源逻辑和降级方案。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 只加缓存,不设计失效与一致性策略。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 继续补齐多级缓存、缓存预热、分布式缓存治理和旗标管理平台。
适用场景
- 当你准备把《缓存策略(内存 + 分布式)》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《缓存策略(内存 + 分布式)》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《缓存策略(内存 + 分布式)》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《缓存策略(内存 + 分布式)》最大的收益和代价分别是什么?
