Click here to Skip to main content
Click here to Skip to main content

SQL SERVER - How To Handle Deadlock

By , 22 Sep 2009
 

Introduction

A deadlock is a situation wherein two transactions wait for each other to give up their respective locks.

When this happens, the SQL Server ends the deadlock by automatically choosing one and aborting the process, allowing the other process to continue. The aborted transaction is rolled back and an error message is sent to the user of the aborted process. Generally, the transaction that requires the least amount of overhead to rollback is the transaction that is aborted.

This article will explain how to handle deadlocks in a user-friendly way.

The Deadlock

Transaction A attempts to update table 1 and subsequently read/update data from table 2, whereas transaction B attempts to update table 2 and subsequently read/update data from table 1. In such situations, transaction A holds locks that transaction B needs to complete its task and vice versa; neither transaction can complete until the other transaction releases locks.

The Deadlock Situation

The below example shows the deadlock situation between the two transactions.

Transaction A

 BEGIN TRANSACTION

 UPDATE Customer SET LastName = 'John' WHERE CustomerId=111
 WAITFOR DELAY '00:00:05' -- Wait for 5 ms
 UPDATE Orders SET CustomerId = 1 WHERE OrderId = 221

 COMMIT TRANSACTION

Transaction B

 BEGIN TRANSACTION

 UPDATE Orders SET ShippingId = 12 WHERE OrderId = 221
 WAITFOR DELAY '00:00:05' -- Wait for 5 ms
 UPDATE Customer SET FirstName = 'Mike' WHERE CustomerId=111

 COMMIT TRANSACTION 

If both the transactions are executed at the same time, then Transaction A locks and updates Customer table whereas transaction B locks and updates Orders table. After a delay of 5 ms, transaction A looks for the lock on Orders table which is already held by transaction B and transaction B looks for lock on Customer table which is held by transaction A. So both the transactions cannot proceed further, the deadlock occurs and the SQL server returns the error message 1205 for the aborted transaction.

(1 row(s) affected)
Msg 1205, Level 13, State 45, Line 5
Transaction (Process ID 52) was deadlocked on lock resources with 
another process and has been chosen as the deadlock victim.
Rerun the transaction.

But what if you don't like the default behavior (aborting the transaction)? Can you change it? Yes, you can, by rewriting Transactions A and B as shown below.

Transaction A

RETRY: -- Label RETRY
BEGIN TRANSACTION
BEGIN TRY

	UPDATE Customer SET LastName = 'John' WHERE CustomerId=111
	WAITFOR DELAY '00:00:05'  -- Wait for 5 ms
	UPDATE Orders SET CustomerId = 1 WHERE OrderId = 221

	COMMIT TRANSACTION
END TRY
BEGIN CATCH
	PRINT 'Rollback Transaction'
	ROLLBACK TRANSACTION
	IF ERROR_NUMBER() = 1205 -- Deadlock Error Number
	BEGIN
		WAITFOR DELAY '00:00:00.05' -- Wait for 5 ms
		GOTO RETRY -- Go to Label RETRY
	END
END CATCH

Transaction B

RETRY: -- Label RETRY
BEGIN TRANSACTION
BEGIN TRY
	UPDATE Orders SET ShippingId = 12 Where OrderId = 221
	WAITFOR DELAY '00:00:05' -- Wait for 5 ms
	UPDATE Customer SET FirstName = 'Mike' WHERE CustomerId=111

	COMMIT TRANSACTION
END TRY
BEGIN CATCH
	PRINT 'Rollback Transaction'
	ROLLBACK TRANSACTION
	IF ERROR_NUMBER() = 1205 -- Deadlock Error Number
	BEGIN
		WAITFOR DELAY '00:00:00.05' -- Wait for 5 ms
		GOTO RETRY -- Go to Label RETRY
	END
END CATCH

Here I have used Label RETRY at the beginning of both the transactions. The TRY/CATCH method is used to handle the exceptions in the transactions. If the code within the TRY block fails, the control automatically jumps to the CATCH block, letting the transaction roll back, and if the exception is occurred due to deadlock (Error_Number 1205), the transaction waits for 5 milliseconds. The delay is used here because the other transaction (which is not aborted) can complete its operation within delay duration and release the lock on the table which was required by the aborted transaction. You can increase the delay according to the size of your transactions. After the delay, the transaction starts executing from the beginning (RETRY: Label RETRY at the beginning of the transaction) using the below statement:

GOTO RETRY -- Go to Label RETRY

This statement is used to transfer the control to the label named RETRY (which is at the beginning).

Now Execute the Transaction A and Transaction B at the same time. Both the transactions will execute successfully. Have a look into the outputs of the transaction where the exception occurred.

(1 row(s) affected)
Rollback Transaction

