Entity Framework中的SqlException - 因为会话中有其他线程正在运行,所以不允许新的事务。

679

我目前遇到了这个错误:

System.Data.SqlClient.SqlException:因为会话中有其他线程在运行,所以不允许新事务。

在运行以下代码时出现此错误:

public class ProductManager : IProductManager
{
    #region Declare Models
    private RivWorks.Model.Negotiation.RIV_Entities _dbRiv = RivWorks.Model.Stores.RivEntities(AppSettings.RivWorkEntities_connString);
    private RivWorks.Model.NegotiationAutos.RivFeedsEntities _dbFeed = RivWorks.Model.Stores.FeedEntities(AppSettings.FeedAutosEntities_connString);
    #endregion

    public IProduct GetProductById(Guid productId)
    {
        // Do a quick sync of the feeds...
        SyncFeeds();
        ...
        // get a product...
        ...
        return product;
    }

    private void SyncFeeds()
    {
        bool found = false;
        string feedSource = "AUTO";
        switch (feedSource) // companyFeedDetail.FeedSourceTable.ToUpper())
        {
            case "AUTO":
                var clientList = from a in _dbFeed.Client.Include("Auto") select a;
                foreach (RivWorks.Model.NegotiationAutos.Client client in clientList)
                {
                    var companyFeedDetailList = from a in _dbRiv.AutoNegotiationDetails where a.ClientID == client.ClientID select a;
                    foreach (RivWorks.Model.Negotiation.AutoNegotiationDetails companyFeedDetail in companyFeedDetailList)
                    {
                        if (companyFeedDetail.FeedSourceTable.ToUpper() == "AUTO")
                        {
                            var company = (from a in _dbRiv.Company.Include("Product") where a.CompanyId == companyFeedDetail.CompanyId select a).First();
                            foreach (RivWorks.Model.NegotiationAutos.Auto sourceProduct in client.Auto)
                            {
                                foreach (RivWorks.Model.Negotiation.Product targetProduct in company.Product)
                                {
                                    if (targetProduct.alternateProductID == sourceProduct.AutoID)
                                    {
                                        found = true;
                                        break;
                                    }
                                }
                                if (!found)
                                {
                                    var newProduct = new RivWorks.Model.Negotiation.Product();
                                    newProduct.alternateProductID = sourceProduct.AutoID;
                                    newProduct.isFromFeed = true;
                                    newProduct.isDeleted = false;
                                    newProduct.SKU = sourceProduct.StockNumber;
                                    company.Product.Add(newProduct);
                                }
                            }
                            _dbRiv.SaveChanges();  // ### THIS BREAKS ### //
                        }
                    }
                }
                break;
        }
    }
}

模型#1 - 此模型位于我们的 Dev 服务器上的数据库中。 模型#1 http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/bdb2b000-6e60-4af0-a7a1-2bb6b05d8bc1/Model1.png

模型#2 - 此模型位于我们的 Prod 服务器上的数据库中,并且每天通过自动供应更新。 alt text http://content.screencast.com/users/Keith.Barrows/folders/Jing/media/4260259f-bce6-43d5-9d2a-017bd9a980d4/Model2.png

注意 - 模型#1 中用红圈标出的项目是我用来“映射”到模型#2 的字段。 请忽略模型#2 中的红色圆圈:那是我曾经提出的另一个问题,现在已得到解答。

注意:我仍然需要进行 isDeleted 检查,以便如果已经从客户的清单中删除,则可以从 DB1 中进行软删除。

我想要做的所有事情,就是使用此特定代码将 DB1 中的公司与 DB2 中的客户连接起来,从 DB2 获取其产品清单,如果它们还不存在于 DB1 中,则将其插入到 DB1 中。 第一次运行应该是完全提取库存。 之后每次运行都不应发生任何事情,除非新的库存在夜间供应中出现。

  

