诊断SQL Server 2005中的死锁

时间:2008-08-21 14:18:42

标签: sql-server sql-server-2005 deadlock

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

我附加了探查器,使用this excellent article on troubleshooting deadlocks设置了跟踪配置文件,并捕获了一堆示例。奇怪的是,死锁写总是相同

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小时前我又遇到了另一个死锁错误。同样的症状,同样的罪魁祸首写道。

真正奇怪的是,您在上面看到的锁定写入SQL语句是非常特定的代码路径的一部分。在向问题添加新答案时,仅执行 - 它会使用新答案计数和最后日期/用户更新父问题。显然,这与我们正在进行的大量读取相比并不常见!据我所知,我们在应用程序的任何地方都没有进行大量的写操作。

我意识到NOLOCK是一个巨大的锤子,但我们在这里运行的大多数查询都不需要那么准确。如果您的用户个人资料已过期几秒,您会关心吗?

使用NOLOCK和Linq比Scott Hanselman discusses here更难。

我们正在调整使用

的想法
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

在基本数据库上下文中,以便我们所有的LINQ查询都有此设置。没有它,我们必须在3-4行事务代码块中包装我们所做的每个LINQ调用(好的,简单的读取,这是绝大多数),这很难看。

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

想法?想法?


  

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

Jeremy,我们在基本控制器中共享一个静态datacontext:

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;
    }
}

您是否建议我们为每个控制器或每页创建一个新的上下文,或者更经常地创建?

22 个答案:

答案 0 :(得分:44)

根据MSDN:

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

  

当任何一个   阅读提交的快照或   允许SNAPSHOT ISOLATION数据库   选项为ON,逻辑副本   (版本)为所有数据维护   在...执行的修改   数据库。每次修改一行   通过特定的交易,   数据库引擎存储的实例   以前提交的版本   tempdb中行的图像。每   版本标记有事务   交易的序号   这改变了。的版本   修改的行使用链接链接   名单。最新的行值始终是   存储在当前数据库中   链接到存储的版本化行   在tempdb。

     

对于短期交易,a   修改后的行的版本可能会得到   没有缓存在缓冲池中   被写入的磁盘文件   tempdb数据库。如果需要的话   版本化的行是短暂的,它   只会从中掉下来   缓冲池,可能不一定   招致I / O开销。

额外开销似乎会有轻微的性能损失,但可能会忽略不计。我们应该测试以确保。

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

ALTER Database [StackOverflow.Beta] SET READ_COMMITTED_SNAPSHOT ON

答案 1 :(得分:37)

NOLOCK READ UNCOMMITTED 是一个滑坡。除非您了解为什么首先发生死锁,否则不应该使用它们。我会担心你说,“我们已经为所有SQL查询添加了(nolock)”。需要在任何地方添加 WITH NOLOCK ,这肯定表明您的数据层存在问题。

更新语句本身看起来有点问题。您是在事先确定计数还是从对象中提取它? AnswerCount = AnswerCount+1添加问题时可能是处理此问题的更好方法。然后,您不需要事务来获取正确的计数,并且您不必担心可能会让您自己暴露的并发问题。

在没有大量工作且没有启用脏读的情况下解决此类死锁问题的一种简单方法是使用"Snapshot Isolation Mode"(SQL 2005中的新增内容),它将始终为您提供对上一次未修改的内容的清晰读取数据。如果要优雅地处理死锁语句,也可以很容易地捕获并重试死锁语句。

答案 2 :(得分:25)

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

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

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

你现在有一种情况,A锁定X并试图获得Y,而B锁定Y并试图获得X.

当然,我们需要OP更新他的帖子,其中包含有关正在使用哪些索引的更多信息,以确认这是否真的是原因。

答案 3 :(得分:18)

我对这个问题及随之而来的答案感到非常不舒服。有很多“尝试这种神奇的尘埃!没有那魔法尘埃!”

我无法看到你已经分析了所采取的锁定的任何地方,并确定了什么类型的锁是死锁的。

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

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

DBCC TRACEON (1222, -1)

这样当发生死锁时,您将获得更好的诊断功能。

答案 4 :(得分:14)

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

答案 5 :(得分:10)

在烧毁房子以便全面捕捉NOLOCK的飞行之前,您可能需要查看应该使用Profiler捕获的死锁图。

请记住,死锁需要(至少)2个锁。连接1有锁A,想要锁B - 反之亦然连接2.这是一个无法解决的情况,有人必须给。

到目前为止,你所展示的内容是通过简单的锁定解决的,Sql Server很乐意整天使用它。

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

我希望至少有4个语句可以完成这个难题(或者是一个带有多个锁的语句 - 也许在Posts表上有一个触发器?)。

