如何锁定ConcurrentDictionary避免后台更新与接口请求的数据冲突?
可行解决方案
针对你遇到的缓存更新竞态问题,以下是几种可落地的实现方案:
方案1:用ReaderWriterLockSlim做读写控制
ConcurrentDictionary仅能保证单操作的线程安全,跨操作的原子性需要额外锁控制,ReaderWriterLockSlim适合读多写少的缓存场景,性能优于普通lock:
- 封装缓存的所有读写、更新操作,不对外直接暴露
ConcurrentDictionary实例 - 读请求获取读锁,支持多线程并行读取,不影响接口性能
- 接口更新操作和后台全量重载操作获取写锁,保证两类更新操作互斥
- 你的全量重载仅需10秒、且运行频率极低(每日/每周一次),短暂的阻塞对业务几乎无影响
示例核心代码:
static class LocationMemoryCache { private static readonly ConcurrentDictionary<int, LocationCityContract> _locationCities = new(); private static readonly ReaderWriterLockSlim _lock = new(); // 读方法 public static LocationCityContract Get(int cityId) { _lock.EnterReadLock(); try { _locationCities.TryGetValue(cityId, out var val); return val; } finally { _lock.ExitReadLock(); } } // 单条更新方法 public static void Update(int cityId, LocationCityContract newVal) { _lock.EnterWriteLock(); try { _locationCities[cityId] = newVal; } finally { _lock.ExitWriteLock(); } } // 全量重载方法 public static void ReloadAll(Dictionary<int, LocationCityContract> allData) { _lock.EnterWriteLock(); try { _locationCities.Clear(); foreach (var item in allData) { _locationCities.TryAdd(item.Key, item.Value); } } finally { _lock.ExitWriteLock(); } } }
方案2:版本戳比对避免无效覆盖
类似你提到的对象版本号思路,完全无锁不阻塞请求:
- 给
LocationCityContract增加LastUpdatedTime时间戳字段 - 接口更新缓存时,将该字段设置为当前时间
- 后台任务全量重载时,不直接覆盖缓存值,先比对当前缓存中对象的
LastUpdatedTime:- 若数据库查询到的记录更新时间 > 缓存中的时间,才覆盖缓存
- 若缓存中的时间更新,说明是接口刚更新的新值,跳过本条的覆盖逻辑
方案3:原子替换缓存实例
零阻塞实现全量重载,完全不影响线上请求:
- 后台任务全量查询数据库时,先在内存中构造一个全新的
ConcurrentDictionary实例,把所有查询到的数据写入新实例 - 新实例构造完成后,原子替换静态类中的缓存引用
- 额外维护一个线程安全的临时变更队列,接口更新数据库时,同时把更新操作写入队列
- 后台任务完成实例替换后,重放队列中的所有更新到新实例,再清空队列,避免替换过程中的接口更新丢失
方案4:从根源避免竞态
结合你的业务场景,该方案成本最低:
- 取消定时运行的后台重载任务,新增一个权限控制的手动重载接口
- 仅当你手动修改数据库后,主动调用该接口触发缓存重载
- 完全消除定时任务和实时接口的竞态可能,实现成本最低
针对你补充的用户维度缓存场景,由于仅用户本人可更新对应对象、更新频率低,冲突概率极低,如果业务可以接受极小概率的临时数据不一致,无需额外做控制,现有实现即可满足需求。
内容的提问来源于stack exchange,提问作者Alex Sapozhnikov
相关产品推荐
相关产品推荐

