首页 > 解决方案 > C# LazyCache 并发字典垃圾回收

问题描述

基于 Web 的 .Net(C#) 应用程序存在一些问题。我正在使用LazyCache库在用户会话中为属于同一公司的用户缓存频繁的 JSON 响应(一些在 80+KB 左右)。

我们需要做的一件事是跟踪特定公司的缓存键,因此当公司中的任何用户对正在缓存的项目进行变异更改时,我们需要清除这些项目的缓存以供该特定公司的用户强制执行在接收到下一个请求后立即重新填充缓存。

我们选择LazyCache库是因为我们想在内存中执行此操作,而无需使用 Redis 等外部缓存源,因为我们没有大量使用。

我们使用这种方法遇到的一个问题是,我们需要在缓存的任何时候跟踪属于特定客户的所有缓存键。因此,当公司用户对相关资源进行任何变异更改时,我们需要使属于该公司的所有缓存键过期。

为了实现这一点,我们有一个所有 Web 控制器都可以访问的全局缓存。

private readonly IAppCache _cache = new CachingService();

protected IAppCache GetCache()
{
    return _cache;
}

使用此缓存的控制器的简化示例(请原谅任何拼写错误!)如下所示

[HttpGet]
[Route("{customerId}/accounts/users")]
public async Task<Users> GetUsers([Required]string customerId)
{
    var usersBusinessLogic = await _provider.GetUsersBusinessLogic(customerId)

    var newCacheKey= "GetUsers." + customerId;

    CacheUtil.StoreCacheKey(customerId,newCacheKey)

    return await GetCache().GetOrAddAsync(newCacheKey, () => usersBusinessLogic.GetUsers(), DateTimeOffset.Now.AddMinutes(10));
}

我们使用带有静态方法的 util 类和静态并发字典来存储缓存键 - 每个公司 (GUID) 可以有许多缓存键。

private static readonly ConcurrentDictionary<Guid, ConcurrentHashSet<string>> cacheKeys = new ConcurrentDictionary<Guid, ConcurrentHashSet<string>>();

public static void StoreCacheKey(Guid customerId, string newCacheKey)
{
    cacheKeys.AddOrUpdate(customerId, new ConcurrentHashSet<string>() { newCacheKey }, (key, existingCacheKeys) =>
    {
        existingCacheKeys.Add(newCacheKey);
        return existingCacheKeys;
    });
}

在同一个 util 类中,当我们需要删除特定公司的所有缓存键时,我们有一个类似于下面的方法(这是在其他控制器中进行变异更改时引起的)

public static void ClearCustomerCache(IAppCache cache, Guid customerId)
{
    var customerCacheKeys = new ConcurrentHashSet<string>();

    if (!cacheKeys.TryGetValue(customerId,out customerCacheKeys))
    {
        return new ConcurrentHashSet<string>();
    }


    foreach (var cacheKey in customerCacheKeys)
    {
        cache.Remove(cacheKey);
    }

    cacheKeys.TryRemove(customerId, out _);
}

我们最近遇到了性能问题,即我们的 Web 请求响应时间会随着时间的推移而显着减慢 - 我们看不到每秒请求数的显着变化。

查看垃圾收集指标,我们似乎注意到第 2 代堆大小和对象大小似乎在上升——我们没有看到内存被回收。

我们仍在调试中,但我想知道使用上述方法是否会导致我们看到的问题。我们想要线程安全,但是使用我们上面的并发字典是否会出现问题,即使我们删除了内存没有被释放的项目,从而导致过多的 Gen 2 收集。

此外,我们正在使用工作站垃圾收集模式,想象切换到服务器模式 GC 会帮助我们(我们的 IIS 服务器有 8 个处理器 + 16 GB 内存),但不确定切换会解决所有问题。

标签: c#multithreadingperformancegarbage-collectionlazycache

解决方案


大对象 (> 85k) 属于第 2 代大对象堆 (LOH),它们被固定在内存中。

  1. GC 扫描 LOH 并标记死对象
  2. 相邻的死对象组合成空闲内存
  3. LOH压缩
  4. 进一步的分配只是试图填补死对象留下的空洞。

气相色谱 LOH

没有压缩,但只有重新分配可能会导致内存碎片。长时间运行的服务器进程可以通过这个来完成 - 这并不罕见。您可能会看到随着时间的推移出现碎片。

服务器 GC 恰好是多线程的——我不希望它能解决碎片问题。

您可以尝试分解您的大对象 - 这对于您的应用程序可能不可行。

LargeObjectHeapCompaction您可以在缓存清除后尝试设置- 假设它不常见。

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

最终,我建议对堆进行分析以找出有效的方法。


推荐阅读