答案 6 :(得分:7)

  

如果您的用户个人资料已过期几秒,您会关注吗?

不 - 这完全可以接受。设置基本事务隔离级别可能是最好/最干净的方法。

答案 7 :(得分:4)

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

答案 8 :(得分:3)

将默认设置为未提交读取不是一个好主意。毫无疑问,你的意志会引入不一致,最终会出现比你现在更糟糕的问题。快照隔离可能效果很好,但它对Sql Server的工作方式进行了重大改变,并在tempdb上加载了巨大的

这是你应该做的:使用try-catch(在T-SQL中)来检测死锁条件。当它发生时,只需重新运行查询。这是标准的数据库编程实践。

在保罗·尼尔森的Sql Server 2005 Bible中有很好的例子。

这是我使用的快速模板:

-- 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;    

答案 9 :(得分: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)...

所以无论如何,这可能不是它,但是它可能值得一试,也许与一些负载测试相结合。

答案 10 :(得分:3)

Q值。为什么首先将AnswerCount存储在Posts表中?

另一种方法是通过不在表中存储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更有效。

皮肤猫的方法不止一种。数据库模式的过早反规范化可能会引入可伸缩性问题。

答案 11 :(得分:3)

您肯定希望将READ_COMMITTED_SNAPSHOT设置为on,默认情况下不是这样。这给你MVCC语义。这与Oracle默认使用的一样。拥有一个MVCC数据库是非常有用的,不使用一个是疯了。这允许您在事务中运行以下内容:

更新USERS设置FirstName ='foobar'; //决定睡一年。

同时没有提交上述内容,每个人都可以继续从该表中选择。如果你不熟悉MVCC,你会感到震惊,没有它你就能活下去。认真。

答案 12 :(得分:2)

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

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

由于您使用的是LINQ,因此无法确定是否可以更改更新顺序。但这是值得关注的。

答案 13 :(得分:1)

现在我看到Jeremy的回答,我想我记得最好的做法是为每个数据操作使用一个新的DataContext。 Rob Conery写了几篇关于DataContext的帖子,他总是把它们推出来而不是使用单例。

这是我们用于Video.Show(link to source view in 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);
}

答案 14 :(得分:1)

你应该实现脏读。

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

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

这可能会为您提供所谓的“幻像读取”,即当您的查询处理来自尚未提交的事务的数据时。

  

我们这里没有经营银行网站,我们每次都不需要完美的准确度

使用脏读。你是对的,他们不会给你完美的准确性,但他们应该清除你死锁的问题。

  

如果没有这个,我们必须在3-4行事务代码块中包装我们所做的每个LINQ调用(好吧,简单的读取,这是绝大多数),这很难看

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

答案 15 :(得分:1)

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

我正在构建一个解决方案,用于像你一样实现静态上下文,当我在压力测试期间在服务器的野兽(百万+)上投掷大量请求时,我也得到了读/写锁随机

一旦我改变了我的策略,在每个查询的LINQ级别使用不同的数据上下文,并且相信SQL服务器可以运行其连接池魔法,锁似乎就会消失。

当然我受到了一段时间的压力,所以在同一时间尝试了很多事情,所以我不能100%肯定这是什么修复它,但我有很高的信心 - 让我们放就这样。

答案 16 :(得分:1)

那么实现重试机制有什么问题?总是会出现死锁的可能性,为什么没有一些逻辑来识别它并再试一次?

至少其他一些选项会不会引入重试系统很少开始时所采取的性能惩罚?

另外,在重试发生时不要忘记某种日志记录,这样你就不会经常遇到罕见的情况。

答案 17 :(得分:1)

  

如果您的用户个人资料已过期几秒,您会关注吗?

几秒钟肯定是可以接受的。不管怎样,似乎不会那么长,除非有很多人同时提交答案。

答案 18 :(得分:0)

如果我的个人资料甚至过时了几分钟,我也没关系。

它是否在失败后重新尝试读取?当发射大量随机读数时,肯定有可能在他们无法阅读时会发现一些随机读数。与读取次数相比,我使用的大多数应用程序都是非常少的写入,并且我确信读取数据与您获得的数字相差无几。

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

答案 19 :(得分:0)

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

专注于缩短长时间运行的查询,我敢打赌,死锁问题将会消失。

答案 20 :(得分:0)

有同样的问题,并且不能在TransactionScope上使用“IsolationLevel = IsolationLevel.ReadUncommitted”,因为服务器没有启用DTS(!)。

这就是我用扩展方法做的事情:

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...
}

欢迎消费!

答案 21 :(得分:0)

只要将隔离级别设置为未提交读取,就不会对Greg达成一致意见。对其他查询没有任何不良影响。

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

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