在SQL Server 2005中诊断死锁


82

我们在Stack Overflow SQL Server 2005数据库中看到了一些有害但罕见的死锁情况。

我附加了事件探查器,并使用这篇有关解决死锁的出色文章来建立跟踪配置文件,并捕获了许多示例。奇怪的是,死锁写入总是相同的

UPDATE [dbo].[Posts]
SET [AnswerCount] = @p1, [LastActivityDate] = @p2, [LastActivityUserId] = @p3
WHERE [Id] = @p0

其他死锁语句有所不同,但通常是对posts表的一些琐碎,简单的读取。这个总是在僵局中被杀死。这是一个例子

SELECT
[t0].[Id], [t0].[PostTypeId], [t0].[Score], [t0].[Views], [t0].[AnswerCount], 
[t0].[AcceptedAnswerId], [t0].[IsLocked], [t0].[IsLockedEdit], [t0].[ParentId], 
[t0].[CurrentRevisionId], [t0].[FirstRevisionId], [t0].[LockedReason],
[t0].[LastActivityDate], [t0].[LastActivityUserId]
FROM [dbo].[Posts] AS [t0]
WHERE [t0].[ParentId] = @p0

确切地说,我们没有看到写/写死锁,而是看到了读/写。

目前,我们混合使用LINQ和参数化SQL查询。我们已经添加with (nolock)了所有的SQL查询。这可能有所帮助。我昨天还修复了一个(非常)写得不好的徽章查询,每次都要花费20秒以上的时间,而且每分钟都在运行。我希望这是一些锁定问题的根源!

不幸的是,大约2小时前,我又遇到了一个死锁错误。同样的症状,同样的罪魁祸首。

真正奇怪的是,您在上面看到的锁定write SQL语句是非常特定的代码路径的一部分。在将新答案添加到问题时才执行它-它使用新答案计数和最后日期/用户来更新父问题。显然,相对于我们正在进行的大量读取,这不是那么普遍!据我所知,我们并未在应用程序中的任何地方进行大量写入操作。

我意识到NOLOCK有点像大锤子,但是我们在这里运行的大多数查询不需要那么精确。您是否会担心用户配置文件过时几秒钟?

正如Scott Hanselman在这里讨论的那样,与Linq一起使用NOLOCK更加困难。

我们正在调情使用

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

在基础数据库上下文上运行,以便我们所有的LINQ查询都具有此设置。否则,我们将必须将我们进行的每个LINQ调用(很简单的读取代码,占绝大多数)包装在3-4行的事务代码块中,这很丑陋。

我想我对SQL 2005中的琐碎读取会在写入时陷入僵局感到有些沮丧。我可以看到写/写死锁是一个很大的问题,但是读吗?我们不在这里运行银行网站,我们每次都不需要完美的准确性。

有想法吗?有什么想法吗?


您要为每个操作实例化一个新的LINQ to SQL DataContext对象,还是为所有调用共享相同的静态上下文?

杰里米(Jeremy),在大多数情况下,我们在基本控制器中共享一个静态数据上下文:

private DBContext _db;
/// <summary>
/// Gets the DataContext to be used by a Request's controllers.
/// </summary>
public DBContext DB
{
    get
    {
        if (_db == null)
        {
            _db = new DBContext() { SessionName = GetType().Name };
            //_db.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
        }
        return _db;
    }
}

您是否建议我们为每个控制器,每个页面或..更频繁地创建一个新的上下文?


2
什么<a href=" en.wikipedia.org/wiki/... mode</a>您使用的是“悲观”(锁定为基础)或“乐观”(的<a href =” en.wikipedia.org/wiki/ ……)?
John Siracusa

我同意上述Guy的回应-而不是尝试解决症状,为什么不解决根本原因?将运行中的AnswerCount总计添加到Posts表中,您已经创建了潜在的阻止资源。Jeff是否愿意为StackOverflow发布他的ERD,以便人们可以批评?
andyp

2
哇-在您与Scott的播客中听说过此事。也不能相信它也不会以更好的配置开箱即用。我将向我们的DBA展示这个(因为他们也广泛使用“ nolock”)
Dan Esparza,

