学习新思路
趣事:
最近有人留言,如何在在Net Core中使用Redis,同时高性能使用,并且完美结合呢?
今天开一篇新文章,聊说一下.Net Core+Redis简单应用,其实整体简单。通过一个商品例子来说。
直接开干!
01
第1章:项目背景与技术选型
1.1 业务场景:高频商品目录查询的性能挑战
在现代电商平台中,商品目录(Product Catalog)是被访问最频繁的数据之一。用户浏览首页、分类页、搜索结果页等场景都会触发商品信息的查询。假设一个中等规模的电商平台,日均商品目录相关请求量可达数百万次。若每次请求都直接查询数据库,将带来以下问题:
- 数据库压力巨大
:大量重复的 SELECT 查询会迅速耗尽数据库连接池,导致响应延迟增加,甚至引发数据库崩溃。 - 响应速度慢
:数据库磁盘I/O和复杂查询逻辑导致平均响应时间可能在 50ms~200ms 以上,影响用户体验。 - 横向扩展困难
:单纯增加数据库读副本成本高昂,且存在主从延迟问题。
解决方案:引入 Redis 缓存,将热点数据(如商品名称、价格、库存状态、分类信息等)存储在内存中,使高频查询直接命中缓存,从而:
-
将响应时间从 ~100ms降低至~1-5ms。 -
减少数据库负载 80% 以上。 -
提升系统整体吞吐量和可扩展性。
02
1.2 技术栈选型
- 后端框架
:.NET Core 6+ Web API -
跨平台、高性能、现代化的Web开发框架。 -
内置依赖注入、配置管理、中间件等优秀特性。 - 缓存中间件
:Redis -
开源、高性能的内存数据结构存储,支持字符串、哈希、列表、集合等多种数据结构。 -
支持持久化、主从复制、集群模式,适合生产环境。 -
与 .NET 生态集成良好,有成熟的客户端库 StackExchange.Redis和Microsoft.Extensions.Caching.StackExchangeRedis。 - 数据存储
:SQL Server / MySQL / PostgreSQL (以 SQL Server 为例) - 开发工具
:Visual Studio 2022 / VS Code
03
1.3 本章小结
本章我们明确了在 .NET Core Web API 项目中为商品目录引入 Redis 缓存的必要性,并完成了技术栈的选型。接下来,我们将搭建开发环境,为实战做好准备。
04
第2章:环境搭建与项目初始化
本章将带领您完成项目的初始环境配置和基础代码搭建。
2.1 安装与启动 Redis
Windows 用户:
-
推荐使用 Redis for Windows 的 MSI 安装包。 -
安装完成后,Redis 服务会自动在后台运行,默认端口 6379。 -
打开命令提示符,输入 redis-cli ping,若返回PONG,则表示 Redis 服务正常。 macOS 用户:
-
使用 Homebrew 安装: brew install redis -
启动服务: brew services start redis -
验证: redis-cli ping应返回PONG Docker 用户(推荐用于开发):
docker run -d -p 6379:6379 --name myredis redis:alpine
05
2.2 创建 .NET Core Web API 项目
打开终端,执行以下命令:
dotnet new webapi -n ProductCatalogApicd ProductCatalogApi
06
2.3 安装 Redis NuGet 包
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
这个包是微软官方提供的 Redis 缓存扩展,与 ASP.NET Core 的 IDistributedCache 接口无缝集成。
07
2.4 配置 Redis 连接
打开 appsettings.json,添加 Redis 配置:
{"Logging": {"LogLevel": {"Default": "Information","Microsoft.AspNetCore": "Warning"}},"AllowedHosts": "*","Redis": {"Configuration": "localhost:6379", // 如果使用 Docker,可能需要 "host.docker.internal:6379""InstanceName": "ProductCatalogCache"}}
注意:生产环境应使用更安全的连接字符串(如密码、SSL),并考虑使用 Azure Cache for Redis 或 AWS ElastiCache。
08
2.5 在 Program.cs 中注册 Redis 服务
打开 Program.cs,在 var builder = WebApplication.CreateBuilder(args); 之后,添加 Redis 服务注册:
using Microsoft.Extensions.Caching.StackExchangeRedis;var builder = WebApplication.CreateBuilder(args);// 添加 Redis 缓存服务builder.Services.AddStackExchangeRedisCache(options =>{options.Configuration = builder.Configuration.GetConnectionString("Redis:Configuration")?? builder.Configuration["Redis:Configuration"];options.InstanceName = builder.Configuration["Redis:InstanceName"];});// ... 其他服务注册 (如 Swagger, Controllers)builder.Services.AddControllers();var app = builder.Build();// ... 中间件配置app.UseAuthorization();app.MapControllers();app.Run();
09
2.6 创建商品实体与模拟数据层
创建 Models/Product.cs:
public class Product{public int Id { get; set; }public string Name { get; set; } = string.Empty;public decimal Price { get; set; }public string Category { get; set; } = string.Empty;public bool IsAvailable { get; set; }}
创建 Data/FakeProductDbContext.cs(模拟数据库):
public class FakeProductDbContext{private static readonly List<Product> _products = new(){new Product { Id = 1, Name = "Laptop", Price = 999.99m, Category = "Electronics", IsAvailable = true },new Product { Id = 2, Name = "Coffee Mug", Price = 12.50m, Category = "Home", IsAvailable = true },new Product { Id = 3, Name = "Running Shoes", Price = 89.99m, Category = "Sports", IsAvailable = false },// 可以添加更多模拟数据};public List<Product> GetAllProducts(){// 模拟数据库查询延迟Task.Delay(100).Wait(); // 100ms 延迟return _products;}public Product? GetProductById(int id){Task.Delay(50).Wait(); // 50ms 延迟return _products.FirstOrDefault(p => p.Id == id);}}
10
2.7 本章小结
本章完成了以下工作:
-
✅ 成功安装并验证了 Redis 服务。 -
✅ 创建了 .NET Core Web API 项目。 -
✅ 安装了 Redis 客户端包并配置了连接。 -
✅ 注册了 IDistributedCache服务。 -
✅ 定义了商品模型和模拟数据层。
11
12
第3章:实现商品目录缓存逻辑
本章将实现核心的缓存读写逻辑,让商品目录查询优先从 Redis 缓存中获取数据。
13
3.1 创建 ProductsController
使用以下命令创建控制器(或手动在 Controllers 目录下创建文件):
dotnet new controller -name ProductsController --api
14
3.2 注入依赖服务
修改 Controllers/ProductsController.cs,注入 IDistributedCache 和 FakeProductDbContext:
using Microsoft.AspNetCore.Mvc;using Microsoft.Extensions.Caching.Distributed;using ProductCatalogApi.Data;using ProductCatalogApi.Models;namespace ProductCatalogApi.Controllers;[][]public class ProductsController : ControllerBase{private readonly IDistributedCache _cache;private readonly FakeProductDbContext _dbContext;private const string ALL_PRODUCTS_CACHE_KEY = "all_products_list";private static readonly TimeSpan CACHE_DURATION = TimeSpan.FromMinutes(10); // 10分钟滑动过期public ProductsController(IDistributedCache cache, FakeProductDbContext dbContext){_cache = cache;_dbContext = dbContext;}// ... 后续添加 Action 方法}
关键点:
IDistributedCache是 ASP.NET Core 提供的分布式缓存抽象接口,Redis 实现了它。 -
定义了缓存键 ALL_PRODUCTS_CACHE_KEY和缓存时长CACHE_DURATION。
15
3.3 实现 GetAllProducts 缓存逻辑
添加 GET /api/products 方法,实现“先查缓存,未命中再查数据库,并回填缓存”的经典模式:
[HttpGet]public async Task<ActionResult<IEnumerable<Product>>> Get(){// 1. 尝试从缓存获取数据var cachedData = await _cache.GetStringAsync(ALL_PRODUCTS_CACHE_KEY);if (!string.IsNullOrEmpty(cachedData)){// 缓存命中:直接反序列化并返回var products = System.Text.Json.JsonSerializer.Deserialize<List<Product>>(cachededData);return Ok(products);}// 2. 缓存未命中:查询数据库var productsFromDb = _dbContext.GetAllProducts();// 3. 将数据库结果序列化并写入缓存var jsonData = System.Text.Json.JsonSerializer.Serialize(productsFromDb);var options = new DistributedCacheEntryOptions().SetSlidingExpiration(CACHE_DURATION); // 滑动过期:每次访问重置过期时间await _cache.SetStringAsync(ALL_PRODUCTS_CACHE_KEY, jsonData, options);return Ok(productsFromDb);}
详细解释:
GetStringAsync:从 Redis 获取字符串值。Redis 存储的是字节流, String方法是常用封装。- 缓存穿透防护
:虽然简单判断 null或空字符串,但更完善的方案应缓存“空结果”以防止恶意攻击。 SetSlidingExpiration:滑动过期策略。如果数据在10分钟内被再次访问,过期时间将重新计算。适合热点数据。 - JSON 序列化
: System.Text.Json是 .NET 内置高性能序列化器,用于将List<Product>转为 JSON 字符串存储。
16
3.4 优化:避免重复序列化
我们可以创建一个帮助方法来减少重复代码:
private async Task<string?> GetFromCacheOrDatabaseAsync(string cacheKey,Func<List<Product>> databaseQuery,DistributedCacheEntryOptions options){var cached = await _cache.GetStringAsync(cacheKey);if (!string.IsNullOrEmpty(cached)) return cached;var data = databaseQuery();var json = JsonSerializer.Serialize(data);await _cache.SetStringAsync(cacheKey, json, options);return json;}
然后在 Get 方法中调用:
var jsonData = await GetFromCacheOrDatabaseAsync(ALL_PRODUCTS_CACHE_KEY,() => _dbContext.GetAllProducts(),new DistributedCacheEntryOptions().SetSlidingExpiration(CACHE_DURATION));var products = JsonSerializer.Deserialize<List<Product>>(jsonData!);return Ok(products);
17
3.5 测试缓存效果
-
启动项目: dotnet run -
使用 Swagger UI 或 curl访问GET /api/products - 第一次请求
:响应较慢(约 100ms+),因为需要查询数据库并写入缓存。 - 后续请求
:响应极快(几毫秒),数据直接来自 Redis 缓存。 -
可以使用 redis-cli连接 Redis,执行KEYS *查看缓存键,GET all_products_list查看缓存内容。
18
3.6 本章小结
本章实现了商品列表的缓存读取逻辑:
-
✅ 使用 IDistributedCache与 Redis 交互。 -
✅ 实现了“缓存-数据库”两级查询模式。 -
✅ 使用 JSON 序列化存储复杂对象。 -
✅ 应用了滑动过期策略提升缓存利用率。
19
20
第4章:精细化缓存与缓存更新策略
本章将深入探讨更复杂的缓存场景,包括单个商品缓存、缓存更新和高级防护策略。
21
4.1 实现单个商品缓存
高频查询不仅包括全量列表,也包括通过 ID 查询单个商品。我们需要为 GET /api/products/{id} 添加缓存。
方案一:独立键存储(推荐初学者)
每个商品使用独立的缓存键。
[]public async Task<ActionResult<Product>> Get(int id){var cacheKey = $"product_{id}";// 1. 尝试从缓存获取var cachedProduct = await _cache.GetStringAsync(cacheKey);if (!string.IsNullOrEmpty(cachedProduct)){var product = JsonSerializer.Deserialize<Product>(cachedProduct);return Ok(product);}// 2. 缓存未命中,查数据库var productFromDb = _dbContext.GetProductById(id);if (productFromDb == null) return NotFound();// 3. 回填缓存var jsonData = JsonSerializer.Serialize(productFromDb);var options = new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(15)); // 可设置不同过期时间await _cache.SetStringAsync(cacheKey, jsonData, options);return Ok(productFromDb);}
优点:简单直观,易于理解和维护。缺点:如果商品数量巨大,会产生大量独立的 key,管理不便。
22
方案二:使用 Redis 哈希(Hash)结构(推荐)
将同一类数据(如所有商品)存储在一个 Hash 中,field 为商品 ID,value 为商品 JSON。
// 在 ProductsController 中添加private const string PRODUCTS_HASH_KEY = "products_hash";[]public async Task<ActionResult<Product>> GetWithHash(int id){// 使用 Hash 结构读取var redisValue = await _cache.GetAndRefreshAsync(PRODUCTS_HASH_KEY, id.ToString());if (!redisValue.HasValue){var product = _dbContext.GetProductById(id);if (product == null) return NotFound();// 写入 Hash,并设置整个 Hash 的过期时间await _cache.SetAndRefreshAsync(PRODUCTS_HASH_KEY, id.ToString(),JsonSerializer.Serialize(product),TimeSpan.FromMinutes(20));return Ok(product);}var deserialized = JsonSerializer.Deserialize<Product>(redisValue!);return Ok(deserialized);}
优点:
减少 key 数量,便于管理。 支持对整个 Hash 设置统一过期时间。 可以使用 HGETALL批量获取,适合缓存预热。内存利用率更高。
注意:
GetAndRefreshAsync和SetAndRefreshAsync需要自行扩展或使用底层StackExchange.RedisAPI。
23
4.2 缓存更新与失效(Cache Invalidation)
当商品信息被修改时,必须确保缓存中的旧数据被清除或更新,否则用户会看到过期信息。
场景:更新商品信息
[]public IActionResult Put(int id, [FromBody] Product updatedProduct){var existing = _dbContext.GetProductById(id);if (existing == null) return NotFound();// 更新数据库// ... 更新逻辑// 失效缓存:删除相关缓存项_cache.Remove($"product_{id}"); // 删除单个商品缓存_cache.Remove(ALL_PRODUCTS_CACHE_KEY); // 删除全量列表缓存(下次请求会重建)// 或者如果是 Hash 结构:// _cache.HashDelete(PRODUCTS_HASH_KEY, id.ToString());return NoContent();}
策略选择:
- 删除(Invalidate)
:简单直接,下次查询时重建缓存。 - 更新(Update)
:直接将新数据写入缓存。需保证更新操作的原子性,避免脏数据。
推荐在写操作频繁不高时使用“删除”策略,避免缓存与数据库不一致的风险。
24
4.3 缓存预热(Cache Warming)
系统启动或大促前,主动将热点数据加载到缓存,避免冷启动时大量请求击穿缓存。
在 Program.cs 中添加预热逻辑:
var app = builder.Build();// 缓存预热using (var scope = app.Services.CreateScope()){var cache = scope.ServiceProvider.GetRequiredService<IDistributedCache>();var dbContext = scope.ServiceProvider.GetRequiredService<FakeProductDbContext>();var allProducts = dbContext.GetAllProducts();var json = JsonSerializer.Serialize(allProducts);await cache.SetStringAsync(ALL_PRODUCTS_CACHE_KEY, json,new DistributedCacheEntryOptions().SetSlidingExpiration(CACHE_DURATION));Console.WriteLine("✅ 缓存预热完成:商品目录已加载");}app.Run();
25
4.4 应对缓存异常情况
缓存穿透(Cache Penetration)
查询一个数据库中根本不存在的数据,每次都会击穿缓存。
解决方案:缓存空值或使用布隆过滤器(Bloom Filter)。
// 在 Get 方法中if (productFromDb == null){// 缓存空结果,防止穿透,但过期时间要短await _cache.SetStringAsync(cacheKey, "",new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(2)));return NotFound();}
缓存雪崩(Cache Avalanche)
大量缓存同时过期,导致瞬间大量请求涌向数据库。
解决方案:
-
设置随机过期时间: TimeSpan.FromMinutes(10 + new Random().Next(0, 10)) -
使用高可用 Redis 集群。 -
后端服务限流降级。
缓存击穿(Cache Breakdown)
某个热点 key 过期瞬间,大量并发请求同时击穿到数据库。
解决方案:
-
使用互斥锁(Mutex):第一个请求查询数据库并重建缓存,其他请求等待。 -
热点数据永不过期,后台定时异步刷新。
26
4.5 本章小结
本章深入探讨了缓存的高级应用:
-
✅ 实现了单个商品的精细化缓存。 -
✅ 引入了 Redis Hash 结构优化存储。 -
✅ 设计了合理的缓存更新与失效策略。 -
✅ 实现了缓存预热以提升启动性能。 -
✅ 讨论了缓存穿透、雪崩、击穿的防护措施。
27
28
第5章:监控、日志与性能测试
一个健壮的缓存系统必须具备良好的可观测性。本章将添加监控、日志,并通过性能测试验证缓存效果。
29
5.1 添加缓存命中率监控
缓存命中率是衡量缓存有效性的核心指标。我们可以使用 ILogger 记录关键信息。
在 ProductsController 中注入 ILogger
private readonly ILogger<ProductsController> _logger;// ... 其他依赖public ProductsController(IDistributedCache cache,FakeProductDbContext dbContext,ILogger<ProductsController> logger){_cache = cache;_dbContext = dbContext;_logger = logger;}
在 Get 方法中记录命中情况
[HttpGet]public async Task<ActionResult<IEnumerable<Product>>> Get(){_logger.LogInformation("收到商品列表查询请求");var cacheKey = ALL_PRODUCTS_CACHE_KEY;var cachedData = await _cache.GetStringAsync(cacheKey);if (!string.IsNullOrEmpty(cachedData)){_logger.LogInformation("缓存命中: {CacheKey}", cacheKey);var products = JsonSerializer.Deserialize<List<Product>>(cachedData);return Ok(products);}_logger.LogWarning("缓存未命中: {CacheKey},查询数据库", cacheKey);var productsFromDb = _dbContext.GetAllProducts();var jsonData = JsonSerializer.Serialize(productsFromDb);var options = new DistributedCacheEntryOptions().SetSlidingExpiration(CACHE_DURATION);await _cache.SetStringAsync(cacheKey, jsonData, options);_logger.LogInformation("数据库查询完成,已回填缓存: {CacheKey}", cacheKey);return Ok(productsFromDb);}
日志级别说明:
Information:常规操作。 Warning:未命中是“正常但值得关注”的事件,用 Warning便于监控工具捕获。Error:用于异常情况。
30
5.2 使用计数器监控命中率
更精确的做法是使用计数器(Counter)来统计命中与未命中次数。
添加私有字段:
private static long _cacheHits = 0;private static long _cacheMisses = 0;
在命中和未命中时递增:
if (!string.IsNullOrEmpty(cachedData)){Interlocked.Increment(ref _cacheHits); // 线程安全_logger.LogInformation("缓存命中... (累计命中: {_cacheHits})", _cacheHits);// ...}else{Interlocked.Increment(ref _cacheMisses);_logger.LogWarning("缓存未命中... (累计未命中: {_cacheMisses})", _cacheMisses);// ...}
注意:Interlocked 保证多线程环境下的原子性。生产环境建议使用 Prometheus + Grafana 或 Application Insights 等专业监控工具。
31
5.3 性能测试:对比缓存效果
我们将使用 wrk(一款高性能 HTTP 压测工具)进行测试。
测试环境
-
本地开发机(8核 CPU, 16GB RAM) -
Redis 和 API 均运行在本地 -
模拟数据:100 条商品记录
测试命令
# 测试开启缓存后的性能(预热后)wrk -t12 -c400 -d30s http://localhost:5000/api/products# 参数说明:# -t12: 12个线程# -c400: 保持400个并发连接# -d30s: 持续30秒
预期结果对比
结论:引入 Redis 缓存后,性能提升可达 10-20 倍,数据库压力显著降低。
32
5.4 监控 Redis 状态
使用 redis-cli 监控 Redis 资源使用情况。
# 连接 Redisredis-cli# 查看内存使用info memory# 关注: used_memory_human# 查看键数量dbsize# 实时监控命令monitor# (谨慎使用,影响性能)# 查看客户端连接info clients# 关注: connected_clients
33
5.5 本章小结
本章为系统添加了可观测性:
-
✅ 通过日志记录缓存命中/未命中事件。 -
✅ 使用计数器粗略统计命中率。 -
✅ 使用 wrk进行压力测试,量化缓存带来的性能提升。 -
✅ 监控 Redis 内存、连接等关键指标。
34
第6章:生产环境考量与高级实践
本章将探讨如何将缓存系统提升到生产就绪(Production-Ready)状态,涵盖高可用、降级、真实数据源集成等关键主题。
35
6.1 优雅降级:Redis 不可用时的处理
Redis 服务可能因网络、宕机等原因暂时不可用。我们的 API 不应因此完全瘫痪。
问题
IDistributedCache 在 Redis 不可用时会抛出 RedisConnectionException,导致 API 返回 500 错误。
解决方案:缓存层代理(Cache Wrapper)
创建一个代理服务,捕获异常并降级到直接查询数据库。
public interface IResilientCache{Task<string?> GetStringAsync(string key);Task SetStringAsync(string key, string value, TimeSpan? expiry = null);Task RemoveAsync(string key);}public class ResilientCache : IResilientCache{private readonly IDistributedCache _cache;private readonly ILogger<ResilientCache> _logger;public ResilientCache(IDistributedCache cache, ILogger<ResilientCache> logger){_cache = cache;_logger = logger;}public async Task<string?> GetStringAsync(string key){try{return await _cache.GetStringAsync(key);}catch (Exception ex) when (ex is RedisConnectionException || ex is TaskCanceledException){_logger.LogWarning(ex, "Redis 获取失败,降级到数据库查询。Key: {Key}", key);return null; // 降级:返回 null,业务层将查询数据库}}public async Task SetStringAsync(string key, string value, TimeSpan? expiry = null){try{var options = new DistributedCacheEntryOptions();if (expiry.HasValue) options.SetSlidingExpiration(expiry.Value);await _cache.SetStringAsync(key, value, options);}catch (Exception ex){_logger.LogError(ex, "Redis 写入失败,但继续执行。Key: {Key}", key);// 写入失败不中断业务流程}}public async Task RemoveAsync(string key){try{await _cache.RemoveAsync(key);}catch (Exception ex){_logger.LogError(ex, "Redis 删除失败。Key: {Key}", key);}}}
在 Program.cs 中替换服务
// 替换默认的 IDistributedCache 为我们的容错实现builder.Services.AddSingleton<IResilientCache, ResilientCache>();// 注意:不再直接注入 IDistributedCache 到控制器
在控制器中使用
private readonly IResilientCache _resilientCache;// ...public ProductsController(IResilientCache resilientCache, FakeProductDbContext dbContext, ILogger<ProductsController> logger){_resilientCache = resilientCache;_dbContext = dbContext;_logger = logger;}// 在 Get 方法中使用 _resilientCache.GetStringAsync / SetStringAsync
效果:当 Redis 宕机时,API 会暂时变慢(直接查 DB),但依然可用。Redis 恢复后,缓存自动重新生效。
36
6.2 后台服务:定时刷新热点缓存
对于极高频访问的数据(如首页商品),可以使用 IHostedService 后台服务定时刷新缓存,避免过期瞬间的延迟。
public class CacheRefreshService : BackgroundService{private readonly IServiceProvider _serviceProvider;private readonly ILogger<CacheRefreshService> _logger;public CacheRefreshService(IServiceProvider serviceProvider, ILogger<CacheRefreshService> logger){_serviceProvider = serviceProvider;_logger = logger;}protected override async Task ExecuteAsync(CancellationToken stoppingToken){_logger.LogInformation("缓存刷新服务已启动");using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5)); // 每5分钟执行while (!stoppingToken.IsCancellationRequested){try{await RefreshProductCacheAsync(stoppingToken);}catch (Exception ex){_logger.LogError(ex, "后台刷新缓存时发生错误");}await timer.WaitForNextTickAsync(stoppingToken);}}private async Task RefreshProductCacheAsync(CancellationToken ct){using var scope = _serviceProvider.CreateScope();var cache = scope.ServiceProvider.GetRequiredService<IResilientCache>();var dbContext = scope.ServiceProvider.GetRequiredService<FakeProductDbContext>();var products = dbContext.GetAllProducts();var json = JsonSerializer.Serialize(products);await cache.SetStringAsync("all_products_list",json,TimeSpan.FromMinutes(10));_logger.LogInformation("已后台刷新商品列表缓存");}}
注册后台服务
builder.Services.AddHostedService<CacheRefreshService>();
适用场景:首页 Banner、热门商品榜、配置信息等更新不频繁但访问极高的数据。
37
6.3 集成真实数据库(Entity Framework Core)
将 FakeProductDbContext 替换为真实的 EF Core 上下文。
dotnet add package Microsoft.EntityFrameworkCore.SqlServerdotnet add package Microsoft.EntityFrameworkCore.Tools
创建实体和 DbContext
// Product.cs (已存在,确保有 Key 特性)[]public int Id { get; set; }// Data/AppDbContext.cspublic class AppDbContext : DbContext{public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }public DbSet<Product> Products { get; set; }}
配置连接字符串
// appsettings.json"ConnectionStrings": {"DefaultConnection": "Server=localhost;Database=ProductDb;Trusted_Connection=true;TrustServerCertificate=true;"}
注册 DbContext
builder.Services.AddDbContext<AppDbContext>(options =>options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
在 Controller 中使用
private readonly AppDbContext _dbContext; // 替换 FakeProductDbContextpublic ProductsController(IResilientCache resilientCache, AppDbContext dbContext, ILogger<ProductsController> logger){_resilientCache = resilientCache;_dbContext = dbContext;_logger = logger;}// 查询方法var products = await _dbContext.Products.ToListAsync(ct);
38
6.4 Azure Redis Cache 配置示例
生产环境推荐使用托管服务如 Azure Cache for Redis。
// appsettings.Production.json{"Redis": {"Configuration": "your-cache-name.redis.cache.windows.net:6380,password=your-access-key,ssl=True,abortConnect=False","InstanceName": "ProdProductCatalogCache"}}
39
6.5 本章小结
本章提升了系统的生产就绪度:
-
✅ 实现了 IResilientCache代理,支持 Redis 不可用时的优雅降级。 -
✅ 使用 BackgroundService实现后台缓存刷新。 -
✅ 集成了 EF Core 和真实 SQL Server 数据库。 -
✅ 提供了 Azure Redis 的生产配置示例。
40
第7章:总结与最佳实践
本章将对整个实战项目进行总结,并提炼出在 .NET Core 项目中使用 Redis 缓存的关键最佳实践。
7.1 项目成果回顾
通过本系列实战,我们成功构建了一个高性能的商品目录服务:
- 性能飞跃
:通过引入 Redis 缓存,商品查询的平均响应时间从 ~100ms 降低至 ~2ms,吞吐量提升 20 倍以上。 - 架构健壮
:实现了缓存预热、优雅降级、后台刷新等机制,系统具备了应对 Redis 故障的能力。 - 可观测性强
:通过日志和计数器监控缓存命中率,便于运维和性能调优。 - 生产就绪
:集成了真实数据库(EF Core),并提供了云服务(Azure Redis)的配置方案。
41
7.2 .NET Core + Redis 缓存最佳实践清单
✅ 1. 合理选择缓存策略
- 读多写少
的数据最适合缓存(如商品目录、配置信息、用户资料)。 - 高频访问
的单个实体(如 GET /products/123)应单独缓存。 -
避免缓存频繁更新或体积巨大的数据。
42
✅ 2. 设计良好的缓存键(Cache Key)
-
使用有意义的命名,如 products:all,product:123,user:token:abc123。 -
包含业务上下文和关键标识符。 -
考虑使用命名空间(通过 InstanceName或 key 前缀)隔离不同环境或模块。
43
✅ 3. 设置合理的过期时间
- 滑动过期(Sliding Expiration)
:适合热点数据,访问即刷新过期时间。 - 绝对过期(Absolute Expiration)
:适合有明确时效性的数据(如每小时更新的排行榜)。 - 避免永不过期
:防止内存泄漏和数据陈旧。 - 差异化过期
:不同数据设置不同过期时间。
44
✅ 4. 实现缓存一致性
- 写操作后及时失效缓存
:在 Update、Delete操作后,删除或更新相关缓存项。 - 考虑使用消息队列
:在复杂系统中,通过发布/订阅模式通知缓存更新,解耦业务逻辑。
45
✅ 5. 必须处理缓存异常
- 网络分区或 Redis 宕机时,服务应能降级运行
(如第6章的 ResilientCache)。 -
日志要清晰,便于定位缓存相关问题。 -
不要让缓存故障导致整个应用崩溃。
46
✅ 6. 防护缓存常见问题
47
✅ 7. 监控与度量
- 监控缓存命中率
:理想情况应 > 90%。命中率低需分析原因(key设计、过期时间、数据分布)。 - 监控 Redis 资源
:内存使用率、连接数、CPU、网络IO。 - 记录关键日志
:缓存命中/未命中、异常事件。
48
✅ 8. 序列化选择
System.Text.Json:.NET 内置,性能好,推荐首选。 Newtonsoft.Json:功能更丰富,兼容性好。 - 二进制序列化
(如 protobuf):对性能和带宽要求极高时可考虑。
49
7.3 可扩展的未来方向
本项目可进一步扩展:
- 分布式锁
:使用 RedLock算法或 Redis 的SETNX命令实现,解决集群环境下的并发问题。 - 多级缓存
:结合 IMemoryCache(进程内缓存)和 Redis(分布式缓存),实现 L1 + L2 缓存,进一步降低延迟。 - 缓存预热自动化
:根据访问日志分析热点数据,自动预热。 - 引入缓存注解
:使用 AOP(如 Castle DynamicProxy)实现[Cacheable]、[CacheEvict]等注解,减少模板代码。 - 使用 Redis Streams
:实现更复杂的消息传递和事件驱动架构。
50
7.4 结语
通过这个实战项目,您已经掌握了在 .NET Core Web API 中集成 Redis 缓存的核心技能。从环境搭建、代码实现到生产部署,每一步都至关重要。记住,缓存是一把双刃剑——用得好,性能倍增;用不好,则可能引入数据不一致和系统复杂性。
核心思想:缓存的目的是提升性能和保护后端资源,但数据一致性和系统稳定性永远是第一位的。
END

