基于Senparc.CO2NET 缓存策略扩展的缓存使用方法
在高并发应用中,缓存是提升性能、减少数据库压力的核心手段之一。然而,不同场景下的缓存需求差异巨大:单实例应用适合本地内存缓存,分布式系统需要分布式缓存,而混合云架构可能需要多级缓存(Local + Redis)。
Senparc.CO2NET 是 .NET 生态中一款轻量级、高扩展的基础库,其缓存模块通过抽象化设计(ICacheStrategy 接口)将缓存实现与业务逻辑解耦,允许开发者根据需求灵活扩展缓存策略。本文将深入讲解 Senparc.CO2NET 的缓存核心概念、默认策略,并通过** step-by-step 教程和实际场景示例**,教会你如何扩展自定义缓存策略。
目录#
- Senparc.CO2NET 缓存模块概述
- 核心概念解析
- 2.1
ICacheStrategy:缓存策略的核心接口 - 2.2
CacheItem:缓存数据的载体 - 2.3
CacheKey:缓存键的规范生成器 - 2.4
CacheStrategyFactory:策略工厂
- 2.1
- 默认缓存策略与适用场景
- 3.1 本地内存缓存(
LocalMemoryCacheStrategy) - 3.2 Redis 分布式缓存(
RedisCacheStrategy) - 3.3 Memcached 缓存(
MemcachedCacheStrategy)
- 3.1 本地内存缓存(
- 扩展自定义缓存策略:Step-by-Step 教程
- 4.1 需求定义:实现「本地+Redis」混合缓存
- 4.2 步骤1:实现
ICacheStrategy接口 - 4.3 步骤2:处理缓存项序列化与过期逻辑
- 4.4 步骤3:注册自定义策略到工厂
- 4.5 步骤4:ASP.NET Core 中配置与启用
- 最佳实践:避免踩坑的关键技巧
- 实战场景示例
- 6.1 场景1:混合缓存(Local + Redis)优化 latency
- 6.2 场景2:带优先级的 Redis 缓存淘汰策略
- 常见问题排查
- 结论
- 参考资料
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:); - 使用参数化键(如
Id、PageIndex)避免硬编码。
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 需求定义#
混合缓存的核心逻辑:
- 读取顺序:优先从本地内存获取,未命中则从 Redis 获取,并回写本地;
- 写入顺序:同时写入本地内存和 Redis;
- 删除顺序:同时删除本地和 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.Json或Protobuf替代Newtonsoft.Json; - 批量操作:对于批量查询,使用
GetAllAsync而非多次GetAsync; - 避免缓存穿透:对不存在的键设置空值缓存(如
CacheItem<Product>(null)),过期时间设为 5-10 分钟。
5.5 分布式缓存可靠性#
- 重试机制:对 Redis 操作添加重试(如
Polly),避免网络波动导致的失败; - 熔断降级:当 Redis 不可用时, fallback 到本地缓存;
- 监控:通过 Redis 监控工具(如 RedisInsight)跟踪缓存命中率、过期率。
6. 实战场景示例#
6.1 场景1:带优先级的 Redis 缓存淘汰策略#
需求:某些高价值数据(如首页轮播图)需优先保留,即使 Redis 内存不足,也不被淘汰。
实现思路:
- 在
CacheItem<T>中添加Priority属性(如Low、Medium、High); - 使用 Redis **有序集合(Sorted Set)**存储缓存键的优先级;
- 当 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 更新时,自动失效所有相关缓存。
实现思路:
- 为
Product表添加更新时间戳(如LastModifiedTime); - 缓存键包含时间戳(如
Product:GetById:123:20240501120000); - 当表更新时,修改时间戳,导致旧缓存键失效。
示例代码:
// 获取 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; - 解决:
- 确保
CacheItem<T>的泛型类型T可序列化(无循环引用、无未初始化的成员); - 统一序列化器(如全项目使用
System.Text.Json)。
- 确保
7.3 本地缓存与 Redis 不一致#
- 现象:本地缓存未更新,但 Redis 已更新;
- 解决:
- 写入操作需同时更新本地和 Redis;
- 本地缓存设置较短的滑动过期(如 5 分钟),强制定期从 Redis 刷新。
7.4 Redis 连接超时#
- 现象:
GetAsync<T>抛出RedisTimeoutException; - 解决:
- 优化 Redis 集群配置(如增加节点、开启 Pipeline);
- 使用
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. 参考资料#
-
Senparc.CO2NET 官方文档:https://docs.senparc.com/CO2NET/
-
StackExchange.Redis 文档:https://stackexchange.github.io/StackExchange.Redis/
-
.NET 缓存最佳实践:https://learn.microsoft.com/zh-cn/aspnet/core/performance/caching/
-
Redis 序列化优化:https://redis.io/docs/data-types/strings/#serialization
通过本文的讲解,相信你已掌握 Senparc.CO2NET 缓存策略扩展的核心方法。欢迎在评论区分享你的自定义策略场景!