Blazor Server多租户字典并发锁实现及外部锁策略咨询
背景
在Blazor Server作用域服务中,使用以下普通字典管理多租户状态,存在多线程并发修改问题,计划通过SyncRoot加锁解决:
public Dictionary<string, GlobalDataModel> Global { get; } = new(); public Dictionary<string, Dictionary<long, LocalDataModel>> Local { get; } = new();
问题1:容器类内添加项的锁写法哪种正确?
结论:写法二正确
写法一的核心问题是「检查键是否存在」和「加锁添加」逻辑分离,存在竞态条件:多个线程可能同时通过ContainsKey(tenantId) == false的检查,随后排队进入锁。第一个线程完成添加后,后续线程进入锁执行Add会直接抛出ArgumentException(键已存在)。
写法二将「检查+添加」的完整逻辑放在锁内,确保同一时刻只有一个线程能执行这段操作,从根源避免了重复添加的问题,代码如下:
lock (syncRoot) { if (Local.ContainsKey(tenantId) == false) { Local.Add(tenantId, new Dictionary<long, LocalDataModel>()); } }
如果想优化性能(减少锁的持有时间),可以采用双重检查锁定,但需给Local添加volatile修饰以保证内存可见性,写法示例:
if (!Local.ContainsKey(tenantId)) { lock (syncRoot) { if (!Local.ContainsKey(tenantId)) { Local.Add(tenantId, new Dictionary<long, LocalDataModel>()); } } }
问题2:复制集合安全迭代时的锁层级选择
结论:需根据操作范围选择锁层级,核心是保证「读取/复制目标集合」的全程,集合不会被修改,且必须复制集合副本而非直接引用
你的现有两种写法都存在缺陷:仅把Resources的引用赋值给resources,即使加锁,后续其他线程修改原集合时,resources引用的还是同一个集合,迭代时依然会触发并发异常。正确操作是在锁内完成集合复制(比如生成新List)。
针对锁层级的选择:
- 仅操作
Resources集合时:锁Resources本身(数据模型层级)是更合理的细粒度锁,能减少锁冲突范围,但必须确保所有修改该Resources的代码都使用同一个锁对象(即都锁这个Resources实例)。示例代码:
[Inject] private IDataAdaptor DataAdaptor { get; set; }; List<Resource> resources; // 先确保能安全访问到目标Resources lock (DataAdaptor.SyncRoot) // 假设容器类暴露了内部SyncRoot { var targetResources = DataAdaptor.Local[TenantId][ProjectId].Resources; lock (targetResources) { resources = new List<Resource>(targetResources); } }
外层
Local字典也存在并发修改时:访问DataAdaptor.Local[TenantId][ProjectId]必须先锁容器类的syncRoot,否则可能因字典并发修改抛出InvalidOperationException。不推荐方式一(锁整个
DataAdaptor):锁范围过大,会导致所有服务操作串行执行,严重影响并发性能。
问题3:外部类是否需要创建独立SyncRoot?
结论:不需要,且绝对不能这么做
外部类创建的SyncRoot和容器类内部的SyncRoot是不同对象,无法实现同步,会直接导致锁失效。正确做法有两种:
- 封装所有字典操作到容器类内部:在
IDataAdaptor实现类中提供线程安全方法,比如public List<Resource> GetProjectResources(string tenantId, long projectId),方法内部自行用syncRoot加锁并完成集合复制,外部类只需调用方法,无需处理锁逻辑。示例:
public class DataAdaptor : IDataAdaptor { private readonly object _syncRoot = new object(); public Dictionary<string, Dictionary<long, LocalDataModel>> Local { get; } = new(); public List<Resource> GetProjectResources(string tenantId, long projectId) { lock (_syncRoot) { if (!Local.TryGetValue(tenantId, out var tenantData)) return new List<Resource>(); if (!tenantData.TryGetValue(projectId, out var projectData)) return new List<Resource>(); lock (projectData.Resources) { return new List<Resource>(projectData.Resources); } } } }
- 暴露容器类的SyncRoot给外部:如果必须让外部类直接操作字典,在容器类中添加
public object SyncRoot => _syncRoot;属性,外部类统一使用这个SyncRoot加锁,确保和容器内部操作同步。
内容的提问来源于stack exchange,提问作者Pippo

