诊断SQL Server 2005中的死锁

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

我附上了剖析器,使用这篇关于解决死锁的优秀文章设置了一个跟踪configuration文件,并捕获了一堆示例。 奇怪的是, 死锁的写法总是一样的

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

另一个死锁陈述各不相同,但通常是一些简单的,简单的阅读post表。 这个人总是在僵局中遇难。 这是一个例子

 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秒,每分钟运行一次。 我希望这是一些locking问题的根源!

不幸的是,大约两个小时前我又遇到了另一个死锁错误。 相同的确切症状,相同的罪魁祸首写。

真正奇怪的是,上面看到的locking写入SQL语句是非常特定的代码path的一部分。 只有当一个新的答案被添加到一个问题时才会被执行 – 它用新的答案数和最后的date/用户来更新父问题。 显然,这与我们正在进行的大量阅读不是那么常见! 据我所知,我们没有在应用程序的任何地方做大量的写入。

我意识到,NOLOCK是一个巨大的锤子,但我们在这里运行的大多数查询不需要那么准确。 你会关心,如果你的用户configuration文件是几秒钟过时?

像Scott Hanselman在这里讨论的那样,使用Linq的NOLOCK有点困难。

我们调情使用的想法

 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED 

在基本的数据库上下文,以便我们所有的LINQ查询都有这个集合。 如果没有这些,我们必须在3-4行事务代码块中包装我们所做的每个LINQ调用(也就是简单的读取,这是绝大多数的),这很丑陋。

我想我有点沮丧,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; } } 

你build议我们为每个控制器,每个页面创build一个新的上下文,还是更经常地?

根据MSDN:

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

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

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

额外的开销似乎有一个小小的性能损失,但可能可以忽略不计。 我们应该testing以确保。

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

 ALTER Database [StackOverflow.Beta] SET READ_COMMITTED_SNAPSHOT ON 

NOLOCKREAD UNCOMMITTED是一个滑坡。 除非你明白为什么僵局首先发生,否则你不应该使用它们。 我担心你说:“我们已经添加了(nolock)所有的SQL查询”。 需要随处添加WITH NOLOCK是一个肯定的迹象,表明在数据层中有问题。

更新语句本身看起来有点问题。 你在交易中早些时候确定了计数,还是只是从一个对象中拉出来呢? AnswerCount = AnswerCount+1当一个问题被添加可能是一个更好的方法来处理这个问题。 那么你不需要一个事务来获得正确的计数,你不必担心你可能暴露自己的并发问题。

一个简单的方法来解决这种types的死锁问题,没有太多的工作,也没有启用脏读取,就是使用"Snapshot Isolation Mode" (SQL 2005中的新增function),它总是能够清楚地读取最后未修改的数据。 如果你想处理它们,你也可以相当容易地捕获和重试死锁的语句。

OP的问题是问为什么会出现这个问题。 这篇文章希望能够回答这个问题,同时让别人解决可能的解决scheme。

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

如果SELECT cmd在索引X上执行共享读取locking以便由ParentId进行search,然后需要对聚集索引执行共享读取locking以获取其余列,而UPDATE cmd执行写入独占时会发生死锁locking聚簇索引并需要在索引X上获得写独占锁来更新它。

你现在有一个情况,lockingX,并试图获得Y而BlockingY,并试图获得X.

当然,我们需要OP来更新他的post,提供更多关于哪些指数正在起作用的信息,以确认这是否是真正的原因。

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

我看不到任何你已经分析的锁,并确定什么types的锁死锁。

你所指出的只是发生了一些locking – 而不是什么是死锁。

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

DBCC TRACEON(1222,-1)

所以当发生死锁的时候,你会有更好的诊断。

你是否为每个操作实例化了一个新的LINQ to SQL DataContext对象,或者你可能为所有的调用共享相同的静态上下文? 我最初尝试了后一种方法,并从我记得,它导致DB中不需要的locking。 我现在为每个primefaces操作创build一个新的上下文。

在烧房子之前,先用NOLOCK把一只苍蝇赶下来,你可能想看看你应该用Profiler捕捉到的死锁图。

记住一个死锁需要(至less)2个锁。 连接1有锁A,想要连接2,反之亦然。这是一个无法解决的情况,有人不得不放弃。

到目前为止所显示的是通过简单的locking来解决的,Sql Server整天都很乐意。

我怀疑你(或者LINQ)正在用那个UPDATE语句开始一个事务,并且事先select一些其他的信息。 但是,您真的需要通过死锁图回溯才能find每个线程所拥有的锁,然后通过Profiler回溯以查找导致这些锁被授予的语句。

