为什么这两个sql语句死锁? (死锁图表+详细信息)

时间:2010-03-20 14:00:07

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

我有以下死锁图,它描述了两个互相死锁的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.

Raw Deadlock Schema 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实例(这意味着它拥有自己的数据库连接,事务和存储库上下文)。所以我假设这意味着有三个不同的数据库连接同时发生。

最后,这是使用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的实施   InsertOrUpdate。它可能会做一个   首先查找,因为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帖子的顶部......会发生什么?

它有效。 WOWZ

它也适用于SerializableRead Commited - 当我不调用Delete方法时,它们都有效。

那么为什么删除方法会被锁定?是否因为select(serializable}会发生锁定并发生一些死锁?

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

  • 1st抓取数据实例。
  • 第二个(和第三个)抓取相同数据的另一个实例。
  • 现在,第1次删除。细。
  • 第二次删除..但是行已经消失了......所以我得到了关于影响意外行数(0)的奇怪错误。&lt; ==删除了零项。

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


更新

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

1 个答案:

答案 0 :(得分:4)

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

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

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

我的第一个建议是强制交易范围为read committed。在实践中几乎从不需要TransactionScopes的默认可序列化级别,这是一种性能损失,并且在这种特殊情况下,通过将范围锁定纳入等式,使死锁调查中出现了许多不必要的噪声,使一切变得复杂。请发布在read committed下发生的死锁信息。

另外,不要发布死锁图的图片。一张图片告诉千言万语不对,发布原始死锁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上进行搜索,从而获取可序列化的锁。

这里的主要元凶是事务隔离级别(Serializable)和InsertOrUpdate上的EF行为。我无法就EF行为给出建议,但可序列化级别肯定是过度杀伤。这让我们回到你在读取提交级别下得到的错误,不幸的是,这也是我无法评论的EF错误。