Sage.Caching.Memory
1.0.0.8
dotnet add package Sage.Caching.Memory --version 1.0.0.8
NuGet\Install-Package Sage.Caching.Memory -Version 1.0.0.8
<PackageReference Include="Sage.Caching.Memory" Version="1.0.0.8" />
<PackageVersion Include="Sage.Caching.Memory" Version="1.0.0.8" />
<PackageReference Include="Sage.Caching.Memory" />
paket add Sage.Caching.Memory --version 1.0.0.8
#r "nuget: Sage.Caching.Memory, 1.0.0.8"
#:package Sage.Caching.Memory@1.0.0.8
#addin nuget:?package=Sage.Caching.Memory&version=1.0.0.8
#tool nuget:?package=Sage.Caching.Memory&version=1.0.0.8
Sage.Caching.Memory
Sage.Caching.Memory 是对 Microsoft.Extensions.Caching.Memory 的轻量封装,提供了更直接的缓存读写接口,支持:
- 线程安全的内存缓存操作
Region分组管理与分组清理- 同步 / 异步缓存创建
- 可选统计信息
KeyPrefix键前缀隔离- AOT 兼容
适合在 ASP.NET Core、WebApi、Minimal API、Native AOT 项目中直接使用。
适用场景
- 单机 WebApi 的热点数据缓存
- 查询结果缓存
- 下游接口返回值短时缓存
- 按模块或业务分组清理缓存
不适合:
- 多实例共享缓存
- 需要跨进程一致性的缓存
- 需要持久化的缓存
这类场景建议使用 Redis 等分布式缓存。
安装
dotnet add package Sage.Caching.Memory
快速开始
1. 注册服务
using Microsoft.Extensions.Caching.Memory;
using Sage.Caching.Memory.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCacheService(options =>
{
// 是否启用统计
options.EnableStats = false;
// 当工厂方法创建缓存失败时,是否直接抛出异常
options.ThrowOnCreateError = true;
// 键前缀,建议按应用或服务名设置
options.KeyPrefix = "WebService.Callback";
// 是否启用类型检查
options.EnableTypeChecking = false;
// 最大缓存大小限制
options.MaximumCacheSize = 10000;
// 到达内存压力时压缩比例
options.CompactionPercentage = 0.10;
// 过期扫描频率
options.ExpirationScanFrequency = TimeSpan.FromMinutes(1);
// 默认缓存项配置
options.DefaultEntryOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
SlidingExpiration = TimeSpan.FromMinutes(10),
Size = 1,
Priority = CacheItemPriority.Normal
};
});
var app = builder.Build();
app.Run();
2. 在 WebApi Minimal(AOT) 中使用
推荐直接在 endpoint 参数中注入 ICacheService:
using Microsoft.Extensions.Caching.Memory;
using Sage.Caching.Memory.Services;
app.MapGet("/callbacks/{id}", async (string id, ICacheService cacheService) =>
{
var result = await cacheService.GetOrCreateAsync(
key: $"callback:{id}",
valueFactory: async () =>
{
await Task.Delay(50);
return new
{
Id = id,
Name = $"Callback-{id}",
Time = DateTimeOffset.UtcNow
};
},
region: "Callback",
options: new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
SlidingExpiration = TimeSpan.FromMinutes(2),
Size = 1,
Priority = CacheItemPriority.Normal
});
return Results.Ok(result);
});
3. 在 Controller 中使用
using Microsoft.AspNetCore.Mvc;
using Sage.Caching.Memory.Services;
[ApiController]
[Route("api/[controller]")]
public class DataController : ControllerBase
{
private readonly ICacheService _cacheService;
public DataController(ICacheService cacheService)
{
_cacheService = cacheService;
}
[HttpGet("{id}")]
public async Task<IActionResult> Get(string id)
{
var data = await _cacheService.GetOrCreateAsync(
key: $"data:{id}",
valueFactory: async () =>
{
await Task.Delay(100);
return new { Id = id, Name = $"Item-{id}" };
},
region: "Data");
return Ok(data);
}
}
核心接口
主要接口为 ICacheService:
bool TryGetValue<T>(string key, out T? value);
void Set<T>(string key, T value, string? region = null, MemoryCacheEntryOptions? options = null);
void Remove(string key, string? region = null);
T? GetOrCreate<T>(string key, Func<T> valueFactory, string? region = null, MemoryCacheEntryOptions? options = null);
ValueTask<T?> GetOrCreateAsync<T>(string key, Func<ValueTask<T>> valueFactory, string? region = null, MemoryCacheEntryOptions? options = null);
void Clear();
int ClearRegion(string region);
void SetMany<T>(IEnumerable<KeyValuePair<string, T>> items, string? region = null, MemoryCacheEntryOptions? options = null);
int RemoveMany(IEnumerable<string> keys);
CacheStats GetCacheStats();
IReadOnlyCollection<string> GetRegionKeys(string region);
常用用法
1. 手动写入缓存
cacheService.Set(
key: "user:1001",
value: new UserDto(1001, "Alice"),
region: "User");
2. 手动读取缓存
if (cacheService.TryGetValue<UserDto>("user:1001", out var user))
{
return Results.Ok(user);
}
return Results.NotFound();
3. 缓存不存在时自动创建
同步:
var product = cacheService.GetOrCreate(
key: "product:1",
valueFactory: () => new ProductDto(1, "Apple"),
region: "Product");
异步:
var product = await cacheService.GetOrCreateAsync(
key: "product:1",
valueFactory: async () =>
{
await Task.Delay(50);
return new ProductDto(1, "Apple");
},
region: "Product");
4. 自定义当前缓存项过期时间
var order = await cacheService.GetOrCreateAsync(
key: "order:2026001",
valueFactory: async () =>
{
await Task.Delay(20);
return new { Id = 2026001, Status = "Paid" };
},
region: "Order",
options: new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(3),
SlidingExpiration = TimeSpan.FromMinutes(1),
Size = 1,
Priority = CacheItemPriority.High
});
5. 删除单个缓存
cacheService.Remove("user:1001");
如果你明确知道这个 key 属于哪个分组,也可以同时传入 region:
cacheService.Remove("user:1001", region: "User");
6. 清空某个分组
var removedCount = cacheService.ClearRegion("User");
7. 清空全部缓存
cacheService.Clear();
8. 批量写入
var items = new Dictionary<string, string>
{
["config:siteName"] = "Sage",
["config:theme"] = "default",
["config:lang"] = "zh-CN"
};
cacheService.SetMany(items, region: "Config");
9. 批量删除
var removed = cacheService.RemoveMany(new[]
{
"config:siteName",
"config:theme"
});
10. 查看分组内有哪些键
var keys = cacheService.GetRegionKeys("User");
11. 获取缓存统计信息
var stats = cacheService.GetCacheStats();
Console.WriteLine($"Hits: {stats.Hits}");
Console.WriteLine($"Misses: {stats.Misses}");
Console.WriteLine($"HitRate: {stats.HitRate:P2}");
Console.WriteLine($"TotalItems: {stats.TotalItems}");
配置项说明
MemoryCacheServiceOptions
| 配置项 | 说明 |
|---|---|
DefaultEntryOptions |
默认缓存项配置 |
EnableStats |
是否启用统计信息,默认 false |
ThrowOnCreateError |
工厂方法异常时是否直接抛出,默认 true |
StatsFlushInterval |
统计信息刷新间隔 |
MaximumCacheSize |
缓存总大小限制 |
ExpirationScanFrequency |
过期扫描频率 |
CompactionPercentage |
内存压力时压缩比例 |
EnableTypeChecking |
是否校验读取类型是否匹配 |
KeyPrefix |
键前缀,防止多模块键冲突 |
MemoryCacheEntryOptions 属性说明
这是单个缓存项的配置对象。最常用的是过期时间、优先级和 Size。
| 属性 | 类型 | 说明 | 是否常用 |
|---|---|---|---|
AbsoluteExpiration |
DateTimeOffset? |
绝对过期时间点。到指定时间后缓存失效。 | 常用 |
AbsoluteExpirationRelativeToNow |
TimeSpan? |
相对于“当前时间”的绝对过期时长,例如 30 分钟后失效。 | 很常用 |
SlidingExpiration |
TimeSpan? |
滑动过期时间。缓存项在一段时间内没有被访问就失效。再次访问会刷新这段时间,但不会超过绝对过期时间。 | 很常用 |
Priority |
CacheItemPriority |
缓存优先级。在内存压力清理时,优先级高的项更不容易被清掉。默认是 Normal。 |
常用 |
Size |
long? |
当前缓存项占用的大小单位。只有配置了 MaximumCacheSize 时才需要重点关注。 |
很常用 |
ExpirationTokens |
IList<IChangeToken> |
变更令牌集合。任一令牌触发后,缓存项过期。适合做“依赖某个配置变化自动失效”。 | 进阶 |
PostEvictionCallbacks |
IList<PostEvictionCallbackRegistration> |
缓存项被移除后触发的回调。可用于日志、监控或清理附属资源。 | 进阶 |
常用属性怎么选
- 只想固定时间后过期:
new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
Size = 1
}
- 想“长时间没人访问才过期”:
new MemoryCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(10),
Size = 1
}
- 想“最多保留 30 分钟,但 10 分钟不访问也过期”:
new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
SlidingExpiration = TimeSpan.FromMinutes(10),
Size = 1,
Priority = CacheItemPriority.Normal
}
属性使用建议
AbsoluteExpiration和AbsoluteExpirationRelativeToNow二选一通常就够了,优先推荐后者。SlidingExpiration适合热点数据,但不适合要求“必须在固定时点后失效”的数据。SlidingExpiration与绝对过期可以同时使用,最终谁先到期就按谁失效。- 设置了
MaximumCacheSize后,请务必为缓存项提供Size。 Priority只影响内存压力触发的清理,不影响正常的时间过期。
当前库实现注意
- 本库内部会克隆
MemoryCacheEntryOptions后再写入缓存,因此常用属性AbsoluteExpiration、AbsoluteExpirationRelativeToNow、SlidingExpiration、Priority、Size和逐出回调会生效。 - 如果你计划大量依赖
ExpirationTokens,建议先确认当前版本是否已完整透传该属性;当前实现更推荐优先使用时间过期和业务主动Remove的方式控制失效。
使用建议
1. key 要稳定、可预测
建议按业务维度命名:
"user:1001"
"order:2026001"
"callback:task-001"
不要直接把随机字符串当业务主键缓存,后期不方便排查和清理。
2. region 只是分组标签,不参与键隔离
例如下面两段代码:
cacheService.Set("user:1", userA, region: "A");
cacheService.Set("user:1", userB, region: "B");
它们依然是在操作同一个业务 key。
也就是说:
region主要用于ClearRegion("xxx")- 真正避免冲突靠的是
key - 不同分组里不要复用同名业务 key
3. 不要手动拼接 KeyPrefix
如果你配置了:
options.KeyPrefix = "WebService.Callback";
业务代码里依然只写原始 key:
cacheService.Set("callback:1001", value);
不要自己写成:
cacheService.Set("WebService.Callback:callback:1001", value);
前缀应由缓存服务内部统一处理。
4. 设置了 MaximumCacheSize 后,要注意每项的 Size
如果你启用了总大小限制:
options.MaximumCacheSize = 10000;
那么每个缓存项都应提供 Size。你可以:
- 在
DefaultEntryOptions里统一设置Size - 或者在单次写入时单独指定
Size
例如:
options.DefaultEntryOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
Size = 1
};
注意:MaximumCacheSize 是 IMemoryCache 的大小单位,不等同于“字节数”。
5. 统计信息有轻微性能开销
如果你只关心缓存功能,不关心命中率、未命中数、逐出数,建议保持:
options.EnableStats = false;
WebApi Minimal(AOT) 推荐示例
下面是一份适合直接复制到 Program.cs 的最小示例:
using Microsoft.Extensions.Caching.Memory;
using Sage.Caching.Memory.Extensions;
using Sage.Caching.Memory.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCacheService(options =>
{
options.EnableStats = false;
options.ThrowOnCreateError = true;
options.KeyPrefix = "WebService.Callback";
options.EnableTypeChecking = false;
options.MaximumCacheSize = 10000;
options.CompactionPercentage = 0.10;
options.ExpirationScanFrequency = TimeSpan.FromMinutes(1);
options.DefaultEntryOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
SlidingExpiration = TimeSpan.FromMinutes(10),
Size = 1,
Priority = CacheItemPriority.Normal
};
});
var app = builder.Build();
app.MapGet("/callback/{id}", async (string id, ICacheService cacheService) =>
{
var result = await cacheService.GetOrCreateAsync(
key: $"callback:{id}",
valueFactory: async () =>
{
await Task.Delay(50);
return new
{
Id = id,
Time = DateTimeOffset.UtcNow
};
},
region: "Callback");
return Results.Ok(result);
});
app.MapDelete("/cache/region/{region}", (string region, ICacheService cacheService) =>
{
var removed = cacheService.ClearRegion(region);
return Results.Ok(new { Region = region, Removed = removed });
});
app.MapGet("/cache/stats", (ICacheService cacheService) =>
{
return Results.Ok(cacheService.GetCacheStats());
});
app.Run();
常见问题
1. 为什么我设置了 region,不同分组还是可能冲突?
因为 region 的作用是“分组清理”,不是“键命名空间隔离”。
真正决定缓存项唯一性的核心仍然是 key。
2. 为什么建议加 KeyPrefix?
当同一个进程里存在多个模块、多个服务、多个业务域时,统一加前缀更容易避免键冲突。
3. Native AOT 可以用吗?
可以。本项目已按 AOT 兼容方式设计,适合在 Minimal API / Native AOT 项目中使用。
许可证
本项目采用 Apache 2.0 许可证。
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Caching.Memory (>= 10.0.7)
-
net9.0
- Microsoft.Extensions.Caching.Memory (>= 10.0.7)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
修复 开启了 KeyPrefix 时 GetOrCreate / GetOrCreateAsync 的实现里会先加一次前缀的问题