我期望至less有4个语句来完成这个难题(或者是一个需要多个锁的语句 – 也许在Posts表中有一个触发器)。

你会关心,如果你的用户configuration文件是几秒钟过时?

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

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

@Jeff – 我绝对不是这方面的专家,但是在几乎所有的调用中都实例化了一个新的上下文。 我认为这与使用ADO进行每次调用时创build一个新的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; 

而且我会为更新做同样的事情。 无论如何,我没有像你那么多的stream量,但是当我使用一个共享的DataContext的时候,我确实得到了一些locking,只有less数用户。 没有保证,但可能值得一试。

更新 :然后再看看你的代码,你只是共享该特定的控制器实例的生命周期的数据上下文,基本上看起来很好,除非它在某种程度上由控制器内的多个调用同时使用。 ScottGu在一个话题上说:

控制器只能处理单个请求 – 所以在处理请求结束时,它们被垃圾回收(这意味着DataContext被收集)。

所以无论如何,这可能不是这样,但也可能值得一试,也许与一些负载testing结合起来。

您一定要将READ_COMMITTED_SNAPSHOT设置为打开,这不是默认设置。 这给你MVCC的语义。 这与Oracle默认使用的是相同的。 有一个MVCC数据库是非常有用的,不使用一个是疯了。 这使您可以在事务中运行以下内容:

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

同时没有提到上面,每个人都可以继续从该表中select。 如果你不熟悉MVCC,你会惊讶于你没有它能够生存下去。 认真。

将默认设置为未提交读取不是一个好主意。 毫无疑问,你的意志不一致会导致一个比现在更糟糕的问题。 快照隔离可能工作得很好,但是这对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; 

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

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

不知道自从使用LINQ以来是否可以更改更新的顺序。 但是这是值得关注的。

问:为什么您首先将“ AnswerCount存储在“ Posts表中?

另一种方法是通过不在表中存储AnswerCount来消除对Posts表的“回写”,而是根据需要dynamic地计算对post的回答数。

是的,这意味着你正在运行一个额外的查询:

 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更有效。

有一个以上的方法来剥皮猫。 过早地对数据库模式进行非规范化可能会引起可伸缩性问题。

你会关心,如果你的用户configuration文件是几秒钟过时?

几秒钟肯定是可以接受的。 似乎不会这么长,反正,除非有大量的人同时提交答案。

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

我正在构build一个解决scheme,用于像你一样实现静态上下文,当我在压力testing期间向服务器(百万+)的野兽投掷了大量的请求时,我也随机地读取/写入locking。

只要我改变我的策略,在每个查询的LINQ级别使用不同的数据上下文,并相信SQL服务器可以使用连接池魔术,那么这些锁似乎就消失了。

当然,我受到了一些时间的压力,所以在同一时间尝试了很多东西,所以我不能100%肯定这是固定的,但我有很高的信心 – 让我们这样说吧。

你应该实现脏读。

 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED 

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

这可能会给你所谓的“幻像读取”,这是当你的查询作用于未提交的事务的数据时。

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

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

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

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

那么执行一个重试机制有什么问题? 总会有可能发生死锁,为什么不能有一些逻辑来识别它,然后再试一次呢?

至less其他一些scheme是否会引入性能处罚措施,而这些处罚措施很less会在重试系统启动的时候进行?

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

现在,我看到杰里米的回答,我想我记得听说最好的做法是为每个数据操作使用新的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); } 

只要将隔离级别设置为未提交读取对其他查询没有任何不良影响,我就不得不同意Greg的观点。

我有兴趣知道,杰夫,如何在数据库级别设置它会影响一个查询,如下所示:

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

如果我的个人资料甚至已经过了几分钟,那我也没问题。

失败之后,您是否正在重新尝试读取? 发射大量随机读数当然是可能的,只有less数读数无法读取时才能读取。 与读取次数相比,我使用的大多数应用程序都是非常less的写入操作,而且我确定读取的数量不会超出所获得的数量。

如果执行“READ UNCOMMITTED”并不能解决您的问题,那么很难在不知道更多关于处理的情况下提供帮助。 可能有一些其他的调整选项,这将有助于这种行为。 除非一些MSSQL大师来拯救,否则我build议将问题提交给供应商。

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

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

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

那是我用扩展方法做的:

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

因此,为了select使用关键并发表的人,我们启用这样的“nolock”:

 using (MyDataContext myDS = new MyDataContext()) { myDS.SetNoLock(); // var query = from ...my dirty querys here... } 

欢迎来到!