(1 row(s) affected)

(1 row(s) affected) 

Using RetryCounter

Now, I guess you understood how to handle deadlock without aborting the transaction. Let's move to the next interesting topic about deadlock. Imagine if there are more than two processes that read/update the Customer or Orders table at the same time. Below, I have modified both the transactions where I have shown how we can use RetryCounter to solve the problem.

Transaction A

DECLARE @RetryCounter INT
SET @RetryCounter = 1
RETRY: -- Label RETRY
BEGIN TRANSACTION
BEGIN TRY

	UPDATE Customer SET LastName = 'John' WHERE CustomerId=111
	WAITFOR DELAY '00:00:05'  -- Wait for 5 ms
	UPDATE Orders SET CustomerId = 1 WHERE OrderId = 221

	COMMIT TRANSACTION
END TRY
BEGIN CATCH
	PRINT 'Rollback Transaction'
	ROLLBACK TRANSACTION
	DECLARE @DoRetry bit; -- Whether to Retry transaction or not
	DECLARE @ErrorMessage varchar(500)
	SET @doRetry = 0;
	SET @ErrorMessage = ERROR_MESSAGE()
	IF ERROR_NUMBER() = 1205 -- Deadlock Error Number
	BEGIN
		SET @doRetry = 1; -- Set @doRetry to 1 only for Deadlock
	END
	IF @DoRetry = 1
	BEGIN
		SET @RetryCounter = @RetryCounter + 1 -- Increment Retry Counter By one
		IF (@RetryCounter > 3) -- Check whether Retry Counter reached to 3
		BEGIN
			RAISERROR(@ErrorMessage, 18, 1) -- Raise Error Message if 
				-- still deadlock occurred after three retries
		END
		ELSE
		BEGIN
			WAITFOR DELAY '00:00:00.05' -- Wait for 5 ms
			GOTO RETRY	-- Go to Label RETRY
		END
	END
	ELSE
	BEGIN
		RAISERROR(@ErrorMessage, 18, 1)
	END
END CATCH

Transaction B

DECLARE @RetryCounter INT
SET @RetryCounter = 1
RETRY: -- Label RETRY
BEGIN TRANSACTION
BEGIN TRY
	UPDATE Orders SET ShippingId = 12 Where OrderId = 221
	WAITFOR DELAY '00:00:05' -- Wait for 5 ms
	UPDATE Customer SET FirstName = 'Mike' WHERE CustomerId=111
	COMMIT TRANSACTION
END TRY
BEGIN CATCH
	PRINT 'Rollback Transaction'
	ROLLBACK TRANSACTION
	DECLARE @DoRetry bit; -- Whether to Retry transaction or not
	DECLARE @ErrorMessage varchar(500)
	SET @doRetry = 0;
	SET @ErrorMessage = ERROR_MESSAGE()
	IF ERROR_NUMBER() = 1205 -- Deadlock Error Number
	BEGIN
		SET @doRetry = 1; -- Set @doRetry to 1 only for Deadlock
	END
	IF @DoRetry = 1
	BEGIN
		SET @RetryCounter = @RetryCounter + 1 -- Increment Retry Counter By one
		IF (@RetryCounter > 3) -- Check whether Retry Counter reached to 3
		BEGIN
			RAISERROR(@ErrorMessage, 18, 1) -- Raise Error Message 
				-- if still deadlock occurred after three retries
		END
		ELSE
		BEGIN
			WAITFOR DELAY '00:00:00.05' -- Wait for 5 ms
			GOTO RETRY	-- Go to Label RETRY
		END
	END
	ELSE
	BEGIN
		RAISERROR(@ErrorMessage, 18, 1)
	END
END CATCH

The RetryCounter variable used here gives a chance for the transaction to execute again if it fails due to deadlock (Error_Number 1205). In this example, the transaction can try to execute up to three times if it fails due to a deadlock. This scenario would be very useful if the transaction looking for the lock which was not released by the other transactions for a long time. So the transaction can try three times to check whether the required lock is available.

History

  • 20th September, 2009: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Nitin Chilka
Software Developer (Senior) Proteans Software Solutions Pvt. Ltd.
India India
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5memberjooh5530 Apr '12 - 10:35 
great failover
GeneralMy vote of 5memberAkram El Assas24 Apr '12 - 13:41 
Good article.
QuestionMy vote of 5memberandrusha0077 Mar '12 - 3:34 
Hello, I know this is an old post and all, but I just had an observation about your code.
 
I don't know why you use the variable @doRetry?
 
Why instead of using
IF @DoRetry = 1
 
you dont consider using (like you did on your first code example)
 
IF ERROR_NUMBER() = 1205
 
