在异步服务中缓存
Caching in an asynchronous service
所以,我 运行 遇到了一个有趣的情况。假设我有一组很少更改的数据,所以我不想在每次调用大容量服务时都获取它。我通常会将此数据缓存一段时间(取决于实际更改的频率)。这非常简单:
private T GetOrSet<T>(string key, Func<T> getToSet, int minutes, object lockObject)
{
T value = (T)HttpRuntime.Cache.Get(key);
if (value == null)
{
lock (lockObject)
{
value = (T)HttpRuntime.Cache.Get(key);
if (value == null)
{
value = getToSet();
HttpRuntime.Cache.Insert(key, value, null, DateTime.UtcNow.AddMinutes(minutes), Cache.NoSlidingExpiration);
}
}
}
return value;
}
如果 getToSet
函数失败,我从不插入,因此会在下一次调用时再次尝试。
但是,如果我在异步服务中使用相同的模式,getToSet
函数 returns 一个 Task<>
- 现在我已经缓存了一个 Task
returns一段时间的失败结果。假设调用 getToSet
的时间不可忽略,如何在数据检索期间不阻塞线程的情况下防止缓存失败的结果?
下面是简短但完整的示例;这会将失败的结果缓存一分钟,在此期间,对 GetData
的每次调用都会失败,然后该服务将开始为每次调用返回 5
。
using System;
using System.ServiceModel;
using System.Threading.Tasks;
using System.Web;
using System.Web.Caching;
namespace TaskCacheIssue
{
[ServiceContract]
public interface IService1
{
[OperationContract]
Task<int> GetData();
}
public class Service1 : IService1
{
public async Task<int> GetData()
{
return await GetDataFromCache();
}
private static bool first = true;
private static readonly object deadbolt = new object();
private async Task<int> GetDataFromCache()
{
return await GetOrSet(
"key",
async () => await GetDataFromSomewhereElse(),
1,
deadbolt);
}
private async Task<int> GetDataFromSomewhereElse()
{
// This is actually a longer-running data retrieval.
if (first)
{
first = false;
throw new Exception("FIRST!");
}
return 5;
}
private T GetOrSet<T>(string key, Func<T> getToSet, int minutes, object lockObject)
{
T value = (T)HttpRuntime.Cache.Get(key);
if (value == null)
{
lock (lockObject)
{
value = (T)HttpRuntime.Cache.Get(key);
if (value == null)
{
value = getToSet();
HttpRuntime.Cache.Insert(key, value, null, DateTime.UtcNow.AddMinutes(minutes), Cache.NoSlidingExpiration);
}
}
}
return value;
}
}
}
从缓存中检索值并等待它时检查结果。如果失败,清除缓存并向其中插入一个新值。
不是专门评估空值,而是另外检查缓存值是否是失败的任务。
private T GetOrSet<T>(string key, Func<T> getToSet, int minutes, object lockObject)
{
T value = (T)HttpRuntime.Cache.Get(key);
if (value == null || IsCanceledOrFaultedTask(value))
{
lock (lockObject)
{
value = (T)HttpRuntime.Cache.Get(key);
if (value == null || IsCanceledOrFaultedTask(value))
{
value = getToSet();
HttpRuntime.Cache.Insert(key, value, null, DateTime.UtcNow.AddMinutes(minutes), Cache.NoSlidingExpiration);
}
}
}
return value;
}
private bool IsCanceledOrFaultedTask(object target)
{
if (target is Task)
{
var task = (Task)target;
return (task.IsCanceled || task.IsFaulted);
}
return false;
}
所以,我 运行 遇到了一个有趣的情况。假设我有一组很少更改的数据,所以我不想在每次调用大容量服务时都获取它。我通常会将此数据缓存一段时间(取决于实际更改的频率)。这非常简单:
private T GetOrSet<T>(string key, Func<T> getToSet, int minutes, object lockObject)
{
T value = (T)HttpRuntime.Cache.Get(key);
if (value == null)
{
lock (lockObject)
{
value = (T)HttpRuntime.Cache.Get(key);
if (value == null)
{
value = getToSet();
HttpRuntime.Cache.Insert(key, value, null, DateTime.UtcNow.AddMinutes(minutes), Cache.NoSlidingExpiration);
}
}
}
return value;
}
如果 getToSet
函数失败,我从不插入,因此会在下一次调用时再次尝试。
但是,如果我在异步服务中使用相同的模式,getToSet
函数 returns 一个 Task<>
- 现在我已经缓存了一个 Task
returns一段时间的失败结果。假设调用 getToSet
的时间不可忽略,如何在数据检索期间不阻塞线程的情况下防止缓存失败的结果?
下面是简短但完整的示例;这会将失败的结果缓存一分钟,在此期间,对 GetData
的每次调用都会失败,然后该服务将开始为每次调用返回 5
。
using System;
using System.ServiceModel;
using System.Threading.Tasks;
using System.Web;
using System.Web.Caching;
namespace TaskCacheIssue
{
[ServiceContract]
public interface IService1
{
[OperationContract]
Task<int> GetData();
}
public class Service1 : IService1
{
public async Task<int> GetData()
{
return await GetDataFromCache();
}
private static bool first = true;
private static readonly object deadbolt = new object();
private async Task<int> GetDataFromCache()
{
return await GetOrSet(
"key",
async () => await GetDataFromSomewhereElse(),
1,
deadbolt);
}
private async Task<int> GetDataFromSomewhereElse()
{
// This is actually a longer-running data retrieval.
if (first)
{
first = false;
throw new Exception("FIRST!");
}
return 5;
}
private T GetOrSet<T>(string key, Func<T> getToSet, int minutes, object lockObject)
{
T value = (T)HttpRuntime.Cache.Get(key);
if (value == null)
{
lock (lockObject)
{
value = (T)HttpRuntime.Cache.Get(key);
if (value == null)
{
value = getToSet();
HttpRuntime.Cache.Insert(key, value, null, DateTime.UtcNow.AddMinutes(minutes), Cache.NoSlidingExpiration);
}
}
}
return value;
}
}
}
从缓存中检索值并等待它时检查结果。如果失败,清除缓存并向其中插入一个新值。
不是专门评估空值,而是另外检查缓存值是否是失败的任务。
private T GetOrSet<T>(string key, Func<T> getToSet, int minutes, object lockObject)
{
T value = (T)HttpRuntime.Cache.Get(key);
if (value == null || IsCanceledOrFaultedTask(value))
{
lock (lockObject)
{
value = (T)HttpRuntime.Cache.Get(key);
if (value == null || IsCanceledOrFaultedTask(value))
{
value = getToSet();
HttpRuntime.Cache.Insert(key, value, null, DateTime.UtcNow.AddMinutes(minutes), Cache.NoSlidingExpiration);
}
}
}
return value;
}
private bool IsCanceledOrFaultedTask(object target)
{
if (target is Task)
{
var task = (Task)target;
return (task.IsCanceled || task.IsFaulted);
}
return false;
}