那么最重要的问题是 - 我如何解决我正在遇到的事务错误? 我需要在循环中每次放弃并重新创建我的上下文吗(这对我来说没有意义)?


8
这是我见过的最详细的问题。 - user220583
14
有人还想念存储过程吗? - David
23个回答

789

经过长时间的苦思冥想,我发现 foreach 循环是问题的罪魁祸首。需要做的是调用 EF 并将其返回到目标类型的 IList<T> 中,然后在 IList<T> 上进行循环。

例如:

IList<Client> clientList = from a in _dbFeed.Client.Include("Auto") select a;
foreach (RivWorks.Model.NegotiationAutos.Client client in clientList)
{
   var companyFeedDetailList = from a in _dbRiv.AutoNegotiationDetails where a.ClientID == client.ClientID select a;
    // ...
}

17
是的,这个问题也让我头疼不已。当我发现这个问题时,我差点从椅子上摔下来!虽然我理解了这个问题背后的技术原因,但这并不直观,也没有帮助开发人员陷入“成功的坑”。http://blogs.msdn.com/brada/archive/2003/10/02/50420.aspx - Doctor Jones
11
对于大型数据集,这样做会影响性能吗? 如果表中有数百万条记录。ToList()将把它们全部装入内存。 我正在遇到这个问题,想知道以下解决方案是否可行: a)分离实体 b)创建一个新的ObjectContext并将分离的实体附加到它上面。 c)在新的ObjectContext上调用SaveChanges()。 d)从新的ObjectContext中分离实体。 e)将其重新附加到旧的ObjectContext中。 - Abhijeet Patel
174
问题在于当你仍在从数据库检索结果时,无法调用 SaveChanges。因此,另一个解决方案是在循环完成后保存更改。 - Drew Noakes
5
我也曾遇到这个问题,因此我在微软反馈中心添加了这个链接:https://connect.microsoft.com/VisualStudio/feedback/details/612369/misleading-error-message-in-sqlexception-when-using-entity-framework。请随意投票支持。 - Ian Mercer
44
我们的开发人员倾向于在不考虑后果的情况下将 .ToList() 添加到任何 LINQ 查询中。这必须是第一次添加 .ToList() 真正有用! - Marc
显示剩余8条评论

297

如你所知,当一个foreach正在通过一个活动的读取器从数据库中提取数据时,你无法在其中保存。

对于小数据集,调用ToList()ToArray()是可以的,但是当你有成千上万行数据时,会消耗大量内存。

最好按块加载行。

public static class EntityFrameworkUtil
{
    public static IEnumerable<T> QueryInChunksOf<T>(this IQueryable<T> queryable, int chunkSize)
    {
        return queryable.QueryChunksOfSize(chunkSize).SelectMany(chunk => chunk);
    }

    public static IEnumerable<T[]> QueryChunksOfSize<T>(this IQueryable<T> queryable, int chunkSize)
    {
        int chunkNumber = 0;
        while (true)
        {
            var query = (chunkNumber == 0)
                ? queryable 
                : queryable.Skip(chunkNumber * chunkSize);
            var chunk = query.Take(chunkSize).ToArray();
            if (chunk.Length == 0)
                yield break;
            yield return chunk;
            chunkNumber++;
        }
    }
}

根据上述扩展方法,您可以这样编写查询:
foreach (var client in clientList.OrderBy(c => c.Id).QueryInChunksOf(100))
{
    // do stuff
    context.SaveChanges();
}

调用此方法的可查询对象必须是有序的。这是因为Entity Framework仅支持在有序查询上使用IQueryable<T>.Skip(int),这在考虑到多个不同范围的查询需要排序稳定时是有意义的。如果排序对您不重要,只需按主键排序,因为这很可能具有聚集索引。

此版本将以100个批次查询数据库。请注意,每个实体都会调用SaveChanges()

如果您想显著提高吞吐量,应该更少地调用SaveChanges()。改用以下代码:

