【问题标题】:Why are these two sql statements deadlocking? (Deadlock graph + details included)为什么这两条sql语句会死锁? (包括死锁图+详细信息)
【发布时间】:2010-03-20 14:00:06
【问题描述】:

我有以下死锁图,它描述了两个相互死锁的 sql 语句。我只是不确定如何分析这个问题,然后修复我的 sql 代码以防止这种情况发生。

主要死锁图

alt text http://img140.imageshack.us/img140/6193/deadlock1.png Click here for a bigger image.

左侧,细节

alt text http://img715.imageshack.us/img715/3999/deadlock2.png Click here for a bigger image.

右侧,细节

alt text http://img686.imageshack.us/img686/5097/deadlock3.png Click here for a bigger image.

原始死锁架构 xml 文件

Click here to download the xml file.

表架构

alt text http://img509.imageshack.us/img509/5843/deadlockschema.png

LogEntries 表详细信息

alt text http://img28.imageshack.us/img28/9732/deadlocklogentriestable.png

连接的客户表详细信息

alt text http://img11.imageshack.us/img11/7681/deadlockconnectedclient.png

代码在做什么?

我正在同时读取多个文件(例如,在这个例子中我们说 3 个)同时。每个文件包含不同的数据但相同类型的数据。然后我将数据插入到LogEntries 表中,然后(如果需要)我从ConnectedClients 表中插入或删除一些内容。

这是我的 sql 代码。

using (TransactionScope transactionScope = new TransactionScope())
{
    _logEntryRepository.InsertOrUpdate(logEntry);

    // Now, if this log entry was a NewConnection or an LostConnection, then we need to make sure we update the ConnectedClients.
    if (logEntry.EventType == EventType.NewConnection)
    {
        _connectedClientRepository.Insert(new ConnectedClient { LogEntryId = logEntry.LogEntryId });
     }

    // A (PB) BanKick does _NOT_ register a lost connection .. so we need to make sure we handle those scenario's as a LostConnection.
    if (logEntry.EventType == EventType.LostConnection ||
        logEntry.EventType == EventType.BanKick)
    {
        _connectedClientRepository.Delete(logEntry.ClientName, logEntry.ClientIpAndPort);
    }

    _unitOfWork.Commit();
    transactionScope.Complete();
}

现在每个文件都有自己的UnitOfWork 实例(这意味着它有自己的数据库连接、事务和存储库上下文)。所以我假设这意味着数据库有 3 个不同的连接同时发生。

最后,这是使用Entity Framework 作为存储库,但请不要让这阻止你思考这个问题

使用分析工具,Isolation LevelSerializable。我也试过ReadCommitedReadUncommited,但它们都出错了:-

  • ReadCommited:同上。死锁。
  • ReadUncommited:不同的错误。 EF 异常表示它期望返回一些结果,但一无所获。我猜这是 LogEntryId Identity (scope_identity) 值,由于脏读而无法检索到。

请帮忙!

PS。顺便说一句,这是 Sql Server 2008。


更新 #2

在阅读Remus Rusanu的更新回复后,我觉得我可以尝试提供更多信息,看看其他人是否可以提供进一步的帮助。

EF图

alt text http://img691.imageshack.us/img691/600/deadlockefmodel.png

现在,Remus 建议(注意,他确实说他不熟悉 EF)...

拼图的最后一块, 无法解释的锁左节点上有 PK_ConnectedClients,我假设是 从 EF 实施 插入或更新。它可能会做一个 首先查找,并且由于 FK 之间声明的关系 ConnectedClients 和 LogEntries,它 寻找 PK_ConnectedClients,因此 获取可序列化锁。

有趣。我不确定为什么左节点锁定PK_ConnectedClients,如上所述。好的,让我们看看该方法的代码......

public void InsertOrUpdate(LogEntry logEntry)
{
    LoggingService.Debug("About to InsertOrUpdate a logEntry");

    logEntry.ThrowIfArgumentIsNull("logEntry");

    if (logEntry.LogEntryId <= 0)
    {
        LoggingService.Debug("Current logEntry instance doesn't have an Id. Instance object will be 'AddObject'.");
        Context.LogEntries.AddObject(logEntry);
    }
    else
    {
        LoggingService.Debug("Current logEntry instance has an Id. Instance object will be 'Attached'.");
        Context.LogEntries.Attach(logEntry);
    }
}

嗯。这是一个简单的AddObject(又名。插入)或Attach(又名。更新)。没有参考。 Sql 代码也没有提示任何查找内容。

好吧……我还有另外两种方法……也许他们正在做一些查找?

在 ConnectedClientRepository ...

public void Insert(ConnectedClient connectedClient)
{
    connectedClient.ThrowIfArgumentIsNull("connectedClient");

    Context.ConnectedClients.AddObject(connectedClient);
}

不 -> 也是基本的,因为。

幸运的最后一种方法?哇..现在这很有趣....

public void Delete(string clientName, string clientIpAndPort)
{
    clientName.ThrowIfArgumentIsNullOrEmpty("clientName");
    clientIpAndPort.ThrowIfArgumentIsNullOrEmpty("clientIpAndPort");

    // First we need to attach this object to the object manager.
    var existingConnectedClient = (from x in GetConnectedClients()
                                   where x.LogEntry.ClientName == clientName.Trim() &&
                                   x.LogEntry.ClientIpAndPort == clientIpAndPort.Trim() &&
                                   x.LogEntry.EventTypeId == (byte)EventType.NewConnection
                                   select x)
                                  .Take(1)
                                  .SingleOrDefault();

    if (existingConnectedClient != null)
    {
        Context.ConnectedClients.DeleteObject(existingConnectedClient);
    }
}

