.NET Core 5中IMemoryCache序列化对象引发内存不足问题求解
解决方案
针对高并发下反序列化大JSON导致的内存不足问题,提供以下几种可行方案,按优先级排序:
1. 标记对象为不可变,直接返回缓存实例(最优解)
如果业务场景中前端仅展示目录数据、不会修改实例并提交回后端,将Catalog及内部嵌套对象(如Product)定义为不可变类型,直接返回缓存的实例即可,完全避免拷贝/反序列化的内存开销。
代码示例
// 用record定义不可变类型(或手动实现get-only属性+构造函数) public record Catalog(IReadOnlyList<Product> Products); public record Product(int Id, string Name, string Category, decimal Price); // 修改缓存方法 private readonly object _lockObj = new object(); public Catalog GetCatalog(IMemoryCache cache) { var cachedCatalog = cache.Get<Catalog>("catalogKey"); if (cachedCatalog == null) { lock (_lockObj) { // double-check锁确保线程安全 cachedCatalog = cache.Get<Catalog>("catalogKey"); if (cachedCatalog == null) { cachedCatalog = loadCatalogData(); // 返回不可变实例 cache.Set("catalogKey", cachedCatalog, TimeSpan.FromHours(3)); } } } return cachedCatalog; // 直接返回缓存实例,不可变无需担心修改 }
2. 缓存原始对象,每次请求返回深拷贝实例
放弃缓存JSON字符串,改为缓存加载好的Catalog对象,每次请求时对缓存对象做深拷贝,避免JSON序列化/反序列化的双重内存消耗(JSON字符串本身比强类型对象占用更多内存)。
代码示例
private readonly object _lockObj = new object(); public Catalog GetCatalog(IMemoryCache cache) { var cachedCatalog = cache.Get<Catalog>("catalogKey"); if (cachedCatalog == null) { lock (_lockObj) { cachedCatalog = cache.Get<Catalog>("catalogKey"); if (cachedCatalog == null) { cachedCatalog = loadCatalogData(); cache.Set("catalogKey", cachedCatalog, TimeSpan.FromHours(3)); } } } // 返回深拷贝实例,确保每个用户拿到独立对象 return DeepClone(cachedCatalog); } // 实现深拷贝(可选更高效的方式,如BinaryFormatter/手动拷贝) private Catalog DeepClone(Catalog source) { var json = JsonSerializer.Serialize(source); return JsonSerializer.Deserialize<Catalog>(json); }
3. 使用对象池复用大对象实例
利用.NET的Microsoft.Extensions.ObjectPool预创建多个Catalog实例,每次请求时从池中取出实例,将缓存的原始数据复制到实例中,用完后返回池。这种方式能减少大对象频繁创建导致的内存碎片化和GC压力。
代码示例
private readonly ObjectPool<Catalog> _catalogPool; private readonly object _lockObj = new object(); // 构造函数注入对象池 public YourService(ObjectPool<Catalog> catalogPool) { _catalogPool = catalogPool; } public Catalog GetCatalog(IMemoryCache cache) { var cachedCatalog = cache.Get<Catalog>("catalogKey"); if (cachedCatalog == null) { lock (_lockObj) { cachedCatalog = cache.Get<Catalog>("catalogKey"); if (cachedCatalog == null) { cachedCatalog = loadCatalogData(); cache.Set("catalogKey", cachedCatalog, TimeSpan.FromHours(3)); } } } // 从对象池获取实例 var instance = _catalogPool.Get(); // 将缓存数据复制到实例(需实现属性映射逻辑,如手动赋值/AutoMapper) MapCatalogData(cachedCatalog, instance); return instance; } // 用ActionFilter回收实例(需注册到管道) public class CatalogPoolFilter : IAsyncActionFilter { private readonly ObjectPool<Catalog> _catalogPool; public CatalogPoolFilter(ObjectPool<Catalog> catalogPool) { _catalogPool = catalogPool; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var resultContext = await next(); if (resultContext.Result is ObjectResult objResult && objResult.Value is Catalog catalog) { // 重置实例状态后返回池 ResetCatalog(catalog); _catalogPool.Return(catalog); } } } // 辅助方法:重置实例状态 private void ResetCatalog(Catalog catalog) { catalog.Products.Clear(); // 假设Products是可清空集合,需根据实际结构调整 // 重置其他属性... } // 辅助方法:映射数据到实例 private void MapCatalogData(Catalog source, Catalog target) { // 手动复制属性,或用AutoMapper target.Id = source.Id; target.Products.AddRange(source.Products); // 复制其他属性... }
4. 拆分缓存粒度,按需加载
不缓存整个大目录,按类别、品牌等维度拆分多个小缓存块,用户请求时仅加载所需部分,大幅降低单次请求的内存占用。
代码示例
public List<Product> GetProductsByCategory(IMemoryCache cache, string category) { var cacheKey = $"catalog_category_{category}"; var products = cache.Get<List<Product>>(cacheKey); if (products == null) { // 从全量目录中提取对应类别数据(或直接查询数据库) var fullCatalog = GetFullCatalog(cache); products = fullCatalog.Products.Where(p => p.Category.Equals(category, StringComparison.OrdinalIgnoreCase)).ToList(); cache.Set(cacheKey, products, TimeSpan.FromHours(3)); } // 返回浅拷贝(如果Product是只读或无需修改) return new List<Product>(products); }
内容的提问来源于stack exchange,提问作者James
相关产品推荐
相关产品推荐

