我们在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小时前遇到了另一个死锁错误.同样的症状,同样的罪魁祸首写.
真正奇怪的是,您在上面看到的锁定写入SQL语句是非常特定的代码路径的一部分.它仅在向问题添加新答案时执行 - 它使用新答案计数和最后日期/用户更新父问题.显然,这与我们正在进行的大量读取相比并不常见!据我所知,我们在应用程序的任何地方都没有进行大量的写操作.
我意识到NOLOCK是一个巨大的锤子,但我们在这里运行的大多数查询都不需要那么准确.如果您的用户个人资料已过期几秒,您会关心吗?
正如Scott Hanselman在这里讨论的那样,将NOLOCK与Linq一起使用有点困难.
我们正在调整使用的想法
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
在基本数据库上下文中,以便我们所有的LINQ查询都有此设置.没有它,我们必须在3-4行事务代码块中包装我们所做的每个LINQ调用(好吧,简单的读取,这是绝大多数),这很难看.
我想我有点沮丧的是,SQL 2005中的琐碎读取可能会使写入死锁.我可以看到写/写死锁是一个很大的问题,但读取?我们这里没有经营银行网站,每次都不需要完美的准确性.
想法?思考?
您是否为每个操作实例化一个新的LINQ to SQL DataContext对象,或者您是否可能为所有调用共享相同的静态上下文?
Jeremy,我们在基本控制器中共享一个静态datacontext大部分:
private DBContext _db; ////// Gets the DataContext to be used by a Request's controllers. /// public DBContext DB { get { if (_db == null) { _db = new DBContext() { SessionName = GetType().Name }; //_db.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"); } return _db; } }
您是否建议我们为每个Controller或每页创建一个新的上下文,或者更经常?
根据MSDN:
http://msdn.microsoft.com/en-us/library/ms191242.aspx
当READ COMMITTED SNAPSHOT或ALLOW SNAPSHOT ISOLATION数据库选项为ON时,将为数据库中执行的所有数据修改维护逻辑副本(版本).每次特定事务修改行时,数据库引擎的实例都会在tempdb中存储该行的先前提交的映像的版本.每个版本都标有进行更改的事务的事务序列号.使用链接列表链接已修改行的版本.最新的行值始终存储在当前数据库中,并链接到tempdb中存储的版本化行.
对于短期运行事务,修改行的版本可能会缓存在缓冲池中,而不会写入tempdb数据库的磁盘文件.如果对版本化行的需求是短暂的,它将简单地从缓冲池中删除,并且可能不一定会产生I/O开销.
对额外开销似乎有轻微的性能损失,但可能可以忽略不计.我们应该测试以确保.
尝试设置此选项并从代码查询中删除所有NOLOCK,除非确实有必要.NOLOCK或在数据库上下文处理程序中使用全局方法来对抗数据库事务隔离级别是问题的创可贴.NOLOCKS将掩盖我们数据层的基本问题,并可能导致选择不可靠的数据,其中自动选择/更新行版本控制似乎是解决方案.
ALTER Database [StackOverflow.Beta] SET READ_COMMITTED_SNAPSHOT ON
NOLOCK和READ UNCOMMITTED是一个滑坡.除非您了解为什么首先发生死锁,否则不应该使用它们.我会担心你说,"我们已经为所有SQL查询添加了(nolock)".需要在任何地方添加WITH NOLOCK,这肯定表明您的数据层存在问题.
更新语句本身看起来有点问题.您是在事先确定计数,还是仅从对象中提取它?AnswerCount = AnswerCount+1
添加问题时可能是处理此问题的更好方法.然后,您不需要事务来获取正确的计数,您不必担心您可能会暴露自己的并发问题.
在没有大量工作且没有启用脏读的情况下解决这种类型的死锁问题的一种简单方法是使用"Snapshot Isolation Mode"
(SQL 2005中的新增功能),它将始终为您提供对最后未修改数据的清晰读取.如果要优雅地处理死锁语句,也可以相当容易地捕获并重试死锁语句.
OP的问题是问为什么会出现这个问题.这篇文章希望能够回答这个问题,同时让其他人能够制定出可能的解决方案.
这可能是与索引相关的问题.例如,假设表Posts有一个非聚集索引X,它包含ParentID和一个(或多个)正在更新的字段(AnswerCount,LastActivityDate,LastActivityUserId).
如果SELECT cmd对索引X执行共享读取锁定以通过ParentId进行搜索,然后需要对聚簇索引执行共享读取锁定以获取剩余列,而UPDATE cmd执行写入独占,则会发生死锁锁定聚簇索引并需要在索引X上获得写独占锁以更新它.
你现在有一个情况,A锁定X并试图获得Y而B锁定Y并试图获得X.
当然,我们需要OP更新他的帖子,其中包含有关正在使用哪些索引的更多信息,以确认这是否真的是原因.
我对这个问题及随之而来的答案感到非常不舒服.有很多"尝试这种神奇的尘埃!没有那魔法尘埃!"
我无法看到你已经分析了所采取的锁定的任何地方,并确定了什么类型的锁是死锁的.
你所指出的只是发生了一些锁定 - 而不是死锁.
在SQL 2005中,您可以使用以下命令获取有关正在取出哪些锁的更多信息:
DBCC TRACEON (1222, -1)
这样当发生死锁时你就会有更好的诊断功能.
您是否为每个操作实例化一个新的LINQ to SQL DataContext对象,或者您是否可能为所有调用共享相同的静态上下文?我最初尝试后一种方法,从我记忆中,它导致数据库中不必要的锁定.我现在为每个原子操作创建一个新的上下文.
在烧毁房子以便全部捕获NOLOCK的飞行之前,您可能想要查看应该使用Profiler捕获的死锁图表.
请记住,死锁需要(至少)2个锁.连接1有锁A,想要锁B - 反之亦然连接2.这是一个无法解决的情况,有人必须给.
到目前为止你所展示的内容是通过简单的锁定来解决的,Sql Server很乐意整天使用它.
我怀疑你(或LINQ)正在使用其中的UPDATE语句启动事务,并且事先选择其他一些信息.但是,您确实需要回溯死锁图以查找每个线程持有的锁,然后通过Profiler回溯以查找导致这些锁被授予的语句.
我希望至少有4个语句来完成这个难题(或者一个带有多个锁的语句 - 也许在Posts表上有一个触发器?).
如果您的用户个人资料已过期几秒,您会关心吗?
不 - 这完全可以接受.设置基本事务隔离级别可能是最好/最干净的方法.
典型的读/写死锁来自索引顺序访问。读取(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不是您的朋友在这里...