诊断SQL Server 2005中的死锁
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了诊断SQL Server 2005中的死锁相关的知识,希望对你有一定的参考价值。
我们在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;
}
}
您是否建议我们为每个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
问:为什么首先将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
更有效。
皮肤猫的方法不止一种。数据库模式的过早反规范化可能会引入可伸缩性问题。
您肯定希望READ_COMMITTED_SNAPSHOT设置为on,默认情况下不是这样。这给你MVCC语义。这与Oracle默认使用的一样。拥有一个MVCC数据库是非常有用的,不使用一个是疯了。这允许您在事务中运行以下内容:
更新USERS设置FirstName ='foobar'; //决定睡一年
同时如果没有上述规定,每个人都可以继续从那张桌子中选择。如果你不熟悉MVCC,你会感到震惊,没有它你就能活下去。认真。
将默认值设置为未提交读取不是一个好主意。毫无疑问,你的意志会引入不一致,最终会出现比你现在更糟糕的问题。快照隔离可能效果很好,但它是对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;
过去对我有用的一件事是确保我的所有查询和更新以相同的顺序访问资源(表)。
也就是说,如果一个查询按照Table1,Table2的顺序更新,并且另一个查询按Table2,Table1的顺序更新它,那么您可能会看到死锁。
由于您使用的是LINQ,因此无法确定是否可以更改更新顺序。但这是值得关注的。
如果您的用户个人资料已过期几秒,您会关心吗?
几秒钟肯定是可以接受的。不管怎样,似乎不会那么长,除非有很多人同时提交答案。
我同意杰里米的观点。您询问是否应为每个控制器或每页创建一个新的数据上下文 - 我倾向于为每个独立查询创建一个新的数据上下文。
我正在构建一个解决方案,用于像你一样实现静态上下文,当我在压力测试期间向服务器(百万+)的野兽投掷大量请求时,我也随机获得读/写锁。
一旦我改变策略以在每个查询的LINQ级别使用不同的数据上下文,并且相信SQL服务器可以运行其连接池魔术,锁似乎就消失了。
当然我受到了一段时间的压力,所以在同一时间尝试了很多事情,所以我不能100%肯定这就是固定它,但我有很高的信心 - 让我们这样说吧。
你应该实现脏读。
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
如果您不一定要求查询具有完美的事务完整性,则在访问具有高并发性的表时应使用脏读。我假设你的帖子表就是其中之一。
这可能会为您提供所谓的“幻像读取”,即当您的查询处理来自尚未提交的事务的数据时。
我们这里没有经营银行网站,每次都不需要完美的准确性
使用脏读。你是对的,他们不会给你完美的准确性,但他们应该清除你死锁的问题。
没有它,我们必须在3-4行事务代码块中包装我们所做的每个LINQ调用(好吧,简单的读取,这是绝大多数),这是丑陋的
如果在“基本数据库上下文”上实现脏读,则如果需要事务完整性,则始终可以使用更高的隔离级别来包装单个调用。
那么实现重试机制有什么问题呢?总是会出现死锁的可能性,为什么没有一些逻辑来识别它并再试一次?
至少其他一些选项是否会引入重试系统很少开始时所采取的性能惩罚?
此外,在重试发生时不要忘记某种日志记录,这样您就不会经常遇到罕见的情况。
现在我看到Jeremy的回答,我想我记得最好的做法是为每个数据操作使用一个新的DataContext。 Rob Conery写了几篇关于DataContext的帖子,他总是把它们推出来而不是使用单例。
- http://blog.wekeroad.com/2007/08/17/linqtosql-ranch-dressing-for-your-database-pizza/
- http://blog.wekeroad.com/mvc-storefront/mvcstore-part-9/(见评论)
这是我们用于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);
}
我必须同意Greg,只要将隔离级别设置为读取未提交对其他查询没有任何不良影响。
我有兴趣知道,Jeff,如何在数据库级别设置它会影响查询,如下所示:
Begin Tran
Insert into Table (Columns) Values (Values)
Select Max(ID) From Table
Commit Tran
NOLOCK和READ UNCOMMITTED是一个滑坡。除非您了解为什么首先发生死锁,否则不应该使用它们。我会担心你说,“我们已经为所有SQL查询添加了(nolock)”。需要在任何地方添加WITH NOLOCK,这肯定表明您的数据层存在问题。
更新语句本身看起来有点问题。您是在事先确定计数还是从对象中提取它? AnswerCount = AnswerCount+1
在添加问题时可能是处理此问题的更好方法。然后,您不需要事务来获取正确的计数,并且您不必担心您可能会暴露自己的并发问题。
在没有大量工作且没有启用脏读的情况下解决此类死锁问题的一种简单方法是使用"Snapshot Isolation Mode"
(SQL 2005中的新增功能),它将始终为您提供对最后未修改数据的清晰读取。如果要优雅地处理死锁语句,也可以相当容易地捕获并重试死锁语句。
如果我的个人资料甚至已经过时几分钟,我也没关系。
它是否在失败后重新尝试读取?当发射大量随机读数时,肯定有可能在他们无法阅读时会发现一些随机读数。与读取数量相比,我使用的大多数应用程序都是非常少的写入,并且我确信读取数据与您获得的数字相差无几。
如果实施“READ UNCOMMITTED”并不能解决您的问题,那么在不了解更多有关处理的情况下很难提供帮助。可能有一些其他调整选项可以帮助这种行为。除非有一些MSSQL专家拯救,我建议将问题提交给供应商。
我会继续调整一切;磁盘子系统的性能如何?平均磁盘队列长度是多少?如果I / O正在备份,真正的问题可能不是这两个死锁的查询,它可能是另一个瓶颈系统的问题;你提到了一个已经调整了20秒的查询,还有其他人吗?
专注于缩短长时间运行的查询,我敢打赌,死锁问题将会消失。
有同样的问题,并且不能在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 ..以上是关于诊断SQL Server 2005中的死锁的主要内容,如果未能解决你的问题,请参考以下文章
SQL Server 2005 中的死锁!两个实时批量更新正在战斗。为啥?
SQL Server死锁诊断--同一行数据在不同索引操作下引起的死锁