82

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

我附加了分析器,使用这篇关于故障排除死锁的优秀文章设置了跟踪配置文件,并捕获了一堆示例。奇怪的是,死锁写总是一样的

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

另一个死锁语句各不相同,但通常是对帖子表的某种琐碎、简单的读取。这个总是在僵局中被杀死。这是一个例子

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 有点像一把巨大的锤子,但是我们在这里运行的大多数查询不需要那么准确。你会关心你的用户资料是否过时了几秒钟吗?

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

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

4

22 回答 22

44

根据 MSDN:

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

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

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

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

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

ALTER Database [StackOverflow.Beta] SET READ_COMMITTED_SNAPSHOT ON
于 2008-08-21T20:53:51.283 回答
37

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

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

一种无需大量工作且不启用脏读即可解决此类死锁问题的简单方法是使用"Snapshot Isolation Mode"(SQL 2005 中的新功能),它将始终为您提供对最后未修改数据的干净读取。如果您想优雅地处理死锁语句,还可以相当容易地捕获并重试它们。

于 2008-08-21T15:23:10.173 回答
25

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

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

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

您现在有一个情况,A 锁定 X 并试图获取 Y,而 B 锁定 Y 并试图获取 X。

当然,我们需要 OP 更新他的帖子,提供更多关于哪些索引正在发挥作用的信息,以确认这是否真的是原因。

于 2008-09-16T19:19:01.077 回答
18

我对这个问题和随之而来的答案感到很不舒服。有很多“试试这个魔法尘埃!不要那个魔法尘埃!”

我在任何地方都看不到您对所占用的锁进行了分析,并确定了死锁的确切类型。

您所指出的只是发生了一些锁——而不是死锁。

在 SQL 2005 中,您可以通过以下方式获取有关正在取出哪些锁的更多信息:

DBCC TRACEON (1222, -1)

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

于 2008-08-26T09:20:03.580 回答
14

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

于 2008-08-21T14:35:41.973 回答
10

在用 NOLOCK 把房子烧掉以捕捉苍蝇之前,您可能想看一下您应该使用 Profiler 捕获的死锁图。

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

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

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

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

于 2008-08-22T07:24:15.607 回答
7

你会关心你的用户资料是否过时了几秒钟吗?

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

于 2008-08-21T14:22:43.563 回答
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 在这里不是你的朋友......

于 2009-05-15T19:37:19.670 回答
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 被收集)......

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

于 2008-08-21T14:50:34.450 回答
3

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

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

Paul Nielson 的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;    
于 2008-08-21T23:37:14.247 回答
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

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

于 2008-08-22T10:12:06.187 回答
3

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

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

同时在不提交上述内容的情况下,每个人都可以继续从该表中进行选择。如果你不熟悉 MVCC,你会感到震惊的是,没有它你也能活下去。严重地。

于 2008-08-25T18:45:37.473 回答
2

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

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

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

于 2008-08-21T15:16:15.450 回答
1

你会关心你的用户资料是否过时了几秒钟吗?

几秒钟肯定是可以接受的。无论如何,它似乎不会那么长,除非有大量的人同时提交答案。

于 2008-08-21T14:36:37.930 回答
1

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

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

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

当然我有一些时间压力,所以在同一时间尝试了很多事情,所以我不能 100% 确定是什么解决了它,但我有很高的信心 - 让我们这么说吧.

于 2008-08-21T15:02:30.647 回答
1

现在我看到了 Jeremy 的回答,我想我记得听说过最好的做法是为每个数据操作使用一个新的 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);
}
于 2008-08-21T16:40:18.523 回答
1

您应该实现脏读。

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

如果您的查询并不绝对需要完美的事务完整性,那么在访问具有高并发性的表时应该使用脏读。我假设您的 Posts 表就是其中之一。

这可能会给您所谓的“幻像读取”,即您的查询作用于尚未提交的事务中的数据。

我们不是在这里经营银行网站,我们不需要每次都完美无缺

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

没有它,我们将不得不将我们所做的每个 LINQ 调用(嗯,简单的读取调用,它们中的绝大多数)包装在一个 3-4 行的事务代码块中,这很难看

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

于 2008-08-21T18:34:38.170 回答
1

那么实现重试机制有什么问题呢?总是有可能发生死锁,所以为什么不使用一些逻辑来识别它,然后再试一次呢?

当重试系统很少启动时,至少其他一些选项不会引入性能损失吗?

另外,当重试发生时不要忘记某种日志记录,这样你就不会陷入罕见变得经常的情况。

于 2008-08-25T18:16:49.100 回答
0

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

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

Begin Tran
Insert into Table (Columns) Values (Values)
Select Max(ID) From Table
Commit Tran
于 2008-08-21T14:33:57.843 回答
0

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

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

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

于 2008-08-21T14:48:35.343 回答
0

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

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

于 2008-10-10T20:07:32.807 回答
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...
}

欢迎提出建议!

于 2008-10-28T13:00:26.193 回答