所以,看上面,我抓住了我想删除的记录的一个实例......如果它存在,那么删除它。

所以..如果我注释掉那个方法调用,在我最初的逻辑方式中直到这篇 SO 帖子的顶部......会发生什么?

有效。哇。

它也可以作为SerializableRead Commited 工作——当我不调用Delete 方法时两者都工作。

那么为什么删除方法会被锁定?是因为选择(使用serializable)会锁定并发生一些死锁吗?

对于read committed,我是否有可能同时发生 3 个删除调用。

  • 第一次抓取数据的一个实例。
  • 第二个(和第三个)抓取相同数据的另一个实例。
  • 现在,第一次删除。很好。
  • 第二次删除 .. 但该行已经消失了 .. 所以我收到了关于影响意外行数 (0) 的奇怪错误。

可能吗?如果是这样......呃......我该如何解决这个问题?这是竞争条件的经典案例吗?是否有可能以某种方式防止这种情况发生?


更新

  • 修复了图片的链接。
  • 链接到原始 XML 死锁文件。这里is the same link
  • 添加了数据库表架构。
  • 添加了两个表的详细信息。

【问题讨论】:

  • _unitOfWork.Commit() 做了什么; ?
  • 大图的链接似乎搞砸了。最后一个实际上是第二个的版本。第二个是第一个的副本。
  • 在FK上有索引的两个表之间有外键关系吗?
  • 能把执行的sql语句和数据库schema贴一下吗?
  • @STMax :它们列在第二张和第三张图片中。够了吗?

标签: .net sql-server transactions deadlock unit-of-work


【解决方案1】:

左侧节点在PK_CustomerRecords 上持有一个RangeS-U lock,并希望在i1 上拥有一个RangeS-U 锁定(我假设它是LogEntries 上的一个索引)。右侧节点在i1 上有一个RangeS-U 锁定,并希望在PK_CustomerRecords 上有一个RangeI-N

显然死锁发生在_logEntriesRepository.InsertOrUpdate(左节点)和_connectedClientRepository.Insert(右节点)之间。在不知道声明的 EF 关系的类型的情况下,我无法评论为什么左侧节点在插入LogEntry 时锁定了PK_CustomerRecords。我怀疑这是由 EF 引起的 ORM 类型行为引起的,例如查找“预加载”成员,或者可能是由围绕已发布代码中范围的更高级别 TransactionScope 引起的。

正如其他人所说,有必要在死锁评估中发布数据库模式,因为访问路径(使用的索引)至关重要。有关索引在死锁中的含义的更详细讨论,请参阅我的文章 Read-Write deadlock

我的第一个建议是强制事务范围为read committed。 TransactionScopes 的默认可序列化级别在实践中几乎不需要,它会消耗性能,在这种特殊情况下,通过将范围锁带入等式,给死锁调查带来了很多不必要的噪音,使一切变得复杂。请贴出read commited下发生的死锁信息。

另外,请勿发布死锁图的图片。一张图说千言万语,这里不假,贴一下原版的死锁XML:里面有很多美图看不到的信息。

更新

从死锁 XML 我可以看到左节点正在执行 insert [dbo].[LogEntries]([GameFileId], [CreatedOn], [EventTypeId], [Message], [Code], [Violation], [ClientName], [ClientGuid], [ClientIpAndPort]) values (@0, @1, @2, null, null, null, @3, @4, @5)&lt;executionStack&gt;&lt;frame&gt; 元素)。但更重要的是,我可以看到神秘索引“i1”背后的对象:objectname="AWing.sys.fulltext_index_docidstatus_1755869322"indexname="i1"。所以死锁发生在全文索引上。

所以死锁的完整解释是:

  • 右节点位于 _connectedClientRepository.Insert,它需要 PK_ConnectedClients 上的范围插入锁定。它在之前执行 _logEntryRepository.InsertOrUpdate 的全文索引 i1 上具有 RangeS-U 锁。
  • 左节点在_logEntryRepository.InsertOrUpdate,在批处理内的INSERT语句处,它需要全文索引i1上的RangeS-U锁。它在 PK_ConnectedClients 上有一个 RangeS-S 锁,阻止了正确的节点,这在图 XML 中没有任何解释。

拼图的最后一块,PK_ConnectedClients 上的左节点无法解释的锁,我假设来自 InsertOrUpdate 的 EF 实现。它可能首先进行查找,并且由于 ConnectedClients 和 LogEntries 之间声明的 FK 关系,它会在 PK_ConnectedClients 上查找,从而获取可序列化锁。

这里的罪魁祸首是事务隔离级别 (Serializable) 和 InsertOrUpdate 上的 EF 行为。我无法就 EF 行为提供建议,但可序列化级别肯定是矫枉过正。这让我们回到了您在读取提交级别下遇到的错误,不幸的是,这又是一个我无法评论的 EF 错误。

【讨论】:

  • 首先,我在开头的帖子(底部下方)中添加了指向 RAW xml 死锁数据的链接。我将尝试获取架构、索引并尝试新的范围。
  • 刚刚尝试使用 ReadCommited。在其中一个文件的导入过程中出现以下错误:"Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries."
  • @Remus Runsanu:哇。很棒的家伙 :) 我现在用更多信息更新了我的初始帖子。请您在这个问题的初始帖子中查看更新#2 吗?我想我们现在越来越近了:)
  • @Remus Runsanu : heya .. 我可以碰一下这个来引起你的注意吗?
  • Sry Krome,但这对于我的舒适区来说太深入了 EF 行为,从这里开始,我会更多地猜测而不是实际回答。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-04-03
  • 2019-12-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多