3
请参阅:samsaffron.com/archive/2008/08/27/Deadlocked+,因为这会造成死锁。打开快照隔离是解决此问题的好方法。
山姆·塞弗隆

Answers:


44

根据MSDN:

http://msdn.microsoft.com/en-us/library/ms191242.aspx

当“ READ COMMITTED SNAPSHOT”或“ ALLOW SNAPSHOT ISOLATION”数据库选项打开时,将为数据库中执行的所有数据修改保留逻辑副本(版本)。每次通过特定事务修改行时,数据库引擎的实例都会在tempdb中存储该行的先前提交映像的版本。每个版本都标有进行更改的事务的事务序列号。修改后的行的版本使用链接列表链接。最新的行值始终存储在当前数据库中,并链接到tempdb中存储的版本化行。

对于短期运行的事务,修改后的行的版本可能会缓存在缓冲池中,而不会写入到tempdb数据库的磁盘文件中。如果对版本化行的需求是短暂的,它将仅从缓冲池中删除,并且不一定会产生I / O开销。

额外的开销似乎对性能有轻微的影响,但它可以忽略不计。我们应该进行测试以确保。

尝试设置此选项,并从代码查询中删除所有NOLOCK,除非确实有必要。NOLOCK或在数据库上下文处理程序中使用全局方法来对抗数据库事务隔离级别是解决该问题的创可贴。NOLOCKS将掩盖我们数据层的基本问题,并可能导致选择不可靠的数据,而自动选择/更新行版本控制似乎是解决方案。

ALTER Database [StackOverflow.Beta] SET READ_COMMITTED_SNAPSHOT ON

3
“ NOLOCKS将掩盖我们数据层的基本问题” ... NOLOCK掩盖什么样的问题?如果我认为我需要NOLOCK,我应该寻找什么问题?
马特·汉密尔顿

3
那这个答案怎么会变成“答案”呢?我还是不明白为什么毫秒级读取会立即导致写入死锁?我猜“ user13484”的答案太糟糕了,如果是这种情况,则没有任何参考。
RichardTheKiwi 2011年

37

NOLOCKREAD UNCOMMITTED是一个滑坡。除非您了解死锁为什么首先发生,否则不要使用它们。我会担心,您说:“我们在所有SQL查询中添加了(nolock)”。需要到处添加WITH NOLOCK的确定信号表明您的数据层存在问题。

更新语句本身看起来有点问题。您是在交易中更早确定计数,还是只是从对象中提取计数?AnswerCount = AnswerCount+1添加问题时可能是解决此问题的更好方法。这样一来,您无需进行交易即可获得正确的计数,也不必担心潜在的并发问题。

解决这种类型的死锁问题而无需进行大量工作且不启用脏读的一种简单方法是使用"Snapshot Isolation Mode"(SQL 2005中的新增功能),该方法将始终为您提供最新未修改数据的清晰读取。如果要优雅地处理死锁的语句,还可以很容易地捕获并重试死锁的语句。


4
我和JEzell在一起-我首先关注的是'SET AnswerCount = <固定值>'。价值从何而来?这让我想知道是否在事务中的其他地方是否已经以一堆锁的方式检索了它。我将从那开始。是的,全球NOLOCK是一项创可贴。
科恩

25

OP的问题是问为什么会出现此问题。这篇文章希望回答这个问题,同时让其他人可以解决可能的解决方案。

这可能是与索引相关的问题。例如,假设表Posts具有一个非聚集索引X,它包含ParentID和一个(或多个)要更新的字段(AnswerCount,LastActivityDate,LastActivityUserId)。

如果SELECT cmd对索引X进行共享读取锁定以通过ParentId搜索,然后需要对聚集索引进行共享读取锁定以获取剩余的列,而UPDATE cmd进行写排斥操作,则会发生死锁锁定聚集索引,并且需要获得对索引X的写独占锁才能对其进行更新。

您现在遇到的情况是,A锁定X并试图获取Y,而B锁定Y并试图获取X。

当然,我们将需要OP来更新他的帖子,以获取有关正在运行的索引的更多信息,以确认这是否是真正的原因。


我同意这种分析-SELECT和UPDATE以不同的顺序处理行,因此每个都试图获取另一个拥有的行锁。
Mike Dimmick

