大数跨境
0
0

《告别数据库压力!.NET Core + Redis 实现商品目录高性能缓存的完整实践》

《告别数据库压力!.NET Core + Redis 实现商品目录高性能缓存的完整实践》 Coco跨境电商
2025-10-21
5
导读:高频商品查询拖垮数据库?本篇实战详解如何用 Redis 为 .NET Core Web API 注入“强心针”。涵盖缓存设计、代码实现、性能压测、异常降级与生产部署,一站式解决缓存穿透、雪崩难题,助你

学习新思路

趣事:

最近有人留言,如何在在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

  1. Windows 用户

    • 推荐使用 Redis for Windows 的 MSI 安装包。
    • 安装完成后,Redis 服务会自动在后台运行,默认端口 6379
    • 打开命令提示符,输入 redis-cli ping,若返回 PONG,则表示 Redis 服务正常。
  2. macOS 用户

    • 使用 Homebrew 安装:brew install redis
    • 启动服务:brew services start redis
    • 验证:redis-cli ping 应返回 PONG
  3. 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 { getset; }    public string Name { getset; } = string.Empty;    public decimal Price { getset; }    public string Category { getset; } = string.Empty;    public bool IsAvailable { getset; }}

创建 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;[ApiController][Route("api/[controller]")]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);}

详细解释

  1. GetStringAsync
    :从 Redis 获取字符串值。Redis 存储的是字节流,String 方法是常用封装。
  2. 缓存穿透防护
    :虽然简单判断 null 或空字符串,但更完善的方案应缓存“空结果”以防止恶意攻击。
  3. SetSlidingExpiration
    :滑动过期策略。如果数据在10分钟内被再次访问,过期时间将重新计算。适合热点数据。
  4. 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 测试缓存效果

  1. 启动项目:dotnet run
  2. 使用 Swagger UI 或 curl 访问 GET /api/products
    • 第一次请求
      :响应较慢(约 100ms+),因为需要查询数据库并写入缓存。
    • 后续请求
      :响应极快(几毫秒),数据直接来自 Redis 缓存。
  3. 可以使用 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} 添加缓存。

方案一:独立键存储(推荐初学者)

每个商品使用独立的缓存键。

[HttpGet("{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 == nullreturn 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";[HttpGet("{id}")]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 == nullreturn 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.Redis API。

23

4.2 缓存更新与失效(Cache Invalidation)

当商品信息被修改时,必须确保缓存中的旧数据被清除或更新,否则用户会看到过期信息。

场景:更新商品信息

[HttpPut("{id}")]public IActionResult Put(int id, [FromBody] Product updatedProduct){    var existing = _dbContext.GetProductById(id);    if (existing == nullreturn 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<IResilientCacheResilientCache>();// 注意:不再直接注入 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 特性)[Key]public int Id { getset; }// Data/AppDbContext.cspublic class AppDbContext : DbContext{    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }    public DbSet<Product> Products { getset; }}

配置连接字符串

// 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:allproduct:123user:token:abc123
  • 包含业务上下文关键标识符
  • 考虑使用命名空间(通过 InstanceName 或 key 前缀)隔离不同环境或模块。

43

✅ 3. 设置合理的过期时间

  • 滑动过期(Sliding Expiration)
    :适合热点数据,访问即刷新过期时间。
  • 绝对过期(Absolute Expiration)
    :适合有明确时效性的数据(如每小时更新的排行榜)。
  • 避免永不过期
    :防止内存泄漏和数据陈旧。
  • 差异化过期
    :不同数据设置不同过期时间。

44

✅ 4. 实现缓存一致性

  • 写操作后及时失效缓存
    :在 UpdateDelete 操作后,删除或更新相关缓存项。
  • 考虑使用消息队列
    :在复杂系统中,通过发布/订阅模式通知缓存更新,解耦业务逻辑。

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


【声明】内容源于网络
0
0
Coco跨境电商
跨境分享所 | 持续提供优质干货
内容 192965
粉丝 3
Coco跨境电商 跨境分享所 | 持续提供优质干货
总阅读406.7k
粉丝3
内容193.0k