foreach (var chunk in clientList.OrderBy(c => c.Id).QueryChunksOfSize(100))
{
    foreach (var client in chunk)
    {
        // do stuff
    }
    context.SaveChanges();
}

这将导致数据库更新调用减少100倍。当然,每个调用需要更长的时间才能完成,但最终你仍然能够获得更好的结果。具体情况因人而异,但对我来说,这是世界上最快的方法。
并且它可以解决你遇到的异常问题。
编辑后,我运行了SQL Profiler并更新了一些内容以提高性能。对于任何有兴趣的人,这里有一些示例SQL,显示了DB创建的内容。
第一个循环不需要跳过任何内容,因此更简单。
SELECT TOP (100)                     -- the chunk size 
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name], 
FROM [dbo].[Clients] AS [Extent1]
ORDER BY [Extent1].[Id] ASC

后续调用需要跳过先前的结果块,因此介绍了使用row_number

SELECT TOP (100)                     -- the chunk size
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name], 
FROM (
    SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], row_number()
    OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number]
    FROM [dbo].[Clients] AS [Extent1]
) AS [Extent1]
WHERE [Extent1].[row_number] > 100   -- the number of rows to skip
ORDER BY [Extent1].[Id] ASC

21
谢谢。您的解释比标记为“已回答”的那个更有用。 - Wagner Danda da Silva Filho
2
这很棒。只有一件事:如果您正在查询某个列并更新该列的值,则需要注意 chunkNumber ++;。假设您有一个“ModifiedDate”列,并且您正在查询.Where(x => x.ModifiedDate!= null),并且在foreach结束时为ModifiedDate设置一个值。通过这种方式,您不会迭代一半的记录,因为一半的记录被跳过了。 - Saber
1
不幸的是,在处理大型数据集时,您可能会遇到OutOfMemoryException - 请参见Entity framework large data set, out of memory exception中的解释。我已经描述了如何在每个批次中更新上下文,请参见SqlException from Entity Framework - New transaction is not allowed because there are other threads running in the session - Michael Freidgeim
1
我认为这应该可以工作。var skip = 0; const int take = 100; List<Employee> emps ; while ((emps = db.Employees.Skip(skip).Take(take).ToList()).Count > 0) { skip += take; foreach (var emp in emps) { // 在这里处理 } } - jwize
1
谢谢!在我的情况下,在“foreach”之后使用“SaveChange”是一个简单的解决方案 :) - Alexander S.
显示剩余3条评论

128
我们已经发布了在Connect上开放的错误的官方回复。我们建议的解决方法如下:
这个错误是由于Entity Framework在SaveChanges()调用期间创建了一个隐式事务所致。解决此错误的最佳方法是使用不同的模式(即,在读取过程中不保存)或显式声明事务。以下是三种可能的解决方案:
// 1: Save after iteration (recommended approach in most cases)
using (var context = new MyContext())
{
    foreach (var person in context.People)
    {
        // Change to person
    }
    context.SaveChanges();
}

// 2: Declare an explicit transaction
using (var transaction = new TransactionScope())
{
    using (var context = new MyContext())
    {
        foreach (var person in context.People)
        {
            // Change to person
            context.SaveChanges();
        }
    }
    transaction.Complete();
}

// 3: Read rows ahead (Dangerous!)
using (var context = new MyContext())
{
    var people = context.People.ToList(); // Note that this forces the database
                                          // to evaluate the query immediately
                                          // and could be very bad for large tables.

    foreach (var person in people)
    {
        // Change to person
        context.SaveChanges();
    }
} 

6
如果您选择使用事务(Transaction)路线,仅仅添加一个 TransactionScope 可能无法解决问题——不要忘记在必要的情况下延长超时时间。例如,如果您将交互式调试进行数据库调用,则操作可能需要很长时间。以下是将事务超时时间延长至一小时的代码示例:using (var transaction = new TransactionScope(TransactionScopeOption.Required, new TimeSpan(1, 0, 0)))。 - Chris Moschini
我第一次从“教程路径”偏离到自己的实际示例时就遇到了这个错误! 然而,对我来说,更简单的解决方案是在迭代后保存! (我认为99%的情况下都是如此,只有1%的情况真正需要在循环内执行数据库保存) - spiderman
糟糕,我刚刚遇到了这个错误。非常讨厌。第二个建议对我来说非常有效,同时将我的SaveChanges移动到循环中。我认为在循环外部保存更改会更好一些。但是好吧,我想我错了?:( - Mr. Young
对我没有用,.NET 4.5。当使用TransactionScope时,我遇到了以下错误:“The underlying provider failed on EnlistTransaction.{"The partner transaction manager has disabled its support for remote/network transactions. (Exception from HRESULT: 0x8004D025)"}”。最终我在迭代之外完成了这项工作。 - Diganta Kumar
使用TransactionScope是危险的,因为表在整个事务期间被锁定。 - Michael Freidgeim

24

在 C# 使用 Entity Framework 中的 foreach 循环中,你无法保存更改。

context.SaveChanges() 方法类似于常规数据库系统 (RDMS) 上的提交操作。

只需进行所有更改(Entity Framework 将缓存它们),然后在循环之外调用 SaveChanges() 一次性保存所有更改,就像执行数据库的提交操作一样。

如果可以一次性保存所有更改,则此方法可行。


3
我认为在这里看到“常规数据库系统(RDMS)”很有趣。 - Dinerdo
1
这似乎是错误的,因为在 EF 的 90% 的情况下,重复调用 SaveChanges 是可以接受的。 - Pxtl
似乎反复调用SaveChanges是可以的,除非foreach循环正在遍历数据库实体。 - kerbasaurus
1
啊哈!将上下文带入for-each循环中!(噗...我当时在想什么?..)谢谢! - Adam Cox

20

foreach循环的结尾处加上 context.SaveChanges()


这是我在我的情况下发现的更好的选择,因为它可以在foreach内部进行保存。 - Almeida
4
这并非总是可选的。 - Pxtl

17
将可查询的列表转换为 .ToList(),它应该可以正常工作。

3
请提供一个示例,而不仅仅发布一个解决方案。 - Ronnie Oosting

14

提示:来自一本书的内容,经过一些调整因为仍然有效:

调用SaveChanges()方法会开始一个事务,如果在迭代完成之前发生异常,就会自动回滚所有已保存到数据库的更改;否则,事务会提交。当你更新或删除大量实体时,可能会尝试在每次实体更新或删除后应用该方法,而不是在迭代完成后应用。

如果你在所有数据处理完之前尝试调用SaveChanges(),就会遇到“不允许启动新事务,因为会话中有其他线程正在运行”的异常。这种异常发生是因为SQL Server不允许在具有打开SqlDataReader的连接上启动新事务,即使由连接字符串启用了多个活动记录集(MARS)(EF的默认连接字符串也启用了MARS)。

有时候了解为什么事情会发生更好;-)


1
一个避免这个问题的好方法是,当你已经打开了一个读取器时,再打开第二个读取器并将那些操作放在第二个读取器中。当你在实体框架中更新主/细节记录时,这是你可能需要的。你为主记录打开第一个连接,为细节记录打开第二个连接。如果你只是读取数据,就不应该有问题。问题出现在更新过程中。 - Herman Van Der Blom
有益的解释。你说得对,了解事情发生的原因很重要。 - Dov Miller
这是在多个Stack Overflow问题中,20个答案中唯一正确的答案。 - N73k
但是这种情况只有在涉及多个线程时才会发生。一个线程正在连接上进行读取,而另一个线程则试图在同一连接上执行SaveChanges操作(都是通过Entity Framework实现的,但我认为这并不重要)。 - N73k
是的,我也遇到了同样的情况。我每晚都会将完整的LDAP目录读入数据库中。因为这是大量数据需要导入,所以我使用了254个线程来处理。.NET很快,但数据库事务不是,因此您可以通过使用254个线程来帮助此过程。现在,这是导入大量数据的标准流程方式。如果导入很简单,则可以用另一种方式完成,但在复杂的数据转换中,这是完美的选择。 - Herman Van Der Blom

9
始终将您的选择用作列表
例如:
var tempGroupOfFiles = Entities.Submited_Files.Where(r => r.FileStatusID == 10 && r.EventID == EventId).ToList();

然后循环遍历集合并保存更改。

 foreach (var item in tempGroupOfFiles)
             {
                 var itemToUpdate = item;
                 if (itemToUpdate != null)
                 {
                     itemToUpdate.FileStatusID = 8;
                     itemToUpdate.LastModifiedDate = DateTime.Now;
                 }
                 Entities.SaveChanges();

             }

4
这绝不是一个好的实践。如果不需要,你不应该频繁执行SaveChanges,而且绝不能“始终使用您的选择作为列表”。 - Dinerdo
@Dinerdo 这真的取决于具体情况。在我的情况下,我有2个foreach循环。外部循环将数据库查询作为列表。例如,此foreach遍历硬件设备。内部foreach从每个设备检索多个数据。根据要求,我需要在从每个设备逐个检索数据后将其保存到数据库中。不能选择在过程结束时保存所有数据。我遇到了相同的错误,但mzonerz的解决方案起作用了。 - jstuardo
@jstuardo 即使使用批处理,仍然如此吗? - Dinerdo
1
@Dinerdo 我同意在哲学层面上这不是一个好的做法。然而,存在一些情况,在for循环内部代码调用另一个方法(比如AddToLog()方法),该方法包括本地调用db.SaveChanges()。在这种情况下,你无法真正控制对db.SaveChanges()的调用。在这种情况下,使用ToList()或类似的结构将像mzonerz建议的那样起作用。谢谢! - AV2000
实际上,这样做会对你造成更多的伤害而不是帮助。我坚持我的观点 - ToList() 绝对不应该一直使用,并且在高性能应用程序中尽可能避免在每个单独项之后保存更改。这将是一个临时解决方案。无论你使用什么日志记录方法,最好也能利用缓冲区。 - Dinerdo

7
我曾经遇到过类似的问题,但情况有所不同。我有一个列表框中的项目列表。用户可以点击一个项目并选择删除,但我使用存储过程来删除该项目,因为在删除该项目时涉及到很多逻辑。当我调用存储过程时,删除工作正常,但任何以后对SaveChanges的调用都会导致错误。我的解决方案是在EF之外调用存储过程,这样就可以正常工作了。由于某种原因,当我使用EF的方式调用存储过程时,它会保持某些东西处于打开状态。

4
最近我也遇到了类似的问题:原因是存储过程中的SELECT语句生成了一个空结果集,如果没有读取该结果集,SaveChanges就会抛出异常。 - n0rd
同样的问题,感谢您的提示,关于存储过程未读结果。 - Pavel K

5
我们在从EF5迁移到EF6后,遇到了这个错误:"New transaction is not allowed because there are other threads running in the session"
我们通过谷歌搜索找到了这里,但在循环内部没有调用 SaveChanges()。这些错误是在使用 ObjectContext.ExecuteFunction 执行存储过程时引发的,而此时在读取数据库的 foreach 循环中已经有其他线程在运行。
任何对 ObjectContext.ExecuteFunction 的调用都会将该函数包装在一个事务中。如果在已经有一个打开的数据读取器的情况下开始一个事务,则会导致出现错误。
可以通过设置以下选项来禁用对 SP 进行事务包装:
_context.Configuration.EnsureTransactionsForFunctionsAndCommands = false;

EnsureTransactionsForFunctionsAndCommands 选项允许SP在不创建自己的事务的情况下运行,因此不再引发错误。

DbContextConfiguration.EnsureTransactionsForFunctionsAndCommands 属性


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接