4

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

主要死锁图

alt text http://img140.imageshack.us/img140/6193/deadlock1.png 点击这里查看大图。

左侧,细节

alt text http://img715.imageshack.us/img715/3999/deadlock2.png 点击这里查看大图。

右侧,细节

alt text http://img686.imageshack.us/img686/5097/deadlock3.png 点击这里查看大图。

原始死锁模式 xml 文件

单击此处下载 xml 文件

表架构

替代文字 http://img509.imageshack.us/img509/5843/deadlockschema.png

LogEntries 表详细信息

替代文字 http://img28.imageshack.us/img28/9732/deadlocklogentriestable.png

已连接的客户端表详细信息

替代文字 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. 我也试过ReadCommitedand ReadUncommited,但他们都错误: -

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

请帮忙!

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


更新#2

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

EF图

替代文字 http://img691.imageshack.us/img691/600/deadlockefmodel.png

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

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

有趣的。如上所述,我不确定为什么左节点有一个 lock on 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帖子的顶部......会发生什么?

有用。哇。

当我不调用该方法时,它也可以作为一个Serializable或两个工作。Read CommitedDelete

那么为什么删除方法会被锁定呢?是因为 select (with serializable) 做了一个锁并且发生了一些死锁吗?

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

  • 第一个抓取数据的一个实例。
  • 第二个(和第三个)抓取相同数据的另一个实例。
  • 现在,第一次删除。美好的。
  • 第二次删除..但是该行已经消失了..因此我得到了一个奇怪的错误,即影响了意外的行数(0)。<== 零个项目被删除。

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


更新

  • 修复了图片的链接。
  • 链接到原始 XML 死锁文件。这是相同的链接
  • 添加了数据库表模式。
  • 添加了两个表的详细信息。
4

1 回答 1

4

左侧节点持有一个RangeS-U lockonPK_CustomerRecords并希望RangeS-U锁定i1(我假设它是一个索引 on LogEntries)。右侧节点有一个RangeS-U锁定i1并想要一个RangeI-Non PK_CustomerRecords

显然死锁发生在_logEntriesRepository.InsertOrUpdate(左节点)和_connectedClientRepository.Insert(右节点)之间。在不知道声明的 EF 关系类型的情况下,我无法评论为什么左侧节点PK_CustomerRecords在插入LogEntry. 我怀疑这是由 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)<executionStack><frame>元素)。但更重要的是,我可以看到神秘索引 '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 上查找,从而获取可序列化锁。

The primary culprit here are the transaction isolation level (Serializable) and the EF behavior on InsertOrUpdate. I can't give advice on the EF behavior, but the serializable level is overkill for sure. Which takes us back to the error you get under read committed level, which, unfortunately, is again an EF error I can't comment on.

于 2010-03-21T01:15:14.310 回答