几个月前,我们的任务是有点类似的 POC。在我们的例子中,我们需要Memoization,它将每个输入的操作结果缓存一段特定的时间。换句话说,如果您发出相同的操作(使用相同的输入调用相同的方法)并且您处于预定义的时间范围内,那么响应将从缓存中提供,否则它将执行原始请求。
首先我们介绍了以下帮助类:
class ValueWithTTL<T>
{
public Lazy<T> Result { get; set; }
public DateTime ExpiresAt { get; set; }
}
ExpiresAt 代表未来某个时间Result 变得陈旧。
我们使用了ConcurrentDictionary 来存储缓存的结果。
这里是Memoizer帮助类的简化版:
public static class Memoizer
{
public static Func<K, V> Memoize<K, V>(this Func<K, V> toBeMemoized, int ttlInMs = 5*1000)
where K : IComparable
{
var memoizedValues = new ConcurrentDictionary<K, ValueWithTTL<V>>();
var ttl = TimeSpan.FromMilliseconds(ttlInMs);
return (input) =>
{
if (memoizedValues.TryGetValue(input, out var valueWithTtl))
{
if (DateTime.UtcNow >= valueWithTtl.ExpiresAt)
{
memoizedValues.TryRemove(input, out _);
valueWithTtl = null;
Console.WriteLine($"!!!'{input}' has expired");
}
}
if (valueWithTtl != null)
return valueWithTtl.Result.Value;
var toBeCached = new Lazy<V>(() => toBeMemoized(input));
var toBeExpired = DateTime.UtcNow.AddMilliseconds(ttlInMs);
var toBeCachedWithTimestamp = new ValueWithTTL<V> { Result = toBeCached, ExpiresAt = toBeExpired};
memoizedValues.TryAdd(input, toBeCachedWithTimestamp);
return toBeCachedWithTimestamp.Result.Value;
};
}
}
- 它接收一个
Func,只有当给定的input 不存在于memoizedValues 中或它存在但ExpiresAt 比现在小时才会执行。
-
Memoize 返回一个与toBeMemoized 具有相同签名的Func,因此它可以很好地用作装饰器或包装器。
这里是同步探测:
private static readonly WebClient wclient = new WebClient();
private static string[] uris = { "http://google.com", "http://9gag.com", "http://stackoverflow.com", "http://gamepod.hu", "http://google.com", "http://google.com", "http://stackoverflow.com" };
static void SyncProbe(IEnumerable<string> uris)
{
var getAndCacheContent = Memoizer.Memoize<string, string>(wclient.DownloadString);
var rand = new Random();
foreach (var uri in uris)
{
var sleepDuration = rand.Next() % 1500;
Thread.Sleep(sleepDuration);
Console.WriteLine($"Slept: {sleepDuration}ms");
var sp = Stopwatch.StartNew();
_ = getAndCacheContent(uri);
sp.Stop();
Console.WriteLine($"'{uri}' request took {sp.ElapsedMilliseconds}ms");
}
}
这里是异步探测:
private static readonly HttpClient client = new HttpClient();
private static string[] uris = { "http://google.com", "http://9gag.com", "http://stackoverflow.com", "http://gamepod.hu", "http://google.com", "http://google.com", "http://stackoverflow.com" };
static async Task AsyncProbe(IEnumerable<string> uris)
{
var getAndCacheContent = Memoizer.Memoize<string, Task<string>>(client.GetStringAsync);
var downloadTasks = new List<Task>();
var rand = new Random();
foreach (var uri in uris)
{
var sleepDuration = rand.Next() % 1500;
await Task.Delay(sleepDuration);
Console.WriteLine($"Slept: {sleepDuration}ms");
downloadTasks.Add(Task.Run(async () =>
{
var sp = Stopwatch.StartNew();
_ = await getAndCacheContent(uri);
sp.Stop();
Console.WriteLine($"'{uri}' request took {sp.ElapsedMilliseconds}ms");
}));
}
await Task.WhenAll(downloadTasks.ToArray());
}
最后是一个示例输出
Slept: 983ms
'http://google.com' request took 416ms
Slept: 965ms
'http://9gag.com' request took 601ms
Slept: 442ms
'http://stackoverflow.com' request took 803ms
Slept: 1047ms
'http://gamepod.hu' request took 267ms
Slept: 844ms
!!!'http://google.com' has expired
'http://google.com' request took 201ms
Slept: 372ms
'http://google.com' request took 0ms
Slept: 302ms
'http://stackoverflow.com' request took 0ms
- 如您所见,我们向 google 发出了 3 个请求,其中 2 个已正常执行,其中 1 个从缓存中提供。无法从缓存中提供第二次尝试,因为它已过时。
- 我们针对 stackoverflow 发出了 2 个请求,第一个请求正常执行,第二个请求从缓存中提供。
这只是一个 POC,因此还有很大的改进空间: