EF 5超时更新;可能是由于僵局

时间:2014-01-06 21:55:33

标签: c# sql-server entity-framework database-deadlocks

我有一个命令行工具,用于通过Entity Framework将数据记录批量导入/导出到应用程序数据库中。该工具适用于将新记录插入数据库,但在尝试更新似乎与EF锁定相关的现有记录时,我遇到了超时错误。

我已阅读some其他many关于实体框架和死锁的posts,但似乎没有一个答案适用于这种情况。我尝试在TransactionScope中包装我的导入代码以及执行SET TRANSACTION ISOLATION LEVEL SQL命令,但都没有解决超时问题。

无论在SaveChanges的单次调用中有多少实体正在更新,都会发生超时。在下面的示例代码中,我将批处理大小设置为1到500之间的值,始终抛出相同的异常。

以下是更新代码的精简版本,后面是异常详细信息和SQL Server活动监视器的屏幕截图。

我正在使用从EDMX模型(模型优先)初始化的Entity Framework 5 DbContext对象。

using(var readContext = new MySourceEntities())
using(var readWriteContext = new MyTargetEntities())
{
    var query = "SELECT ...";  // Determine which records to update
    var keys  = readContext.Database.SQLQuery<int>(query);

    // Group the update into batches to improve performance. Batch()
    // extension method from MoreLINQ
    foreach (var batch in keys.Batch(BATCH_SIZE))
    {
        var sourceRecords = readContext
            .AsNoTracking()
            .Where(x => batch.Contains(x.SharedId))
            .ToList();

        var targetRecords = readWriteContext
            .Where(x => batch.Contains(x.SharedId))
            .ToLookup(x => x.SharedId);

        foreach (var record in sourceRecords)
        {
            // Enforce a constraint on having only a single match
            var target = targetRecords[record.SharedId].Single();

            target.Field = record.Field;
        }

        readWriteContext.SaveChanges();  // <--- Timeout happens here
    }
}

我从命令行应用程序运行它,它抛出的特定堆栈跟踪如下:

An error occurred while updating the entries. See the inner exception for details.
   at System.Data.Entity.Internal.InternalContext.SaveChanges()
   at System.Data.Entity.Internal.LazyInternalContext.SaveChanges()
   at System.Data.Entity.DbContext.SaveChanges()
   at <snip>

An error occurred while updating the entries. See the inner exception for details.
   at System.Data.Mapping.Update.Internal.UpdateTranslator.Update(IEntityStateManager stateManager, IEntityAdapter adapter)
   at System.Data.EntityClient.EntityAdapter.Update(IEntityStateManager entityCache)
   at System.Data.Objects.ObjectContext.SaveChanges(SaveOptions options)
   at System.Data.Entity.Internal.InternalContext.SaveChanges()

Timeout expired.  The timeout period elapsed prior to completion of the operation or the server is not responding. The statement has been terminated.
   at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
   at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite)
   at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean asyncWrite)
   at System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, String methodName, Boolean sendToPipe, Int32 timeout, Boolean asyncWrite)
   at System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
   at System.Data.Mapping.Update.Internal.DynamicUpdateCommand.Execute(UpdateTranslator translator, EntityConnection connection, Dictionary`2 identifierValues, List`1 generatedValues)
   at System.Data.Mapping.Update.Internal.UpdateTranslator.Update(IEntityStateManager stateManager, IEntityAdapter adapter)

SaveChanges方法挂起时,SQL Server活动监视器会显示以下查询,所有这些查询都处于SUSPENDED状态。红色查询来自readContext所针对的数据库,而蓝色查询来自readWriteContext所针对的数据库。

SQL Server Management Studio Activity Monitor

另外,暂停查询自己看起来并不可疑,只是直截了当的SELECT和UPDATE命令。我可以手动运行它们而不会出错。

修改

以下是运行的query子句的详细信息,因为它似乎是相关的。该查询跨数据库执行连接以使记录与SharedId匹配。从this page运行sys.dm_os_waiting_tasks查询,会显示下表。

session_id wait_duration_ms wait_type         blocking_session_id resource_description program_name    text
55         15               ASYNC_NETWORK_IO  NULL                NULL                 EntityFramework <cross-db join query>
54         29310            LCK_M_IX          55                  pagelock fileid=1... EntityFramework <update query>

查询的内容就像这样

SELECT DB1.dbo.Table1.SharedId 
FROM DB2.dbo.Table2 INNER JOIN DB1.dbo.Table1.SharedId
ON DB1.dbo.Table1.SharedId = DB2.dbo.Table2.SharedId
WHERE (
    (DB1.dbo.Table1.Field1 <> DB2.dbo.Table2.Field1) OR
    (DB1.dbo.Table1.Field2 <> DB2.dbo.Table2.Field2)
)

对我来说最令人惊讶的是,查询仍处于活动状态。有关readContext.Database.SQLQuery()调用无法完成查询的任何想法?听起来这种等待类型通常表示应用程序错误,但我不确定我是如何触发此行为的。

1 个答案:

答案 0 :(得分:3)

解决方案是使用SQLQuery()扩展方法明确实现ToList()的结果。这样可以在尝试UPDATE基础表之前完全消耗结果。

令我感到惊讶的是(对我而言)直接SQL命令没有立即返回完整结果 - 特别是因为查询返回了基本类型而不是实体对象。仔细阅读MSDN documentation后,它确实声明IEnumerable<T>方法返回的SQLQuery将在枚举时执行查询“。

我喜欢EF6中的这个方法,这个方法返回一个DbRawSqlQuery<T>对象,这可能会帮助像我这样的人下次更加小心地停下来阅读文档。