这是对整个线程的最佳响应,并且仅解释了为什么实际发生死锁。不好的是它不是第一答案,因为它是这里最好的答案。
乔纳森·凯海耶斯

一些答案缺少关于锁在2个原子简单语句之间的意义。这是唯一尝试对其进行解释的帖子。尽管语句很简单,但是表更新可能涉及多个CIX和NCIX更新,这些更新扩展为许多操作。与涉及NCIX遍历的READ(CIX书签查找)相同。这与以相同顺序连接表等无关(人们会读问题吗?)
RichardTheKiwi 2011年

18

我对这个问题和随之而来的答案感到非常不自在。有很多“尝试一下魔尘!没有那魔尘!”

我看不到任何地方您已经分析了所使用的锁并确定了死锁的确切类型。

您所指示的只是发生了一些锁定-而不是死锁。

在SQL 2005中,您可以使用以下方法获取有关哪些锁被取出的更多信息:

DBCC TRACEON (1222, -1)

这样,当出现死锁时,您将可以进行更好的诊断。


13
SQL Server中的死锁监视器将立即处理死锁。DMV无法解决死锁,因为在您发现死锁之前会先选择并杀死受害者。
乔纳森·凯海耶斯

14

您要为每个操作实例化一个新的LINQ to SQL DataContext对象,还是为所有调用共享相同的静态上下文?我最初尝试了后一种方法,据我所记得,它导致了数据库中不必要的锁定。现在,我为每个原子操作创建一个新的上下文。


10

在烧毁房屋以让NOLOCK泛滥成灾之前,您可能需要看一下应该使用Profiler捕获的死锁图。

请记住,死锁至少需要2个锁。连接1具有锁A,需要锁B-对于连接2反之亦然。这是无法解决的情况,必须有人付出。

到目前为止,您所显示的内容是通过简单的锁定解决的,而Sql Server却很乐意整日这样做。

我怀疑您(或LINQ)正在使用其中的UPDATE语句启动事务,并在事先选择其他一些信息。但是,您确实需要在死锁图中回溯以找到每个线程持有的锁,然后在Profiler中回溯以查找导致授予这些锁的语句。

我希望至少有4条语句来完成这个难题(或者一条语句需要多个锁-也许Posts表上有一个触发器?)。



5

典型的读/写死锁来自索引顺序访问。读取(T1)在索引A上找到行,然后在索引B上查找投影列(通常是聚簇的)。写入(T2)更改索引B(群集)然后必须更新索引A。T1在A上具有S-Lck,在B上想要S-Lck,T2在B上具有X-Lck,在A上想要U-Lck。死锁,粉扑 T1被杀死。这在OLTP流量大且索引太多的环境中很普遍:)。解决方案是使读取不必从A跳到B(即A中包含的列,或从投影列表中删除列)或T2不必从B跳到A(不更新索引列)。不幸的是,linq不是您的朋友在这里...


BTW A和B是同一张表的索引
Remus Rusanu

3

@Jeff-我绝对不是这方面的专家,但是在几乎每个调用上实例化新上下文都取得了不错的效果。我认为这类似于在ADO的每次调用中创建一个新的Connection对象。开销并不像您想的那样糟糕,因为无论如何连接池仍将被使用。

我只是使用像这样的全局静态助手:

public static class AppData
{
    /// <summary>
    /// Gets a new database context
    /// </summary>
    public static CoreDataContext DB
    {
        get
        {
            var dataContext = new CoreDataContext
            {
                DeferredLoadingEnabled = true
            };
            return dataContext;
        }
    }
}

然后我做这样的事情:

var db = AppData.DB;

var results = from p in db.Posts where p.ID = id select p;

我会为更新做同样的事情。无论如何,我所获得的流量几乎没有您那么多,但是当我初期只与少数用户使用共享DataContext时,我肯定会遇到一些锁定。没有任何保证,但值得一试。

更新资料:然后再次查看您的代码,您仅在该特定控制器实例的生存期内共享数据上下文,这基本上看起来还不错,除非控制器中的多个调用以某种方式并发使用它。在一个有关该主题的话题中,ScottGu说:

控制器仅针对单个请求而存在-因此,在处理请求结束时,将对它们进行垃圾收集(这意味着已收集DataContext)...

