我有这个静态类
static class LocationMemoryCache
{
public static readonly ConcurrentDictionary<int, LocationCityContract> LocationCities = new();
}
我的过程
我的问题
如果收到更新城市的请求
我首先想到的解决方案
有没有办法从读/写中锁定/保留并发字典,然后在我完成后释放它?
这样当后台作业开始时,它可以只为自己锁定/保留字典,完成后它会释放它以供其他请求使用。
然后请求可能一直在等待字典发布并使用最新值更新它。
对其他可能的解决方案有什么想法吗?
编辑
如果我手动更新/删除数据库中的某些内容,我希望在后台作业再次运行后显示这些更改。这可能需要一天时间才能显示更改,我对此表示同意。
当 Api 启动时,我会阻止对这个特定“位置”项目的请求,直到后台作业将 IsReady 标记为 true。在我添加后台作业之前,我实现的缓存是线程安全的。
对于“位置”项目中总共 310,000 多条记录,我会说不到 10 秒。
我为什么选择这个答案
我选择 Xerillio 的答案是因为它通过跟踪日期时间解决了后台作业问题。类似于“对象版本”方法。我不会走这条路,因为我已经决定,如果我在数据库中进行手动更新,我不妨创建一个 API 路由来为我执行此操作,以便我可以同时更新数据库和缓存。所以我可能会删除后台作业,或者每周只运行一次。感谢您提供所有答案,我可以接受可能与我更新对象的方式不一致的数据,因为如果一条路由更新 2 个特定值而另一条路由更新 2 个不同的特定值,则出现问题的可能性非常小< /p>
编辑 2
假设我现在有这个缓存和 10,000 个活跃用户
static class LocationMemoryCache
{
public static readonly ConcurrentDictionary<int, LocationCityUserLogContract> LocationCityUserLogs = new();
}
我考虑过的事情
答案 0 :(得分:3)
我建议将 UpdatedAt
/CreatedAt
属性添加到您的 LocationCityContract
或创建具有此类属性的包装器对象 (CacheItem<LocationCityContract>
)。这样您就可以检查您要添加/更新的项目是否比现有对象新,如下所示:
public class CacheItem<T>
{
public T Item { get; }
public DateTime CreatedAt { get; }
// In case of system clock synchronization, consider making CreatedAt
// a long and using Environment.TickCount64. See comment from @Theodor
public CacheItem(T item, DateTime? createdAt = null)
{
Item = item;
CreatedAt = createdAt ?? DateTime.UtcNow;
}
}
// Use it like...
static class LocationMemoryCache
{
public static readonly
ConcurrentDictionary<int, CacheItem<LocationCityContract>> LocationCities = new();
}
// From some request...
var newItem = new CacheItem(newLocation);
// or the background job...
var newItem = new CacheItem(newLocation, updateStart);
LocationMemoryCache.LocationCities
.AddOrUpdate(
newLocation.Id,
newItem,
(_, existingItem) =>
newItem.CreatedAt > existingItem.CreatedAt
? newItem
: existingItem)
);
当一个请求想要更新缓存条目时,他们会像上面一样使用他们完成将项目添加到数据库时的时间戳(参见下面的注释)。
后台作业应在启动后立即保存时间戳(我们称之为 updateStart
)。然后它从数据库中读取所有内容并将项目添加到缓存中,如上所示,其中 CreatedAt
的 newLocation
设置为 updateStart
。这样,后台作业只更新自启动以来未更新的缓存项。也许您不是在后台作业中首先读取数据库中的所有项目,而是一次读取一个项目并相应地更新缓存。在这种情况下,应该在读取每个值之前设置 updateStart
(我们可以改为将其称为 itemReadStart
)。
由于更新缓存中项目的方式有点麻烦,而且您可能会从很多地方进行更新,因此您可以创建一个辅助方法,使对 LocationCities.AddOrUpdate
的调用更容易一些。
注意:
CreatedAt
设置为时间戳,它也可能无法真正反映最后更新的是哪个。由于您可以从手动更新数据库到后台作业更新缓存之间延迟 24 小时,因此这种竞争条件对您来说可能不是问题,因为后台作业会在运行时修复它。LocationMemoryCache[locationId].Item.CityName = updatedName
。相反,你应该例如像这样克隆它:// You need to implement a constructor or similar to clone the object
// depending on how complex it is
var newLoc = new LocationCityContract(LocationMemoryCache[locationId].Item);
newLoc.CityName = updatedName;
var newItem = new CacheItem(newLoc);
LocationMemoryCache.LocationCities
.AddOrUpdate(...); /* <- like above */
答案 1 :(得分:1)
不,无法根据读取/写入的需要锁定 ConcurrentDictionary
,然后在完成后将其释放。此类不提供此功能。您可以在每次访问 lock
时手动使用 ConcurrentDictionary
,但这样做将失去此专用类必须提供的所有优点(大量使用下的低争用),同时保留所有它的缺点(笨拙的 API、开销、分配)。
我的建议是使用受 Dictionary
保护的普通 lock
。这是一种悲观的方法,偶尔会导致一些线程不必要地阻塞,但它也很简单,易于推理其正确性。基本上所有对字典和数据库的访问都会被序列化:
lock
,并保留 lock
直到它完成读取对象。lock
(甚至在更新数据库之前),并保留lock
直到所有的属性对象已更新。lock
(甚至在查询数据库之前),并保留lock
直到新字典取代了旧的。如果证明这种简单方法的性能不可接受,您应该查看更复杂的解决方案。但是,此解决方案与下一个最简单的解决方案(也提供有保证的正确性)之间的复杂性差距可能非常大,因此您最好在走这条路之前有充分的理由。