views:

8981

answers:

7

This one will take some explaining. What I've done is create a specific custom message queue in SQL Server 2005. I have a table with messages that contain timestamps for both acknowledgment and completion. The stored procedure that callers execute to obtain the next message in their queue also acknowledges the message. So far so good. Well, if the system is experiencing a massive amount of transactions (thousands per minute), isn't it possible for a message to be acknowledged by another execution of the stored procedure while another is prepared to so itself? Let me help by showing my SQL code in the stored proc:

--Grab the next message id
declare @MessageId uniqueidentifier
set @MessageId = (select top(1) ActionMessageId from UnacknowledgedDemands);

--Acknowledge the message
update ActionMessages
set AcknowledgedTime = getdate()
where ActionMessageId = @MessageId

--Select the entire message
...
...

In the above code, couldn't another stored procedure running at the same time obtain the same id and attempt to acknowledge it at the same time? Could I (or should I) implement some sort of locking to prevent another stored proc from acknowledging messages that another stored proc is querying?

Wow, did any of this even make sense? It's a bit difficult to put to words...

A: 

You want to wrap your code in a transaction, then SQL server will handle locking the appropriate rows or tables.

begin transaction

--Grab the next message id
declare @MessageId uniqueidentifier
set @MessageId = (select top(1) ActionMessageId from UnacknowledgedDemands);

--Acknowledge the message
update ActionMessages
set AcknowledgedTime = getdate()
where ActionMessageId = @MessageId

commit transaction

--Select the entire message
...
Blorgbeard
A: 

@Blorgbeard:

Are you sure that transactions lock tables being queried even before you issue insert or update commands on that table? I would think performance would be terrible if that were the case.

Kilhoffer
A: 

Should you really be processing things one-by-one? Shouldn't you just have SQL Server acknowledge all unacknowledged messages with todays date and return them? (all also in a transaction of course)

rpetrich
Isnt that what I'm doing? SQL Server is acknowledging the messages on behalf of the caller and returning them.
Kilhoffer
No I meant something like this (untested):BEGIN TRANSACTIONSELECT * FROM UnacknowledgedDemands;UPDATE ActionMessagesSET AcknowledgetTime = getdate()WHERE EXISTS (SELECT * FROM UnacknowledgedDemands WHERE ActionMessages.ActionMessageId = UnacknowledgedDemands.ActionMessageId)COMMIT TRANSACTION
rpetrich
A: 

@Kilhoffer:

The whole SQL batch is parsed before execution, so SQL knows that you're going to do an update to the table as well as select from it.

Edit: Also, SQL will not necessarily lock the whole table - it could just lock the necessary rows. See here for an overview of locking in SQL server.

Blorgbeard
I disagree with that. Parsing doesn't have anything to do with it. If it did, SQL run via EXECUTE would not be transaction-safe.
Euro Micelli
But there's no EXECUTE here. Good point though - how does SQL know what to lock when running arbitrary SQL via execute?
Blorgbeard
+2  A: 

Something like this

--Grab the next message id
begin tran
declare @MessageId uniqueidentifier
select top 1 @MessageId =   ActionMessageId from UnacknowledgedDemands with(holdlock, updlock);

--Acknowledge the message
update ActionMessages
set AcknowledgedTime = getdate()
where ActionMessageId = @MessageId

-- some error checking
commit tran

--Select the entire message
...
...
SQLMenace
A: 

Read more about SQL Server Select Locking here and here. SQL Server has the ability to invoke a table lock on a select. Nothing will happen to the table during the transaction. When the transaction completes, any inserts or updates will then resolve themselves.

Jarrett Meyer
So is this to say that simply wrapping the above code in a transaction, SQL Server does in fact implement select locking as described in the above links?
Kilhoffer
+1  A: 

Instead of explicit locking, which is often escalated by SQL Server to higher granularity than desired, why not just try this approach:

declare @MessageId uniqueidentifier
select top 1 @MessageId = ActionMessageId from UnacknowledgedDemands

update ActionMessages
  set AcknowledgedTime = getdate()
  where ActionMessageId = @MessageId and AcknowledgedTime is null

if @@rowcount > 0
  /* acknoweldge succeeded */
else
  /* concurrent query acknowledged message before us,
     go back and try another one */

The less you lock - the higher concurrency you have.

Constantin