所以无论如何,可能并非如此,但再次尝试一下,也许还要结合一些负载测试。


3

问:为什么将储存AnswerCountPosts表中?

另一种方法是Posts通过不将“回写”存储AnswerCount在表中,而是根据需要动态计算帖子的答案数,从而消除对表的“写回” 。

是的,这意味着您正在运行其他查询:

SELECT COUNT(*) FROM Answers WHERE post_id = @id

或更常见的情况(如果要在主页上显示此内容):

SELECT p.post_id, 
     p.<additional post fields>,
     a.AnswerCount
FROM Posts p
    INNER JOIN AnswersCount_view a
    ON <join criteria>
WHERE <home page criteria>

但这通常会导致INDEX SCAN资源使用,并且使用资源可能比使用更为有效READ ISOLATION

剥猫的方法不止一种。过早地对数据库架构进行反规范化会引入可伸缩性问题。


3

您绝对希望将READ_COMMITTED_SNAPSHOT设置为on,默认情况下不启用。这为您提供了MVCC语义。Oracle默认情况下使用的是同一件事。拥有MVCC数据库是如此有用,而不是一个疯狂的数据库。这使您可以在事务中运行以下命令:

更新用户集FirstName ='foobar'; //决定睡一年。

同时,无需进行上述操作,每个人都可以继续从该表中进行选择。如果您不熟悉MVCC,您会感到震惊,因为您曾经没有过它。说真的


3

将默认值设置为未提交不是一个好主意。毫无疑问,您将引入不一致之处,并最终遇到比现在严重的问题。快照隔离可能效果很好,但是它对Sql Server的工作方式进行了巨大的改变,并带来了巨大的负担。tempdb负担。

这是您应该执行的操作:使用try-catch(在T-SQL中)检测死锁条件。发生这种情况时,只需重新运行查询即可。这是标准的数据库编程实践。

Paul Nielson的Sql Server 2005圣经中有很好的这种技术示例。

这是我使用的快速模板:

-- Deadlock retry template

declare @lastError int;
declare @numErrors int;

set @numErrors = 0;

LockTimeoutRetry:

begin try;

-- The query goes here

return; -- this is the normal end of the procedure

end try begin catch
    set @lastError=@@error
    if @lastError = 1222 or @lastError = 1205 -- Lock timeout or deadlock
    begin;
        if @numErrors >= 3 -- We hit the retry limit
        begin;
            raiserror('Could not get a lock after 3 attempts', 16, 1);
            return -100;
        end;

        -- Wait and then try the transaction again
        waitfor delay '00:00:00.25';
        set @numErrors = @numErrors + 1;
        goto LockTimeoutRetry;

    end;

    -- Some other error occurred
    declare @errorMessage nvarchar(4000), @errorSeverity int
    select    @errorMessage = error_message(),
            @errorSeverity = error_severity()

    raiserror(@errorMessage, @errorSeverity, 1)

    return -100
end catch;    

2
为什么这个解决方案让我感到畏缩?我会看着为什么会陷入僵局..不是一个真正的穷人帮忙解决这个问题。
Pure.Krome,

2

过去为我工作的一件事是确保所有查询和更新访问资源(表)的顺序相同。

也就是说,如果一个查询按Table1,Table2的顺序更新,而另一个查询按Table2,Table1的顺序更新,则您可能会看到死锁。

由于您使用的是LINQ,因此不确定是否可以更改更新顺序。但这是值得一看的。


1

您是否会担心用户配置文件过时几秒钟?

几秒钟绝对可以接受。无论如何,它看起来不会那么长,除非有大量的人同时提交答案。


1

我同意杰里米的观点。您询问是否应该为每个控制器或每个页面创建一个新的数据上下文-我倾向于为每个独立查询创建一个新的数据上下文。

我目前正在构建一个解决方案,该解决方案用于像您一样实现静态上下文,并且当我在压力测试期间向服务器的野兽(数百万个)抛出大量请求时,我还随机获得了读/写锁。

一旦我改变了对每个查询在LINQ级别使用不同数据上下文的策略,并相信SQL Server可以发挥其连接池魔术的作用,这些锁似乎就消失了。

