【问题标题】:Multiple requests hitting the database – wait until cache is available多个请求访问数据库 - 等待缓存可用
【发布时间】:2020-02-28 13:32:37
【问题描述】:

我们正在使用简单的Memory Cache 来缓存密集查询的响应,否则每秒会被多次命中。缓存非常快,偏移量设置为 2 秒。

public StateService
{
    private readonly ObjectCache _cache = MemoryCache.Default;
    private const string _queueStatesCacheKey = "_states";

    public IList<States> GetStates()
    {
        var states = _cache.Get(_statesCacheKey);
                if (states== null)
                {
                    states = getStatesFromDatabase();
                    _cache.Set(_statesCacheKey, states, DateTimeOffset.Now.AddSeconds(Settings.AppSettings.QueueStateCacheExpiration));
                }
       return states as List<States>;
    }
}

方法:getStatesFromDatabase() 只需运行 SQL-query 即可运行存储过程并获取这些值。

这都是通过API 获取的,目前我们遇到的情况是缓存被清除,getStatesFromDatabase() 同时被 4-5 个线程访问。

  • 我可以将GetStates() 包装在Task 中,然后等到它完成后再让另一个请求启动一个新任务吗?
  • 或者我是否必须将其设为单例服务才能正常工作?
  • 或者我是否必须创建一种新的提供程序来处理返回List&lt;States&gt;
  • 以不同的方式缓存它还是在其他地方缓存更好?

【问题讨论】:

    标签: c# sql asp.net caching asp.net-web-api


    【解决方案1】:

    使用SemaphoreSlim 锁定其他线程,直到缓存准备好:

    public StateService
    {
        private readonly ObjectCache _cache = MemoryCache.Default;
        private const string _queueStatesCacheKey = "_states";
        private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
    
        public IList<States> GetStates()
        {
            var states = _cache.Get(_statesCacheKey);
            if (states== null)
            {
               try
               {
                  semaphore.Wait();
                  // we check again from cache, that could have been populated from other thread
                  states = _cache.Get(_statesCacheKey);
                  if(states != null) return states as IList<States>;
                  states = getStatesFromDatabase();
                  _cache.Set(_statesCacheKey, states, DateTimeOffset.Now.AddSeconds(Settings.AppSettings.QueueStateCacheExpiration));
               }
               finally
               {
                  semaphore.Release();
               }
            }
            return states as List<States>;
        }
    }
    

    您可以配置无超时(如示例中所示)或某种超时,可能基于查询的运行时间统计信息。

    请注意,这仅适用于单服务器和单进程设置。如果您的应用程序要在多个服务器或多个进程上运行,则此解决方案将允许每个服务器/进程进行一个并发查询。

    为避免这种情况,您需要检查一些用于从外部管理锁的包(分布式锁)。例如,Nuget 包 DistributedLock 就是可以完成这项工作的。

    【讨论】:

    • 不知道 Semaphore - 看起来正是我需要的。服务器是集群的,但负载均衡器目前只命中一台服务器,所以现在可以正常使用。谢谢:)
    • 好的。如果查询运行相对较快(0.x 秒)并且不会使数据库受到影响,我认为您也可以在集群环境中使用此解决方案......否则,分布式锁定 分布式真的推荐缓存。
    • 小提示是我必须执行“new SemaphoreSlim(1)” - 否则它不会继续通过“Wait()”函数。
    • @HenrikM 你说得对,先生! :) 从文档(facepalm)复制时,我忘记每次都更改0 =&gt; 1。立即修复答案,感谢报告!
    【解决方案2】:

    GetStates() 包装在Task 中并等待它不会解决任何问题,因为您正在使用asp.net 应用程序,每个用户请求都会在新的Thread 中收到。一个请求在返回数据之前等待另一个请求获取某些内容并不是一个好主意。正如我看到实现你的方法,你将不得不锁定方法或维护一些单例中已经运行的任务列表,你将不得不担心线程安全、任务取消等。它会使事情复杂化而不是解决问题。

    如果您的GetStates() 不经常被调用,那么您所拥有的实际上是很好的实现。 替代解决方案是在内存中维护列表,始终返回该列表并通过每 x 秒轮询数据库来更新它。这样,无论有多少用户来,总是只有一个线程从数据库中检索数据。

    基本上,如果您遇到一段时间没有人查询您的服务的情况,那么您的方法很好,如果您的服务足够繁忙,则每 x 秒轮询一次并始终返回列表会更好。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-12-13
      • 2011-06-10
      • 1970-01-01
      • 2020-10-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-09
      相关资源
      最近更新 更多