Lets build up a solution:
Ensure the UPDATE checks the @@ROWCOUNT
Inspect @@ROWCOUNT after the UPDATE
to determine which Worker process wins.
CREATE PROCEDURE [dbo].[GetNextJob]
AS
BEGIN
SET NOCOUNT ON;
DECLARE @jobId INT
SELECT TOP 1 @jobId = Jobs.JobId FROM Jobs
WHERE Jobs.JobStatus = 1
ORDER BY JobId ASC
UPDATE Jobs Set JobStatus = 2
WHERE JobId = @jobId
AND JobStatus = 1;
IF (@@ROWCOUNT = 1)
BEGIN
SELECT @jobId;
END
END
GO
Note that with the above procedure the process that does not win does not return any rows and needs to call the procedure again to get the next row.
The above will fix most of all the cases where both Workers pick up the same piece of work because the UPDATE
guards against this. However, it's possible for @@ROWCOUNT to be 1 for both workers for the same jobId!
Lock the row within a transaction so only 1 Worker can update the Status
CREATE PROCEDURE [dbo].[GetNextJob]
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRANSACTION
DECLARE @jobId INT
SELECT TOP 1 @jobId = Jobs.JobId FROM Jobs WITH (UPDLOCK, ROWLOCK)
WHERE Jobs.JobStatus = 1
ORDER BY JobId ASC
UPDATE Jobs Set JobStatus = 2
WHERE JobId = @jobId
AND JobStatus = 1;
IF (@@ROWCOUNT = 1)
BEGIN
SELECT @jobId;
END
COMMIT
END
GO
Both UPDLOCK and ROWLOCK are required. UPDLOCK on the SELECT tells MSSQL to lock the row as if it is being updated until the transaction is committed. The ROWLOCK (probably isn't necessary) but tells MSSQL to only lock the ROW returned by the SELECT.
Optimising the locking
When 1 process uses the ROWLOCK hint to lock a row, other processes are blocked waiting for that lock to be released. The READPAST hint can be specified. From MSDN:
When READPAST is specified, both
row-level and page-level locks are
skipped. That is, the Database Engine
skips past the rows or pages instead
of blocking the current transaction
until the locks are released.
This will stop the other processes from being blocked and improve performance.
CREATE PROCEDURE [dbo].[GetNextJob]
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRANSACTION
DECLARE @jobId INT
SELECT TOP 1 @jobId = Jobs.JobId FROM Jobs WITH (UPDLOCK, READPAST)
WHERE Jobs.JobStatus = 1
ORDER BY JobId ASC
UPDATE Jobs Set JobStatus = 2
WHERE JobId = @jobId
AND JobStatus = 1;
IF (@@ROWCOUNT = 1)
BEGIN
SELECT @jobId;
END
COMMIT
END
GO
To Consider: Combine SELECT and Update
Combine the SELECT and UPDATE and use a SET to get the ID out.
For example:
DECLARE @val int
UPDATE JobTable
SET @val = JobId,
status = 2
WHERE rowid = (SELECT min(JobId) FROM JobTable WHERE status = 1)
SELECT @val
This still requires the transaction to be SERIALIZABLE to ensure that each row is allocated to one Worker only.
To Consider: Combine SELECT and UPDATE again
Combine the SELECT and UPDATE and use the Output clause.