I think it would make your 2nd code snippet cleaner Smile | :)
GeneralTransaction already in progressmembermtaubman6 May '11 - 6:32 
What happens if you're already in the middle of a transaction when this code is entered? How do you know where to start over from?
GeneralNice Articlememberlinuxjr16 May '10 - 13:23 
Thanks for sharing this.
GeneralExcellent!memberOmar Gamil24 Apr '10 - 23:31 
good solution for a common problem
thanks
GeneralGood articlememberDonsw14 Feb '10 - 11:00 
Good article , I agree with the other comments.
cheers,
Donsw
My Recent Article : CDC - Change Data Capture

GeneralNicegroupMd. Marufuzzaman30 Sep '09 - 6:23 
Nice article...Thumbs Up | :thumbsup:
 
I will not say I have failed 1000 times; I will say that I have discovered 1000 ways that can cause failure – Thomas Edison.


Don't forget to click [Vote] / [Good Answer] on the post(s) that helped you.
 
Thanks
Md. Marufuzzaman

GeneralThanks for good article :)memberchdisme28 Sep '09 - 18:46 
Thanks for good article Smile | :)
QuestionReassurancememberDan Towers28 Sep '09 - 6:28 
I just wrote almost identical code so solve exactly the same problem. The only difference is that I call raisexception, passing in the original exceptions' details so that you get sensible information returned to the user.
 
It was very reassuring to find this article, and find that I'd done it right!
 
Cristian Amarie, please could you elaborate on your method, if you think that it is cleaner / better, rather then just making a passing, critical comment?
GeneralIt might work, but...memberCristian Amarie28 Sep '09 - 1:11 
... this is the equivalent on Sleep/goto in C++.
Simulate a critical section/mutex in SQL and this will do it far more cleaner.
 
Nuclear launch detected

GeneralRe: It might work, but...memberNitin Chilka30 Sep '09 - 18:53 
Hi Cristian Amarie,
Thanks for your feedback.
 
Could u elaborate ur explaination.?
GeneralRe: It might work, but...memberCristian Amarie30 Sep '09 - 22:55 
You can use sp_getapplock to establish a single access point.
 
Or use a table with a single row and UPDLOCK to ensure at most one process will access it, as in:
 

-- create table Lock1 (ID int identity(1, 1) not null primary key)
 
begin transaction
declare @sql_critical_section int
select top 1 @sql_critical_section = ID
from Lock1 with(UPDLOCK) -- ensures <= 1 have UPDLOCK
 
-- ... do transaction thing

commit transaction

After commit, @sql_critical_section is released and available for another process.
 
Nuclear launch detected

GeneralRe: It might work, but...member_henke_10 Apr '11 - 0:23 
No, it's not cleaner. It's worse. The whole point of transactions is that you might want to retry them! What you are suggesting is the equivalent to using IsolationMode.Serializable for all transactions, which means that they are NOT actually transactions!
GeneralRe: It might work, but...memberCristian Amarie7 May '11 - 8:42 
Ha? It's about avoiding deadlock, not transaction. And this SELECT .. UPDLOCK is emulating a CRITICAL_SECTION.
 
Nuclear launch detected

GeneralRe: It might work, but...memberCristian Amarie7 May '11 - 8:44 
The whole point of transactions is .. retry? With R from ACID, right?
 
Nuclear launch detected

GeneralGood ArticlememberChitra Govindasamy24 Sep '09 - 22:48 
Thanx for sharing the info. Keep it up.
GeneralGood onemembercrudeCodeYogi24 Sep '09 - 4:42 
You can identify that the deadlock may occur the example given in the article, however in real world application it is very difficult to identify such transaction that can cause deadlocks.
By the way nice article though.
 
Books are as useful to a stupid person as a mirror is useful to a blind person. - Chanakya

GeneralRe: Good onememberNitin Chilka30 Sep '09 - 18:57 
Thanks for your feedback.
The deadlock can be removed by increasing the WAIT DELAY or RETRYCOUNTER.
you can divide the transaction in small chunks of instrunctions.
GeneralGood PointmemberAbhishek Sur22 Sep '09 - 13:30 
Nice to see this article. Explanation seems good. Thanks for sharing. 5 from me Smile | :)
 
Abhishek Sur

My Latest Articles
Create CLR objects in SQL Server 2005
C# Uncommon Keywords
Read/Write Excel using OleDB

Don't forget to click "Good Answer" if you like to.

GeneralGood one !mvpAbhijit Jana22 Sep '09 - 12:57 
Well explained. Thanks for sharing. 5ed Smile | :)
 
Abhijit Jana | Codeproject MVP
Web Site : abhijitjana.net
Don't forget to click "Good Answer" on the post(s) that helped you.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 22 Sep 2009
Article Copyright 2009 by Nitin Chilka
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid