基于Senparc.CO2NET 缓存策略扩展的缓存使用方法

在高并发应用中,缓存是提升性能、减少数据库压力的核心手段之一。然而,不同场景下的缓存需求差异巨大:单实例应用适合本地内存缓存,分布式系统需要分布式缓存,而混合云架构可能需要多级缓存(Local + Redis)。

Senparc.CO2NET 是 .NET 生态中一款轻量级、高扩展的基础库,其缓存模块通过抽象化设计ICacheStrategy 接口)将缓存实现与业务逻辑解耦,允许开发者根据需求灵活扩展缓存策略。本文将深入讲解 Senparc.CO2NET 的缓存核心概念、默认策略,并通过** step-by-step 教程实际场景示例**,教会你如何扩展自定义缓存策略。

目录#

  1. Senparc.CO2NET 缓存模块概述
  2. 核心概念解析
    • 2.1 ICacheStrategy:缓存策略的核心接口
    • 2.2 CacheItem:缓存数据的载体
    • 2.3 CacheKey:缓存键的规范生成器
    • 2.4 CacheStrategyFactory:策略工厂
  3. 默认缓存策略与适用场景
    • 3.1 本地内存缓存(LocalMemoryCacheStrategy
    • 3.2 Redis 分布式缓存(RedisCacheStrategy
    • 3.3 Memcached 缓存(MemcachedCacheStrategy
  4. 扩展自定义缓存策略:Step-by-Step 教程
    • 4.1 需求定义:实现「本地+Redis」混合缓存
    • 4.2 步骤1:实现 ICacheStrategy 接口
    • 4.3 步骤2:处理缓存项序列化与过期逻辑
    • 4.4 步骤3:注册自定义策略到工厂
    • 4.5 步骤4:ASP.NET Core 中配置与启用
  5. 最佳实践:避免踩坑的关键技巧
  6. 实战场景示例
    • 6.1 场景1:混合缓存(Local + Redis)优化 latency
    • 6.2 场景2:带优先级的 Redis 缓存淘汰策略
  7. 常见问题排查
  8. 结论
  9. 参考资料

1. Senparc.CO2NET 缓存模块概述#

Senparc.CO2NET 的缓存模块遵循**「接口抽象 + 策略模式」**设计,核心目标是:

  • 屏蔽不同缓存实现的细节(如 MemoryCache、Redis、Memcached);
  • 支持运行时动态切换缓存策略;
  • 允许开发者自定义缓存逻辑(如多级缓存、特殊淘汰规则)。

模块的核心依赖关系如下:

graph TD
    A[业务逻辑] --> B[CacheStrategyFactory]
    B --> C[ICacheStrategy]
    C --> D[LocalMemoryCacheStrategy]
    C --> E[RedisCacheStrategy]
    C --> F[CustomCacheStrategy]

2. 核心概念解析#

在扩展缓存策略前,必须理解以下核心类型:

2.1 ICacheStrategy:缓存策略的核心接口#

ICacheStrategy 定义了缓存操作的标准契约,所有缓存策略(包括自定义)都需实现此接口。关键方法如下:

方法描述
Get<T>(string key)获取指定键的缓存项(泛型)
Set<T>(string key, CacheItem<T> item)设置缓存项
Remove(string key)删除缓存项
GetAsync<T>(...)异步获取缓存项(适合 I/O 密集型操作)
Clear()清空所有缓存

2.2 CacheItem:缓存数据的载体#

CacheItem<T> 是缓存数据的容器,包含以下关键属性:

  • Value:实际缓存的值(泛型类型 T);
  • AbsoluteExpiration:绝对过期时间(如「2024-12-31 23:59:59」);
  • AbsoluteExpirationRelativeToNow:相对于当前时间的绝对过期(如 TimeSpan.FromHours(1));
  • SlidingExpiration:滑动过期时间(如「30分钟内无访问则过期」);
  • DependencyKeys:缓存依赖键(如依赖数据库表的更新)。

2.3 CacheKey:缓存键的规范生成器#

为避免键冲突,Senparc.CO2NET 提供 CacheKey 类生成标准化缓存键。示例:

// 生成带前缀和参数的缓存键:"User:GetUserById:123"
var cacheKey = new CacheKey("User", "GetUserById", new { Id = 123 }).ToString();

最佳实践

  • 为不同业务模块设置唯一前缀(如 User:Order:);
  • 使用参数化键(如 IdPageIndex)避免硬编码。

2.4 CacheStrategyFactory:策略工厂#

CacheStrategyFactory 是获取当前缓存策略的入口,常用方法:

// 获取默认缓存策略(需提前配置)
var currentStrategy = CacheStrategyFactory.GetCacheStrategy();
 
// 动态切换策略(运行时生效)
CacheStrategyFactory.ChangeCacheStrategy(typeof(CustomCacheStrategy));

3. 默认缓存策略与适用场景#

Senparc.CO2NET 提供 3 种内置策略,覆盖大部分基础场景:

3.1 本地内存缓存(LocalMemoryCacheStrategy#

  • 实现:基于 .NET 内置 MemoryCache
  • 适用场景:单实例应用、开发/测试环境;
  • 优缺点
    • ✅ 速度快(无网络开销);
    • ❌ 不支持分布式(多实例缓存不共享);
    • ❌ 重启后缓存失效。

3.2 Redis 分布式缓存(RedisCacheStrategy#

  • 实现:基于 StackExchange.Redis
  • 适用场景:分布式系统、多实例应用;
  • 优缺点
    • ✅ 支持跨实例共享缓存;
    • ✅ 持久化(可选);
    • ❌ 有网络开销(需优化序列化);
    • ❌ 需维护 Redis 集群。

3.3 Memcached 缓存(MemcachedCacheStrategy#

  • 实现:基于 EnyimMemcachedCore
  • 适用场景:旧系统迁移、对内存利用率要求极高的场景;
  • 注意:Memcached 已逐渐被 Redis 取代,除非历史原因,不推荐新项目使用。

4. 扩展自定义缓存策略:Step-by-Step 教程#

接下来,我们通过**「本地+Redis 混合缓存」**场景,演示如何扩展自定义策略。

4.1 需求定义#

混合缓存的核心逻辑:

  1. 读取顺序:优先从本地内存获取,未命中则从 Redis 获取,并回写本地;
  2. 写入顺序:同时写入本地内存和 Redis;
  3. 删除顺序:同时删除本地和 Redis 中的缓存。

适用场景:高并发读场景(如商品详情页),既利用本地缓存的低延迟,又保证分布式一致性。

4.2 步骤1:实现 ICacheStrategy 接口#

创建 HybridCacheStrategy 类,实现 ICacheStrategy 接口:

using Senparc.CO2NET.Cache;
using Senparc.CO2NET.Cache.BaseCache;
using StackExchange.Redis;
using System.Text.Json;
 
public class HybridCacheStrategy : ICacheStrategy
{
    // 本地内存缓存(基于 Senparc 内置的 LocalMemoryCacheStrategy)
    private readonly LocalMemoryCacheStrategy _localCache;
    // Redis 缓存(基于 Senparc 内置的 RedisCacheStrategy)
    private readonly RedisCacheStrategy _redisCache;
 
    // 通过 DI 注入 Redis 连接字符串
    public HybridCacheStrategy(string redisConnectionString)
    {
        _localCache = new LocalMemoryCacheStrategy();
        _redisCache = new RedisCacheStrategy(redisConnectionString);
    }
 
    #region 核心方法实现
    public CacheItem<T> Get<T>(string key)
    {
        // 1. 优先读本地缓存
        var localItem = _localCache.Get<T>(key);
        if (localItem != null)
        {
            return localItem;
        }
 
        // 2. 本地未命中,读 Redis
        var redisItem = _redisCache.Get<T>(key);
        if (redisItem != null)
        {
            // 3. 回写本地缓存(设置较短的滑动过期,避免本地缓存过时)
            _localCache.Set(key, redisItem with { SlidingExpiration = TimeSpan.FromMinutes(5) });
        }
 
        return redisItem;
    }
 
    public async Task<CacheItem<T>> GetAsync<T>(string key, CancellationToken cancellationToken = default)
    {
        // 异步方法需保持逻辑一致,注意 await
        var localItem = await _localCache.GetAsync<T>(key, cancellationToken);
        if (localItem != null)
        {
            return localItem;
        }
 
        var redisItem = await _redisCache.GetAsync<T>(key, cancellationToken);
        if (redisItem != null)
        {
            await _localCache.SetAsync(key, redisItem with { SlidingExpiration = TimeSpan.FromMinutes(5) }, cancellationToken);
        }
 
        return redisItem;
    }
 
    public void Set<T>(string key, CacheItem<T> cacheItem)
    {
        // 同时写入本地和 Redis
        _localCache.Set(key, cacheItem);
        _redisCache.Set(key, cacheItem);
    }
 
    public async Task SetAsync<T>(string key, CacheItem<T> cacheItem, CancellationToken cancellationToken = default)
    {
        await Task.WhenAll(
            _localCache.SetAsync(key, cacheItem, cancellationToken),
            _redisCache.SetAsync(key, cacheItem, cancellationToken)
        );
    }
 
    public void Remove(string key)
    {
        _localCache.Remove(key);
        _redisCache.Remove(key);
    }
 
    public async Task RemoveAsync(string key, CancellationToken cancellationToken = default)
    {
        await Task.WhenAll(
            _localCache.RemoveAsync(key, cancellationToken),
            _redisCache.RemoveAsync(key, cancellationToken)
        );
    }
    #endregion
 
    #region 其他方法实现(按需补充)
    public void Clear()
    {
        _localCache.Clear();
        _redisCache.Clear();
    }
 
    public async Task ClearAsync(CancellationToken cancellationToken = default)
    {
        await Task.WhenAll(
            _localCache.ClearAsync(cancellationToken),
            _redisCache.ClearAsync(cancellationToken)
        );
    }
 
    // 其他方法(如 GetAllKeys、Exists 等)同理,需同时操作本地和 Redis
    #endregion
}

4.3 步骤2:处理缓存项序列化与过期逻辑#

对于分布式缓存(如 Redis),CacheItem<T> 需要序列化才能存储。Senparc.CO2NET 默认使用 Newtonsoft.Json,但推荐使用 System.Text.Json 提升性能:

// 在 HybridCacheStrategy 中添加序列化配置
private static readonly JsonSerializerOptions _jsonOptions = new()
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    WriteIndented = false,
    IgnoreNullValues = true
};
 
// 示例:Redis 序列化逻辑(替换默认实现)
private string SerializeCacheItem<T>(CacheItem<T> item)
{
    return JsonSerializer.Serialize(item, _jsonOptions);
}
 
private CacheItem<T>? DeserializeCacheItem<T>(string json)
{
    return JsonSerializer.Deserialize<CacheItem<T>>(json, _jsonOptions);
}

4.4 步骤3:注册自定义策略到工厂#

自定义策略需通过 CacheStrategyFactory 注册,才能被业务逻辑使用。在 ASP.NET Core 中,可通过**依赖注入(DI)**配置:

// Program.cs
var builder = WebApplication.CreateBuilder(args);
 
// 1. 配置 Redis 连接字符串
var redisConnectionString = builder.Configuration.GetConnectionString("Redis") 
                           ?? throw new ArgumentNullException("Redis 连接字符串未配置");
 
// 2. 注册混合缓存策略
builder.Services.AddSenparcGlobalServices(builder.Configuration)
    .AddSenparcCache(options =>
    {
        // 注册自定义策略(通过工厂方法传入 Redis 连接字符串)
        options.AddCacheStrategy(() => new HybridCacheStrategy(redisConnectionString));
        // 设置默认策略为混合缓存
        options.DefaultCacheStrategy = typeof(HybridCacheStrategy);
    });
 
var app = builder.Build();
// ...

4.5 步骤4:业务逻辑中使用自定义策略#

注册完成后,业务逻辑可通过 CacheStrategyFactory 直接使用混合缓存:

public class ProductService
{
    public async Task<Product> GetProductByIdAsync(int productId)
    {
        var cacheKey = new CacheKey("Product", "GetProductById", new { ProductId = productId }).ToString();
 
        // 从混合缓存获取
        var cacheItem = await CacheStrategyFactory.GetCacheStrategy()
                                                 .GetAsync<Product>(cacheKey);
 
        if (cacheItem != null)
        {
            return cacheItem.Value;
        }
 
        // 缓存未命中,查询数据库
        var product = await _dbContext.Products.FindAsync(productId);
        if (product == null)
        {
            return null;
        }
 
        // 写入混合缓存(设置 1 小时绝对过期 + 30 分钟滑动过期)
        await CacheStrategyFactory.GetCacheStrategy().SetAsync(
            key: cacheKey,
            cacheItem: new CacheItem<Product>(product)
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),
                SlidingExpiration = TimeSpan.FromMinutes(30)
            }
        );
 
        return product;
    }
}

5. 最佳实践#

5.1 策略选择原则#

场景推荐策略
单实例、低并发LocalMemoryCacheStrategy
分布式、高并发RedisCacheStrategy
高并发读、低更新HybridCacheStrategy
旧系统迁移MemcachedCacheStrategy

5.2 缓存键设计#

  • 唯一前缀:用业务模块名作为前缀(如 Product:Order:);
  • 参数化:用动态参数生成键(如 Product:GetById:123);
  • 避免特殊字符:如 :| 可能导致 Redis 键解析错误。

5.3 过期策略优化#

  • 绝对过期:适用于时效性强的数据(如验证码、促销活动);
  • 滑动过期:适用于访问频率不均的数据(如商品详情页);
  • 混合过期:分布式缓存用绝对过期,本地缓存用滑动过期(如 HybridCacheStrategy)。

5.4 性能优化#

  • 减少序列化开销:使用 System.Text.JsonProtobuf 替代 Newtonsoft.Json
  • 批量操作:对于批量查询,使用 GetAllAsync 而非多次 GetAsync
  • 避免缓存穿透:对不存在的键设置空值缓存(如 CacheItem<Product>(null)),过期时间设为 5-10 分钟。

5.5 分布式缓存可靠性#

  • 重试机制:对 Redis 操作添加重试(如 Polly),避免网络波动导致的失败;
  • 熔断降级:当 Redis 不可用时, fallback 到本地缓存;
  • 监控:通过 Redis 监控工具(如 RedisInsight)跟踪缓存命中率、过期率。

6. 实战场景示例#

6.1 场景1:带优先级的 Redis 缓存淘汰策略#

需求:某些高价值数据(如首页轮播图)需优先保留,即使 Redis 内存不足,也不被淘汰。

实现思路

  1. CacheItem<T> 中添加 Priority 属性(如 LowMediumHigh);
  2. 使用 Redis **有序集合(Sorted Set)**存储缓存键的优先级;
  3. 当 Redis 内存不足时,优先淘汰低优先级键。

关键代码片段:

public class PriorityRedisCacheStrategy : RedisCacheStrategy
{
    public enum CachePriority { Low, Medium, High }
 
    public PriorityRedisCacheStrategy(string connectionString) : base(connectionString) { }
 
    public override async Task SetAsync<T>(string key, CacheItem<T> item, CancellationToken cancellationToken = default)
    {
        // 1. 写入 Redis 字符串(存储缓存值)
        await base.SetAsync(key, item, cancellationToken);
 
        // 2. 将键写入有序集合(分数为优先级,High=3,Medium=2,Low=1)
        var priority = item.GetExtension<CachePriority>() ?? CachePriority.Medium;
        var score = priority switch
        {
            CachePriority.High => 3,
            CachePriority.Medium => 2,
            _ => 1
        };
        await _redisDatabase.SortedSetAddAsync("cache:priorities", key, score);
    }
 
    // 淘汰逻辑:当内存不足时,删除低优先级键
    public async Task EvictLowPriorityItemsAsync(int count)
    {
        // 获取分数最低的 count 个键
        var lowPriorityKeys = await _redisDatabase.SortedSetRangeByRankAsync("cache:priorities", 0, count - 1);
        foreach (var key in lowPriorityKeys)
        {
            await RemoveAsync(key.ToString());
            await _redisDatabase.SortedSetRemoveAsync("cache:priorities", key);
        }
    }
}

6.2 场景2:缓存依赖(Cache Dependency)#

需求:当数据库表 Product 更新时,自动失效所有相关缓存。

实现思路

  1. Product 表添加更新时间戳(如 LastModifiedTime);
  2. 缓存键包含时间戳(如 Product:GetById:123:20240501120000);
  3. 当表更新时,修改时间戳,导致旧缓存键失效。

示例代码:

// 获取 Product 表的最新时间戳
private async Task<DateTime> GetProductLastModifiedTimeAsync()
{
    return await _dbContext.Products.MaxAsync(p => p.LastModifiedTime);
}
 
public async Task<Product> GetProductByIdAsync(int productId)
{
    var lastModifiedTime = await GetProductLastModifiedTimeAsync();
    // 缓存键包含时间戳:"Product:GetById:123:20240501120000"
    var cacheKey = new CacheKey("Product", "GetById", new 
    { 
        ProductId = productId,
        LastModified = lastModifiedTime.ToString("yyyyMMddHHmmss")
    }).ToString();
 
    // 后续逻辑同之前的示例...
}

7. 常见问题排查#

7.1 缓存键冲突#

  • 现象:不同业务模块的缓存被互相覆盖;
  • 解决:为每个模块设置唯一前缀(如 Product:Order:)。

7.2 分布式缓存序列化错误#

  • 现象GetAsync<T> 返回 null 或抛出 JsonException
  • 解决
    1. 确保 CacheItem<T> 的泛型类型 T 可序列化(无循环引用、无未初始化的成员);
    2. 统一序列化器(如全项目使用 System.Text.Json)。

7.3 本地缓存与 Redis 不一致#

  • 现象:本地缓存未更新,但 Redis 已更新;
  • 解决
    1. 写入操作需同时更新本地和 Redis;
    2. 本地缓存设置较短的滑动过期(如 5 分钟),强制定期从 Redis 刷新。

7.4 Redis 连接超时#

  • 现象GetAsync<T> 抛出 RedisTimeoutException
  • 解决
    1. 优化 Redis 集群配置(如增加节点、开启 Pipeline);
    2. 使用 Polly 添加重试策略:
// 添加 Polly 重试策略
builder.Services.AddHttpClient()
    .AddTransientHttpErrorPolicy(p => 
        p.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))));

8. 结论#

Senparc.CO2NET 的缓存策略扩展机制,为 .NET 开发者提供了灵活、可定制的缓存解决方案。通过实现 ICacheStrategy 接口,你可以轻松构建:

  • 多级缓存(Local + Redis);
  • 带优先级的淘汰策略;
  • 基于数据库依赖的缓存失效逻辑。

关键原则:缓存策略需贴合业务场景——没有「银弹」策略,只有「最适合」的策略。

9. 参考资料#

  1. Senparc.CO2NET 官方文档:https://docs.senparc.com/CO2NET/

  2. StackExchange.Redis 文档:https://stackexchange.github.io/StackExchange.Redis/

  3. .NET 缓存最佳实践:https://learn.microsoft.com/zh-cn/aspnet/core/performance/caching/

  4. Redis 序列化优化:https://redis.io/docs/data-types/strings/#serialization

通过本文的讲解,相信你已掌握 Senparc.CO2NET 缓存策略扩展的核心方法。欢迎在评论区分享你的自定义策略场景!