如何在单个进程中模拟 SQL Server 中的死锁?
Posted
技术标签:
【中文标题】如何在单个进程中模拟 SQL Server 中的死锁?【英文标题】:How to simulate a deadlock in SQL Server in a single process? 【发布时间】:2012-07-19 21:59:40 【问题描述】:我们的客户端代码检测到死锁,等待一段时间,然后重试请求最多 5 次。重试逻辑根据错误号 1205 检测死锁。
我的目标是测试各种存储过程中的死锁重试逻辑和死锁处理。我可以使用两个不同的连接创建死锁。但是,我想在单个存储过程本身内部模拟死锁。
死锁会引发以下错误消息:
消息 1205,第 13 级,状态 51,第 1 行 事务(进程 ID 66)原为 与另一个进程在锁定资源上死锁并已被选中 作为僵局的受害者。重新运行事务。
我看到这个错误信息在sys.messages
:
select * from sys.messages where message_id = 1205 and language_id = 1033
message_id language_id severity is_event_logged text
1205 1033 13 0 Transaction (Process ID %d) was deadlocked on %.*ls resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
我无法使用 RAISERROR
引发此错误:
raiserror(1205, 13, 51)
消息 2732,第 16 级,状态 1,第 1 行 错误号 1205 无效。 数字必须在 13000 到 2147483647 之间,不能是 50000。
我们的死锁重试逻辑检查错误号是否为 1205。死锁需要与普通死锁具有相同的消息 ID、级别和状态。
有没有办法模拟死锁(使用 RAISERROR 或任何其他方式)并只用一个进程获得相同的消息编号?
我们的数据库使用 SQL 2005 兼容性,但我们的服务器从 2005 到 2008 R2 有所不同。
【问题讨论】:
我认为这是不可能的——死锁的本质是两个不同的进程争夺一个对象的锁。单个进程可以取出锁,但它会与谁发生死锁呢?我会饶有兴趣地看这个问题。如果您从 C# 代码调用,则可以通过引发与死锁 SqlException 具有相同值 (SqlException.Number = 1205
) 的 SqlException 来模拟此操作。
我在并行查询中看到了查询内死锁,并且在使用两个单独的索引两次更新事务中的单行时,我看到了查询死锁本身。我认为这两个死锁都是 SQL Server 错误,已在各种服务包中修复。但是,是的,在单个存储过程中轻松模拟这一点非常方便。
遗憾的是,RaiseError 强制 msg id 超过 50,000,否则您可以很容易地模拟它。在 .Net 中,您当然可以在 SQL Server 中引发自己的错误,您的应用程序重新捕获该错误,然后将其重新抛出为编号为 1250 的 SqlException;或者您可以捕获此错误并将其视为死锁。
不确定是否可以使用 OPENROWSET
在单个存储过程中绊倒自己。可能值得一试。
仅仅是因为只需要调用一个存储过程吗?如果是这样,您可以调用一个存储过程,该过程使用 sp_start_job 生成一个单独的异步进程。
【参考方案1】:
正如许多人指出的那样,答案是否定的,单个进程本身无法可靠地死锁。我想出了以下解决方案来模拟开发或测试系统上的死锁..
在 SQL Server Management Studio 窗口中运行以下脚本。 (仅在 2008 R2 上测试。)您可以根据需要让它一直运行。
在要模拟死锁的地方,插入对sp_simulatedeadlock
的调用。运行你的进程,就会发生死锁。
完成测试后,停止 SSMS 查询并运行底部的清理代码。
/*
This script helps simulate deadlocks. Run the entire script in a SQL query window. It will continue running until stopped.
In the target script, insert a call to sp_simulatedeadlock where you want the deadlock to occur.
This stored procedure, also created below, causes the deadlock.
When you are done, stop the execution of this window and run the code in the cleanup section at the bottom.
*/
set nocount on
if object_id('DeadlockTest') is not null
drop table DeadlockTest
create table DeadlockTest
(
Deadlock_Key int primary key clustered,
Deadlock_Count int
)
go
if exists (select * from sysobjects where id = object_id(N'sp_simulatedeadlock')
AND objectproperty(id, N'IsProcedure') = 1)
drop procedure sp_simulatedeadlock
GO
create procedure sp_simulatedeadlock
(
@MaxDeadlocks int = -1 -- specify the number of deadlocks you want; -1 = constant deadlocking
)
as begin
set nocount on
if object_id('DeadlockTest') is null
return
-- Volunteer to be a deadlock victim.
set deadlock_priority low
declare @DeadlockCount int
select @DeadlockCount = Deadlock_Count -- this starts at 0
from DeadlockTest
where Deadlock_Key = 2
-- Trace the start of each deadlock event.
-- To listen to the trace event, setup a SQL Server Profiler trace with event class "UserConfigurable:0".
-- Note that the user running this proc must have ALTER TRACE permission.
-- Also note that there are only 128 characters allowed in the trace text.
declare @trace nvarchar(128)
if @MaxDeadlocks > 0 AND @DeadlockCount > @MaxDeadlocks
begin
set @trace = N'Deadlock Test @MaxDeadlocks: ' + cast(@MaxDeadlocks as nvarchar) + N' @DeadlockCount: ' + cast(@DeadlockCount as nvarchar) + N' Resetting deadlock count. Will not cause deadlock.'
exec sp_trace_generateevent
@eventid = 82, -- 82 = UserConfigurable:0 through 91 = UserConfigurable:9
@userinfo = @trace
-- Reset the number of deadlocks.
-- Hopefully if there is an outer transaction, it will complete and persist this change.
update DeadlockTest
set Deadlock_Count = 0
where Deadlock_Key = 2
return
end
set @trace = N'Deadlock Test @MaxDeadlocks: ' + cast(@MaxDeadlocks as nvarchar) + N' @DeadlockCount: ' + cast(@DeadlockCount as nvarchar) + N' Simulating deadlock.'
exec sp_trace_generateevent
@eventid = 82, -- 82 = UserConfigurable:0 through 91 = UserConfigurable:9
@userinfo = @trace
declare @StartedTransaction bit
set @StartedTransaction = 0
if @@trancount = 0
begin
set @StartedTransaction = 1
begin transaction
end
-- lock 2nd record
update DeadlockTest
set Deadlock_Count = Deadlock_Count
from DeadlockTest
where Deadlock_Key = 2
-- lock 1st record to cause deadlock
update DeadlockTest
set Deadlock_Count = Deadlock_Count
from DeadlockTest
where Deadlock_Key = 1
if @StartedTransaction = 1
rollback
end
go
insert into DeadlockTest(Deadlock_Key, Deadlock_Count)
select 1, 0
union select 2, 0
-- Force other processes to be the deadlock victim.
set deadlock_priority high
begin transaction
while 1 = 1
begin
begin try
begin transaction
-- lock 1st record
update DeadlockTest
set Deadlock_Count = Deadlock_Count
from DeadlockTest
where Deadlock_Key = 1
waitfor delay '00:00:10'
-- lock 2nd record (which will be locked when the target proc calls sp_simulatedeadlock)
update DeadlockTest
set Deadlock_Count = Deadlock_Count
from DeadlockTest
where Deadlock_Key = 2
rollback
end try
begin catch
print 'Error ' + convert(varchar(20), ERROR_NUMBER()) + ': ' + ERROR_MESSAGE()
goto cleanup
end catch
end
cleanup:
if @@trancount > 0
rollback
drop procedure sp_simulatedeadlock
drop table DeadlockTest
【讨论】:
【参考方案2】:您可以通过运行来利用微软似乎并不急于修复的错误
use tempdb
begin tran
go
CREATE TYPE dbo.IntIntSet AS TABLE(
Value0 Int NOT NULL,
Value1 Int NOT NULL
)
go
declare @myPK dbo.IntIntSet;
go
rollback
此 SQL 将导致自身发生死锁。在 Aaron Bertand 的博客http://sqlperformance.com/2013/11/t-sql-queries/single-tx-deadlock
中了解更多详情【讨论】:
这很有趣!谢谢你提到它。对于未来的读者,Microsoft 已将有关此问题的 Connect 报告关闭为“不会修复”,因为该行为是设计使然。 非常出色,先生。这让我得到了我想要的确切的东西,一个带有 1205 作为错误号的 SqlException。这个正在进入我的法术书。 超级!在 SQL Server 2017 中仍然有效!【参考方案3】:(显然我没有足够的声誉来添加评论。所以发布作为答案。)
死锁至少需要两个进程。唯一的例外是查询内并行死锁,这种死锁是不可能重现的。
但是,您可以在运行完全相同的查询(或 sp)的两个进程上模拟死锁。一些想法here
【讨论】:
【参考方案4】:这可以在单个会话中可靠地工作。使用服务代理激活来调用死锁所需的第二个线程。
注意 1:不包括清理脚本 注意2:必须启用服务代理: ALTER DATABASE dbname SET ENABLE_BROKER WITH ROLLBACK IMMEDIATE;
EXEC sp_executesql N'
CREATE OR ALTER PROCEDURE DeadlockReceive
AS
DECLARE @MessageBody NVARCHAR(1000);
RECEIVE @MessageBody = CAST(message_body AS NVARCHAR(1000) )FROM DeadlockQueue
SELECT @MessageBody
EXEC sp_executesql @MessageBody;'
IF EXISTS (SELECT * FROM sys.services WHERE name = 'DeadlockService') DROP SERVICE DeadlockService
IF OBJECT_ID('DeadlockQueue') IS NOT NULL DROP QUEUE dbo.DeadlockQueue
IF EXISTS (SELECT * FROM sys.service_contracts WHERE name = 'DeadlockContract') DROP CONTRACT DeadlockContract
IF EXISTS (SELECT * FROM sys.service_message_types WHERE name = 'DeadlockMessage') DROP MESSAGE TYPE DeadlockMessage
DROP TABLE IF EXISTS DeadlockTable1 ;
DROP TABLE IF EXISTS DeadlockTable2 ;
CREATE MESSAGE TYPE DeadlockMessage VALIDATION = NONE;
CREATE QUEUE DeadlockQueue WITH STATUS = ON, ACTIVATION (PROCEDURE_NAME = DeadlockReceive, EXECUTE AS SELF, MAX_QUEUE_READERS = 1);
CREATE CONTRACT DeadlockContract AUTHORIZATION dbo (DeadlockMessage SENT BY ANY);
CREATE SERVICE DeadlockService ON QUEUE DeadlockQueue (DeadlockContract);
CREATE TABLE DeadlockTable1 (Value INT); INSERT dbo.DeadlockTable1 SELECT 1;
CREATE TABLE DeadlockTable2 (Value INT); INSERT dbo.DeadlockTable2 SELECT 1;
DECLARE @ch UNIQUEIDENTIFIER
BEGIN DIALOG @ch FROM SERVICE DeadlockService TO SERVICE 'DeadlockService' ON CONTRACT DeadlockContract WITH ENCRYPTION = OFF ;
SEND ON CONVERSATION @ch MESSAGE TYPE DeadlockMessage (N'
set deadlock_priority high;
begin tran;
update DeadlockTable2 set value = 5;
waitfor delay ''00:00:01'';
update DeadlockTable1 set value = 5;
commit')
SET DEADLOCK_PRIORITY LOW
BEGIN TRAN
UPDATE dbo.DeadlockTable1 SET Value = 2
waitfor delay '00:00:01';
UPDATE dbo.DeadlockTable2 SET Value = 2
COMMIT
【讨论】:
【参考方案5】:使用 Parallel 在 C# 中重现的最简单方法 例如
var List = ... (add some items with same ids)
Parallel.ForEach(List,
(item) =>
ReportsDataContext erdc = null;
try
using (TransactionScope scope = new TransactionScope())
erdc = new ReportsDataContext("....connection....");
var report = erdc.Report.Where(x => x.id == item.id).Select(x => x);
report.Count++
erdc.SubmitChanges();
scope.Complete();
if (erdc != null)
erdc.Dispose();
catch (Exception ex)
if (erdc != null)
erdc.Dispose();
ErrorLog.LogEx("multi thread victim", ex);
更感兴趣的是如何在真正的跨线程情况下防止该错误?
【讨论】:
【参考方案6】:我很难让 Paul 的回答起作用。我做了一些小改动以使其正常工作。
关键是在过程本身内开始和回滚 sp_simulatedeadlock 事务。我没有对 Paul 的回答中的程序进行任何更改。
DECLARE @DeadlockCounter INT = NULL
SELECT @DeadlockCounter = 0
WHILE @DeadlockCounter < 10
BEGIN
BEGIN TRY
/* The procedure was leaving uncommitted transactions, I rollback the transaction in the catch block */
BEGIN tran simulate
Exec sp_simulatedeadlock
/* Code you want to deadlock */
SELECT @DeadlockCounter = 10
END TRY
BEGIN CATCH
Rollback tran simulate
PRINT ERROR_MESSAGE()
IF (ERROR_MESSAGE() LIKE '%deadlock%' OR ERROR_NUMBER() = 1205) AND @DeadlockCounter < 10
BEGIN
SELECT @DeadlockCounter +=1
PRINT @DeadlockCounter
IF @DeadlockCounter = 10
BEGIN
RAISERROR('Deadlock limit exceeded or error raised', 16, 10);
END
END
END CATCH
END
【讨论】:
以上是关于如何在单个进程中模拟 SQL Server 中的死锁?的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 SQL Server 中的存储过程在单个查询中插入多行
如何在单个结果集中列出 SQL Server 中所有数据库中的所有表?
从多个进程并发访问单个 Sql Server 压缩数据库文件