You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor Server多租户字典并发锁实现及外部锁策略咨询

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)。

针对锁层级的选择:

  1. 仅操作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);
    }
}
  1. 外层Local字典也存在并发修改时:访问DataAdaptor.Local[TenantId][ProjectId]必须先锁容器类的syncRoot,否则可能因字典并发修改抛出InvalidOperationException。

  2. 不推荐方式一(锁整个DataAdaptor):锁范围过大,会导致所有服务操作串行执行,严重影响并发性能。

问题3:外部类是否需要创建独立SyncRoot?

结论:不需要,且绝对不能这么做

外部类创建的SyncRoot和容器类内部的SyncRoot是不同对象,无法实现同步,会直接导致锁失效。正确做法有两种:

  1. 封装所有字典操作到容器类内部:在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);
            }
        }
    }
}
  1. 暴露容器类的SyncRoot给外部:如果必须让外部类直接操作字典,在容器类中添加public object SyncRoot => _syncRoot;属性,外部类统一使用这个SyncRoot加锁,确保和容器内部操作同步。

内容的提问来源于stack exchange,提问作者Pippo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 03:30:58