当然,我承受着一定的时间压力,所以要在同一时间尝试许多事情,所以我不能100%地确定这是解决问题的方法,但是我有很高的信心-这么说吧。


1

您应该实现脏读。

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

如果您的查询绝对不要求完美的事务完整性,那么在访问具有高并发性的表时,应该使用脏读。我认为您的“帖子”表就是其中之一。

这可能会给您带来所谓的“幻像读取”,即您的查询对来自尚未提交的事务的数据进行操作时。

我们不在这里运行银行网站,我们每次都不需要完美的准确性

使用脏读。您说对了,因为它们不会给您带来完美的准确性,但是它们应该可以解决死锁问题。

否则,我们将必须将我们进行的每个LINQ调用(很简单的读取,占绝大多数)包装在3-4行交易代码块中,这很丑陋

如果在“基础数据库上下文”上实现脏读,则在需要事务完整性时,始终可以使用更高的隔离级别包装各个调用。


1

那么实现重试机制有什么问题?总是有发生死锁的可能性,那么为什么不使用一些逻辑来识别死锁并重试呢?

至少其他一些选项是否会引入性能损失,而这种损失会在重试系统很少启动的情况下始终存在?

另外,重试发生时也不要忘记进行某种日志记录,这样您就不会陷入这种罕见的情况。


1

现在,我了解了杰里米的答案,我想我记得曾经听说过,最佳实践是对每个数据操作使用新的DataContext。罗伯·科纳里(Rob Conery)写了几篇有关DataContext的文章,他总是将其新闻化,而不是使用单例。

这是我们用于Video.Show的模式(链接到CodePlex中的源代码视图):

using System.Configuration;
namespace VideoShow.Data
{
  public class DataContextFactory
  {
    public static VideoShowDataContext DataContext()
    {
        return new VideoShowDataContext(ConfigurationManager.ConnectionStrings["VideoShowConnectionString"].ConnectionString);
    }
    public static VideoShowDataContext DataContext(string connectionString)
    {
        return new VideoShowDataContext(connectionString);
    }
  }
}

然后在服务级别(或更精确地说,用于更新):

private VideoShowDataContext dataContext = DataContextFactory.DataContext();

public VideoSearchResult GetVideos(int pageSize, int pageNumber, string sortType)
{
  var videos =
  from video in DataContext.Videos
  where video.StatusId == (int)VideoServices.VideoStatus.Complete
  orderby video.DatePublished descending
  select video;
  return GetSearchResult(videos, pageSize, pageNumber);
}

0

我必须同意Greg,只要将隔离级别设置为未提交就不会对其他查询产生任何不良影响。

杰夫,我很想知道,在数据库级别设置它会如何影响如下查询:

Begin Tran
Insert into Table (Columns) Values (Values)
Select Max(ID) From Table
Commit Tran

0

如果我的个人资料过时了几分钟,对我来说也很好。

读取失败后是否要重试?触发大量随机读取时,有可能会击中一些无法读取的数据。与读取次数相比,我使用的大多数应用程序的写入次数很少,并且我确信读取次数与您获得的读取次数相差无几。

如果实现“ READ UNCOMMITTED”无法解决您的问题,那么很难在不了解更多有关处理的情况下提供帮助。可能还有其他一些调整选项可以帮助解决此问题。除非有一些MSSQL专家来解救,否则我建议将问题提交给供应商。


0

我会继续调整一切;磁盘子系统的性能如何?平均磁盘队列长度是多少?如果I / O正在备份,则真正的问题可能不是这两个死锁的查询,可能是另一个使系统瓶颈的查询。您提到了一个经过20秒调整的查询,还有其他查询吗?

专注于缩短长期运行的查询,我敢打赌,僵局问题将消失。


0

遇到相同的问题,并且由于服务器未启用DTS(!),因此不能在TransactionScope上使用“ IsolationLevel = IsolationLevel.ReadUncommitted”。

那就是我用扩展方法所做的:

public static void SetNoLock(this MyDataContext myDS)
{
    myDS.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED");
}

因此,对于使用关键并发表的选择,我们启用“ nolock”,如下所示:

using (MyDataContext myDS = new MyDataContext())
{
   myDS.SetNoLock();

   //  var query = from ...my dirty querys here...
}

欢迎进餐!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.