クライアント側のコードはデッドロックを検出し、間隔を待って、リクエストを最大5回再試行します。再試行ロジックは、エラー番号1205に基づいてデッドロックを検出します。
私の目標は、さまざまなストアドプロシージャ内のデッドロック再試行ロジックとデッドロック処理の両方をテストすることです。 2つの異なる接続を使用してデッドロックを作成できます。ただし、1つのストアドプロシージャ自体の内部でデッドロックをシミュレートしたいと思います。
デッドロックにより、次のエラーメッセージが表示されます。
メッセージ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またはその他の手段を使用して)デッドロックをシミュレートし、同じメッセージ番号を取得する方法はあります1つのプロセスのみ?
私たちのデータベースはSQL 2005互換性を使用していますが、サーバーは2005年から2008 R2まで異なります。
多くの人が指摘したように、答えは「いいえ」です。単一のプロセスは、それ自体を確実にデッドロックすることはできません。開発システムまたはテストシステムのデッドロックをシミュレートするために、次のソリューションを思いつきました。
以下のスクリプトを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
実行することでMicrosoftがすぐに修正できると思われるバグを悪用することができます
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
(どうやらコメントを追加するのに十分な評判がないので、回答として投稿します。)
デッドロックには少なくとも2つのプロセスが必要です。唯一の例外は、クエリ内の並列デッドロックであり、これは再現が不可能です。
ただし、まったく同じクエリ(またはsp)を実行する2つのプロセスでデッドロックをシミュレートできます。いくつかのアイデア ここ
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);
}
実際のクロススレッド状況でそのエラーを防ぐ方法についてもっと興味がありますか?
ポール、質問とフォローアップの回答をありがとう。あなたの投稿は、初めてStack Overflowに参加するきっかけになりました。
私はあなたの答えを機能させるのに苦労しました、そしてそれを機能させるために加えた小さな変更を共有したいだけです。それが誰かの命を救うなら、それだけの価値があります。重要なのは、プロシージャ自体の中でsp_simulatedeadlockトランザクションを開始してロールバックすることです。回答に記載されている手順は変更しませんでした。
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