March 30, 2005 at 5:42 pm
We're currently developing a multithreading windows service which is simply dispatching work to the server farm.
The deadlock occured in one of the stored procedures. The sp is pretty simple, it first gets the minimum id in the table, then set machine name and windows service name for that row. Finally return that row.
here is the sp:
-----------------------------------------------------
CREATE PROCEDURE dbo.GetNextUnprocessedMessage
@StateNew varchar(20),
@StateNext varchar(20),
@WinServiceName varchar(20),
@MachineName varchar(20)
AS
DECLARE @id int
BEGIN TRANSACTION
SET @id = -1;
-- grab the next available message, if present
SET @id = (
SELECT MIN(unprocessed_message_id)
FROM unprocessed_message
WITH (HOLDLOCK, ROWLOCK)
WHERE state = @StateNew
)
-- update the state of the row we just selected (and locked) so that it can't
-- be grabbed by anyone else using this procedure
UPDATE unprocessed_message
WITH(ROWLOCK)
SET state = @StateNext,
win_service_name = @WinServiceName,
machine_name = @MachineName
WHERE unprocessed_message_id = @id
IF (@@ERROR > 0)
BEGIN
ROLLBACK TRANSACTION
RAISERROR('Error fetching next message', 15, -1, @@ERROR)
END
-- return the newly updated row
SELECT *
FROM unprocessed_message
WHERE unprocessed_message_id = @id
COMMIT TRANSACTION
GO
--------------------------------------------------
we set clustered index (unprocessed_message_id) on unprocessed_message, and the isolation level is default (read committed).
please help me on this issue. Thank you.
David
March 30, 2005 at 7:01 pm
Using 'with HoldLock' raises the isolation level to Serializable and will place a range lock on the index for the column 'state'. If 'state' is not indexed then an excessive amout of locks may be caused to be taken.
In your case I think you can get away with an isolation level of 'Repeatable Read' which would only lock the one row retrieved not all rows with state = @StateNew. I think 'with updlock' may give you the equivalent of 'Repeatalbe Read' but I'm not certain about that. Perhaps someone else can confirm.
A minor point, the commit statement can be moved up in the code to just before the last 'Select' that returns the newly updated row to the client. This will reduce the amount of time the locks are held.
Hope that helps - ron
March 31, 2005 at 3:57 am
Perhaps you could get away with just an update, and not having to deal with transactions and locking rows to prevent others from reading in 'mid-process' of the retrieval/update..
Here's an idea, basically based on the idea that you don't necessarily need to get the lowest id, but just any single record with the current state that needs to be processed..
create table #x
( id int not null primary key,
state int not null,
name varchar(20) null
)
insert #x select 1, 1, 'foobar'
insert #x select 2, 0, null
insert #x select 3, 0, null
declare @id int, @stateNew int, @stateNext int, @name varchar(20)
select @stateNew = 0, @stateNext = 1, @name = 'bozo'
set rowcount 1
update #x
set @id = id,
state = @stateNext,
name = @name
where state = @stateNew
set rowcount 0
select @id --contains the id that was updated
..then one can take @id and do further stuff, now knowing which row was updated...
/Kenneth
March 31, 2005 at 9:27 am
Ron,
Thank you for the idea. I may try that to see if it works.
Ken,
Thank you for the help too.
In our case, we have to get the min id, because the processing order of messages are critical. We can't simply retrieve one message and do the update. Sorry for not clearify that.
March 31, 2005 at 11:51 am
Personally, I would persue Ken's Idea and combine both (select + update) into one statement that way you could get rid of the locking problem:
ex:
UPDATE unprocessed_message
SET state = @StateNext,
@id = id,
win_service_name = @WinServiceName,
machine_name = @MachineName
WHERE unprocessed_message_id = SELECT MIN(unprocessed_message_id)
FROM unprocessed_message
WHERE state = @StateNew
hth
* Noel
March 31, 2005 at 2:45 pm
Noel,
BOL:
Transactions must be run at an isolation level of repeatable read or higher to prevent lost updates that can occur when two transactions each retrieve the same row, and then later update the row based on the originally retrieved values. If the two transactions update rows using a single UPDATE statement and do not base the update on the previously retrieved values, lost updates cannot occur at the default isolation level of read committed.
So your suggestion will work fine but its not really avoiding the locking issue as the lock is implied and taken by the system to prevent a lost update situation. That is, other users using an update statement are blocked from reading/updating the row until the first update is complete.
Of course doing it all in one statement is more efficient and allows for shorter period of time that the lock is held.
March 31, 2005 at 3:02 pm
Noel,
I'm not at a machine where I test at the moment but is that syntax correct? I haven't seen it before. If so, live and learn.
UPDATE unprocessed_message
SET state = @StateNext,
@id = id, <-------------
...
EDIT: never mind -- see post below.
March 31, 2005 at 5:05 pm
I guess I can answer my own question from BOL:
Changes existing data in a table.
UPDATE
{
table_name WITH ( <table_hint_limited> [...n])
| view_name
| rowset_function_limited
}
SET
{column_name = {expression | DEFAULT | NULL}
| @variable = expression
| @variable = column = expression } [,...n]
April 1, 2005 at 1:37 am
The point with this 'backward' notation, is that you can update (the other columns) and assign values from columns not updated (ie the PK) in one go.
The net effect is the same as doing update + select from the row updated (for this you need to use explicit transactions in order to prevent others from reading your row in 'mid-process').
However, if all you do is just a single update, then it's impossible for others to read the same key value, since only one at a time can update a row.
The same technique can be used for generating your own keyvalues based on a counter table - all issues with risks assigning the same value to simultaneous readers will just automagically dissappear.
/Kenneth
April 1, 2005 at 7:10 am
I am interested to know if noeld's elegant solution really does avoid deadlock.:
UPDATE unprocessed_message
SET state = @StateNext,
@id = id,
win_service_name = @WinServiceName,
machine_name = @MachineName
WHERE unprocessed_message_id = SELECT MIN(unprocessed_message_id)
FROM unprocessed_message
WHERE state = @StateNew
I think the deadlock in the original sproc occurs if two instances acquire shared locks on the select before either can attempt to acquire the exclusive locks on the update.
Im not sure if noeld's version is dealock proof, so to be on the safe side I would add a UPDLOCK hint on the SELECT subquery.
Im pretty sure if ZPHillM had used UPDLOCK instead of HOLDLOCK in the original sproc, then there would be no deadlock issue.
April 1, 2005 at 8:07 am
Kenneth,
Thanks for your explanation.
The point with this 'backward' notation, is that you can update (the other columns) and assign values from columns not updated (ie the PK) in one go. ...
-ron
April 1, 2005 at 8:16 am
Renato,
I lean towards your idea of putting the UPDLOCK hint on the Select sub query just to play it safe.
This seems like it would be difficult to test and although the documentation suggests it will work without a deadlock I have yet to see any explicit documentation on this type of update (i.e. Update with a SELECT sub query).
-ron
April 1, 2005 at 10:50 am
Renato said:
I think the deadlock in the original sproc occurs if two instances acquire shared locks on the select before either can attempt to acquire the exclusive locks on the update.
Im not sure if noeld's version is dealock proof, so to be on the safe side I would add a UPDLOCK hint on the SELECT subquery.
Im pretty sure if ZPHillM had used UPDLOCK instead of HOLDLOCK in the original sproc, then there would be no deadlock issue.
I have tested the situation shown in the original proc and Renato is correct. Using UPDLOCK (instead of HOLDLOCK) avoids the deadlock by acquiring an exclusive lock during the initial SELECT.
--ron
Viewing 13 posts - 1 through 12 (of 12 total)
You must be logged in to reply to this topic. Login to reply