1

使用:Asp.net Core、Entityframework Core、ABP 4.5

我有一个用户注册和初始化流程。但这需要很长时间。我想并行化这个。这是由于从同一实体进行更新,但具有不同的字段。

我的目标: 1. 端点应该尽快响应;2、长初始化在后台处理;

代码之前(为简洁起见省略了次要细节)

public async Task<ResponceDto> Rgistration(RegModel input)
{       
    var user = await _userRegistrationManager.RegisterAsync(input.EmailAddress, input.Password, false );
    var result = await _userManager.AddToRoleAsync(user, defaultRoleName);
    user.Code = GenerateCode();
    await SendEmail(user.EmailAddress, user.Code);
    await AddSubEntities(user);
    await AddSubCollectionEntities(user);
    await CurrentUnitOfWork.SaveChangesAsync();
    return user.MapTo<ResponceDto>();
}

private async Task AddSubEntities(User user)
{
    var newSubEntity = new newSubEntity { User = user, UserId = user.Id };
    await _subEntityRepo.InsertAsync(newSubEntity); 
    //few another One-to-One entities...
}

private async Task AddSubEntities(User user)
{
    List<AnotherEntity> collection = GetSomeCollection(user.Type);  
    await _anotherEntitieRepo.GetDbContext().AddRangeAsync(collection); 
    //few another One-to-Many collections...
}

尝试改变:

public async Task<ResponceDto> Rgistration(RegModel input)
{
    var user = await _userRegistrationManager.RegisterAsync(input.EmailAddress, input.Password, false );

    Task.Run(async () => {
        var result = await _userManager.AddToRoleAsync(user, defaultRoleName);          
    });

    Task.Run(async () => {
        user.Code = GenerateCode();
        await SendEmail(user.EmailAddress, user.Code);  
    });

    Task.Run(async () => {
    using (var unitOfWork = UnitOfWorkManager.Begin())
    {//long operation. defalt unitOfWork out of scope
        try
        {
            await AddSubEntities(user);                            
        }            
        finally
        {                
            unitOfWork.Complete();
        }
    }           
});

Task.Run(async () => {
    using (var unitOfWork = UnitOfWorkManager.Begin())
    {
        try
        {
            await AddSubCollectionEntities(user);                            
        }            
        finally
        {                
            unitOfWork.Complete();
        }
    }           
});
    await CurrentUnitOfWork.SaveChangesAsync();
    return user.MapTo<ResponceDto>();
}

错误:在这里我得到了很多与竞争相关的不同错误。经常:

  1. 在前一个操作完成之前,在此上下文上开始了第二个操作。这通常是由使用相同 DbContext 实例的不同线程引起的。

  2. 在少数注册调用中:无法在具有唯一索引“YYY”的对象“XXX”中插入重复的键行。重复键值为 (70)。该语句已终止。

我在服务器上考虑了其流中的每个请求,但显然不是。

  1. 或者所有用户都成功注册,但他们在数据库中没有某些子实体。不注册用户比找出他在哪里初始化不正确要容易得多=(

如何保持用户实体“开放”以进行更新,同时“关闭”其他请求发起的更改?如何使此代码线程安全且快速,任何人都可以提供建议吗?

4

2 回答 2

4

在 ASP.NET 中使用Task.Run很少是一个好主意。

无论如何,异步方法都在线程池上运行,因此将它们包装起来Task.Run只是增加开销而没有任何好处。

在 ASP.NET 中使用异步的目的只是为了防止线程被阻塞,以便它们能够为其他 HTTP 请求提供服务。

最终,您的数据库是瓶颈;如果所有这些操作都需要在您向客户端返回响应之前发生,那么除了让它们发生之外您无能为力。

如果可以提前返回并允许一些操作在后台继续运行,那么这里有详细说明如何做到这一点。

于 2020-04-24T12:26:10.780 回答
3

Task.Run不等于并行。它从池中获取一个新线程并在该线程上运行工作,并且由于您不等待它,因此其余代码可以继续进行。但是,那是因为您实际上是在孤立该线程。当操作返回时,所有范围内的服务都将被释放,其中包括您的上下文等内容。结果,任何尚未完成的线程都会出错。

线程池是一种有限资源,在 Web 应用程序的上下文中,它直接等同于服务器的吞吐量。您接受的每一个线程都是您可以服务的少一个请求。因此,您更有可能最终将请求排队,这只会增加处理时间。它几乎不适合Task.Run在 Web 环境中使用。

此外,EF Core(或旧 EF,就此而言)不支持并行化。因此,即使没有上述其他问题,无论如何,它都会阻止你做你想做的事情。

您在这里的查询并不复杂。即使您尝试一次插入 100 多个内容,也应该只需要几毫秒即可完成。如果这里有任何明显的延迟,您需要首先查看数据库服务器的资源和网络延迟。

减速很可能来自电子邮件的发送。不过,这也可能会被优化。我曾经遇到过需要 30 秒才能发送电子邮件的情况,直到我最终发现这是我们的 Exchange 服务器的问题,IT 管理员愚蠢地故意引入了 30 秒的延迟。无论如何,它通常总是比发送电子邮件等后台操作更可取,因为它们不是应用程序功能的核心。然而,这意味着实际上在后台处理它们,即将它们排队并通过诸如托管服务或完全不同的工作进程之类的东西来处理它们。

于 2020-04-24T12:49:07.910 回答