【问题标题】:Caching results from asynchronous methods in ASP.NET在 ASP.NET 中缓存异步方法的结果
【发布时间】:2016-09-27 00:55:06
【问题描述】:

我有一个简单的 MVC 控制器,我使用 await Task.WhenAll 从 Web 服务获取数据进行了异步处理。我想缓存这些调用的结果,这样我就不必一直调用 API。目前,当我得到响应时,我正在视图控制器中缓存结果,但理想情况下,我希望我正在调用的方法(它调用 API)来处理缓存。问题是该方法还不能访问结果,因为它是异步的并且只返回一个任务。

是否有可能有另一种方法在返回结果后对其进行缓存?

public async Task<ActionResult> Index()
{
    // Get data asynchronously
    var languagesTask = GetDataAsync<List<Language>>("languages");
    var currenciesTask = GetDataAsync<List<Currency>>("currencies");

    await Task.WhenAll(languagesTask, currenciesTask);


    // Get results
    List<Language> languages = languagesTask.Result;
    List<Currency> currencies = currenciesTask.Result;


    // Add results to cache
    AddToCache("languages", languages);
    AddToCache("currencies", currencies);


    // Add results to view and return
    ViewBag.languages = languages;
    ViewBag.currencies = currencies;

    return View();
}

public async Task<T> GetDataAsync<T>(string operation)
{
    // Check cache for data first
    string cacheName = operation;

    var cacheData = HttpRuntime.Cache[cacheName];

    if (cacheData != null)
    {
        return (T)cacheData;
    }


    // Get data from remote api
    using (HttpClient client = new HttpClient())
    {
        client.BaseAddress = new Uri("https://myapi.com/");

        var response = await client.GetAsync(operation);

        // Add result to cache
        //...

        return (await response.Content.ReadAsAsync<T>());
    }
}

【问题讨论】:

标签: c# asp.net asp.net-mvc caching asynchronous


【解决方案1】:

只要您的缓存实现在内存中,您就可以缓存任务本身而不是任务结果:

public Task<T> GetDataAsync<T>(string operation)
{
  // Check cache for data first
  var task = HttpRuntime.Cache[operation] as Task<T>;
  if (task != null)
    return task;

  task = DoGetDataAsync(operation);
  AddToCache(operation, task);
  return task;
}

private async Task<T> DoGetDataAsync<T>(string operation)
{
  // Get data from remote api
  using (HttpClient client = new HttpClient())
  {
    client.BaseAddress = new Uri("https://myapi.com/");
    var response = await client.GetAsync(operation);
    return (await response.Content.ReadAsAsync<T>());
  }
}

这种方法还有一个额外的好处,即如果多个 HTTP 请求尝试获取相同的数据,它们实际上会共享任务本身。所以它共享实际的异步操作而不是结果。

但是,这种方法的缺点是 Task&lt;T&gt; 不可序列化,因此如果您使用自定义的磁盘支持缓存或共享缓存(例如 Redis),那么这种方法将不起作用。

【讨论】:

  • 感谢您的回复。这看起来是一个很好的解决方案。我正在使用内存缓存,因此将通过缓存任务进行测试,看看这是否适合应用程序。
【解决方案2】:

回答这个问题有点晚了,但我认为我可以使用一个名为 LazyCache 的开源库来改进 Stevens 的回答,这将在几行代码中为您完成。它最近被更新为处理这种情况下的内存中的缓存任务。它也可以在 nuget 上使用。

鉴于您的获取数据方法是这样的:

private async Task<T> DoGetDataAsync<T>(string operation)
{
  // Get data from remote api
  using (HttpClient client = new HttpClient())
  {
    client.BaseAddress = new Uri("https://myapi.com/");
    var response = await client.GetAsync(operation);
    return (await response.Content.ReadAsAsync<T>());
  }
}

那么你的控制器就变成了

public async Task<ActionResult> Index()
{
    // declare but don't execute a func unless we need to prime the cache
    Func<Task<List<Language>>> languagesFunc = 
        () => GetDataAsync<List<Currency>>("currencies");     

    // get from the cache or execute the func and cache the result   
    var languagesTask = cache.GetOrAddAsync("languages", languagesFunc);

    //same for currencies
    Func<Task<List<Language>>> currenciesFunc = 
        () => GetDataAsync<List<Currency>>("currencies");
    var currenciesTask = cache.GetOrAddAsync("currencies", currenciesFunc);

    // await the responses from the cache (instant) or the api (slow)
    await Task.WhenAll(languagesTask, currenciesTask);

    // use the results
    ViewBag.languages = languagesTask.Result;
    ViewBag.currencies = currenciesTask.Result;

    return View();
}

默认情况下它内置锁定,因此可缓存方法只会在每次缓存未命中时执行一次,并且它使用 lamda,因此您可以一次性执行“获取或添加”。它默认为 20 分钟滑动到期,但您可以在其上设置任何您喜欢的缓存策略。

有关缓存任务的更多信息在api docs 中,您可能会发现sample webapi app to demo caching tasks 很有用。

(免责声明:我是 LazyCache 的作者)

【讨论】:

    【解决方案3】:

    我建议你使用MemoryCache

    MemoryCache cache = MemoryCache.Default;
    string cacheName = "mycachename";
    
    if cache.Contains(cacheName) == false || cache[cacheName] == null)
    {
        // load data
        var data = await response.Content.ReadAsAsync<T>();
        // save data to cache
        cache.Set(cacheName, data, new CacheItemPolicy() { SlidingExpiration = DateTime.Now.AddDays(1).TimeOfDay });
    }
    
    return cache[cacheName];
    

    【讨论】:

    • 您错过了问题的一个重要部分,即如何缓存异步方法的结果。您能否将此添加到您的示例中?
    • 为什么异步使缓存有任何不同?它没有恕我直言。它就是例子。
    • 你说得对,我没有仔细阅读。我猜你的解决方案应该可行。
    • 我想您可能在我构建示例时已经阅读过它。我已经通过多次编辑构建了它。
    猜你喜欢
    • 1970-01-01
    • 2011-10-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-01
    相关资源
    最近更新 更多