我正在尝试为 API 创建自己的缓存实现。这是我第一次使用
ConcurrentDictionary
,我不知道我是否正确使用它。在测试中,有些东西抛出了错误,到目前为止我还无法再次重现它。出了什么问题?
private static readonly ConcurrentDictionary<string, ThrottleInfo> CacheList =
new ConcurrentDictionary<string, ThrottleInfo>();
public override void OnActionExecuting(HttpActionContext actionExecutingContext)
{
if (CacheList.TryGetValue(userIdentifier, out var throttleInfo))
{
if (DateTime.Now >= throttleInfo.ExpiresOn)
{
if (CacheList.TryRemove(userIdentifier, out _))
{
//TODO:
}
}
else
{
if (throttleInfo.RequestCount >= defaultMaxRequest)
{
actionExecutingContext.Response = ResponseMessageExtension
.TooManyRequestHttpResponseMessage();
}
else
{
throttleInfo.Increment();
}
}
}
else
{
if (CacheList.TryAdd(userIdentifier, new ThrottleInfo(Seconds)))
{
//TODO:
}
}
}
public class ThrottleInfo
{
private int _requestCount;
public int RequestCount => _requestCount;
public ThrottleInfo(int addSeconds)
{
Interlocked.Increment(ref _requestCount);
ExpiresOn = ExpiresOn.AddSeconds(addSeconds);
}
public void Increment()
{
// this is about as thread safe as you can get.
// From MSDN: Increments a specified variable and stores the result,
// as an atomic operation.
Interlocked.Increment(ref _requestCount);
// you can return the result of Increment if you want the new value,
// but DO NOT set the counter to the result.
// [i.e. counter = Interlocked.Increment(ref counter);]
// This will break the atomicity.
}
public DateTime ExpiresOn { get; } = DateTime.Now;
}
如果我明白您在 ExpiresOn 已通过时要做什么,请删除该条目,否则更新它或添加(如果不存在)。 您当然可以利用 AddOrUpdateMethod 来简化一些代码。 看看这里的一些很好的例子:https://learn.microsoft.com/en-us/dotnet/standard/collections/thread-safe/how-to-add-and-remove-items 希望这有帮助。
ConcurrentDictionary
仅在以下情况下才足以作为线程安全容器:(1)需要保护的整个状态是其内部状态(它包含的键和值),并且仅当(2)此状态可以使用它提供的专用 API 进行原子突变(GetOrAdd
、AddOrUpdate
)。在您的情况下,不满足第二个要求,因为您需要根据其值的状态有条件地删除键,并且ConcurrentDictionary
类不支持这种情况。
所以你当前的缓存实现不是线程安全的。偶尔抛出异常的事实纯属巧合。如果它是完全防抛出的,它仍然是非线程安全的,因为它不会完全防错,这意味着它可能偶尔(或永久)转换到与其规范不兼容的状态(例如返回过期值) ).
关于
ThrottleInfo
类,它存在一个可见性错误,如果您在一台机器上广泛测试该类,则该错误可能不会被观察到,然后当您在另一台具有不同 CPU 架构的机器上部署应用程序时突然出现。非 volatile private int _requestCount
字段通过公共属性 RequestCount
公开,因此不能保证(基于 C# 规范)所有线程都会看到其最新值。您可以阅读 Igor Ostrovsky 的这篇文章,了解内存模型的特殊性,这可能会让您(像我一样)相信,在多线程代码中使用无锁技术(在本例中使用 Interlocked
类)比在多线程代码中使用更麻烦。这是值得的。如果您阅读并喜欢它,还有本文的第 2 部分。