SQL Server Race Condition Question
Set the Transaction Isolation Level to Serializable.
At lower isolation levels, other transactions can read the data in a row that is read, (but not yet modified) in this transaction. So two transactions can indeed read the same value. At very low isolation (Read Uncommitted) other transactions can even read data after it's been modified (but before committed)...
Review details about SQL Server Isolation Levels here
So bottom line is that the Isolation level is crtitical piece here to control what level of access other transactions get into this one.
NOTE. From the link, about Serializable
Statements cannot read data that has been modified but not yet committed by other transactions.
This is because the locks are placed when the row is modified, not when the Begin Trans
occurs, So what you have done may still allow another transaction to read the old value until the point where you modify it. So I would change the logic to modify it in the same statement as you read it, thereby putting the lock on it at the same time.
begin tran
declare @x int
update def set @x= nextcode, nextcode += 1
waitfor delay '00:00:15'
select @x
commit tran
Preventing Conditional INSERT/UPDATE Race Condition in MS-SQL
As alluded to in the articles I posted to your last question (Conditional INSERT/UPDATE Race Condition and “UPSERT” Race Condition With MERGE) using MERGE
along with HOLDLOCK
is thread safe, so your query would be:
MERGE tblWords WITH (HOLDLOCK) AS w
USING (VALUES (@cl_Word, @cl_WordLangCode, @cl_SourceId)) AS s (cl_Word, cl_WordLangCode, cl_SourceId)
ON s.cl_Word = w.cl_Word
AND s.cl_WordLangCode = w.cl_WordLangCode
WHEN NOT MATCHED THEN
INSERT (cl_Word, cl_WordLangCode, cl_SourceId)
VALUES (s.cl_Word, s.cl_WordLangCode, s.cl_SourceId);
It also looks like this might be a stored procedure and you are using SELECT @cl_WordId
to return the ID to the caller. This falls under one of Aaron Bertrand's bad habits to kick, instead you should use an output parameter, something like:
CREATE PROCEDURE dbo.SaveCLWord
@cl_Word VARCHAR(255),
@cl_WordLangCode VARCHAR(255),
@cl_SourceId INT,
@cl_WordId INT OUTPUT
AS
BEGIN
MERGE tblWords WITH (HOLDLOCK) AS w
USING (VALUES (@cl_Word, @cl_WordLangCode, @cl_SourceId)) AS s (cl_Word, cl_WordLangCode, cl_SourceId)
ON s.cl_Word = w.cl_Word
AND s.cl_WordLangCode = w.cl_WordLangCode
WHEN NOT MATCHED THEN
INSERT (cl_Word, cl_WordLangCode, cl_SourceId)
VALUES (s.cl_Word, s.cl_WordLangCode, s.cl_SourceId);
SELECT @cl_WordId = w.cl_WordId
FROM tblWords AS w
WHERE s.cl_Word = @cl_Word
AND s.cl_WordLangCode = @cl_WordLangCode;
END
ADDEDNUM
You can do this without MERGE
as follows.
BEGIN TRAN
INSERT tblWords (cl_Word, cl_WordLangCode, cl_SourceId)
SELECT @cl_Word, @cl_WordLangCode, @cl_SourceId
WHERE NOT EXISTS
( SELECT 1
FROM tblWords WITH (UPDLOCK, HOLDLOCK)
WHERE cl_Word = @cl_Word
AND l_WordLangCode = @cl_WordLangCode
);
COMMIT TRAN;
SELECT @cl_WordId = w.cl_WordId
FROM tblWords AS w
WHERE s.cl_Word = @cl_Word
AND s.cl_WordLangCode = @cl_WordLangCode;
If you are not using merge because you are concerned about its bugs, or because in this case you don't actually do an UPDATE
, so MERGE
is overkill and an INSERT
will suffice, then that is fair enough. But not using it because it is unfamiliar syntax is not the best reason, take the time to read about it, learn more, and add another string to your SQL bow.
EDIT
From online docs
HOLDLOCK
Is equivalent to SERIALIZABLE. For more information, see SERIALIZABLE later in this topic. HOLDLOCK applies only to the table or view for which it is specified and only for the duration of the transaction defined by the statement that it is used in. HOLDLOCK cannot be used in a SELECT statement that includes the FOR BROWSE option.
So in your query, you have 6 statements:
-- STATETMENT 1
DECLARE @cl_WordId bigint = NULL
--STATEMENT 2
SELECT
@cl_WordId = cl_WordId
FROM tblWords WITH (HOLDLOCK)
WHERE cl_Word = @cl_Word
AND cl_WordLangCode = @cl_WordLangCode
BEGIN
--STATEMENT 3
IF (@cl_WordId IS NULL)
BEGIN
-- STATEMENT 4
INSERT INTO tblWords (cl_Word, cl_WordLangCode, cl_SourceId)
VALUES (@cl_Word, @cl_WordLangCode, @cl_SourceId)
SET @cl_WordId = SCOPE_IDENTITY()
--STATEMENT 5
SELECT
@cl_WordId
END
ELSE
BEGIN
-- STATEMENT 6
SELECT
@cl_WordId
END
END
Since you don't have explicit transactions, each statement runs within its own implicit transaction, so concentrating on statement 2, this is equivalent to:
BEGIN TRAN
SELECT
@cl_WordId = cl_WordId
FROM tblWords WITH (HOLDLOCK)
WHERE cl_Word = @cl_Word
AND cl_WordLangCode = @cl_WordLangCode
COMMIT TRAN
Therefore, since HOLDLOCK
applies for the duration of the transaction in which it is used, the lock is released, the lock is released as soon as this code finishes, so by the time you have progressed to statement 3 and 4 another thread could have inserted to the table.
How to deal with race condition in case when it's possible to have multiple servers (and each of them can have multiple threads)
After reading the majority of the comments let's assume that you need a solution for a relational database.
The main thing that you need to guarantee is that the write operation at the end of your code only happens if the precondition is still valid (e.g. product.Quantity - requestedQuantity
).
This precondition is evaluated at the application side in memory. But the application only sees a snapshot of the data at the moment, when database read happened: _database.GetProduct();
This might become obsolete as soon as someone else is updating the same data. If you want to avoid using SERIALIZABLE
as a transaction isolation level (which has performance implications anyway), the application should detect at the moment of writing if the precondition is still valid. Or said differently, if the data is unchanged while it was working on it.
This can be done by using offline concurrency patterns: Either an optimistic offline lock or a pessimistic offline lock. Many ORM frameworks support these features by default.
SQL Server Process Queue Race Condition
Edit:
I googled to check my answer: "Processing Data Queues in SQL Server with READPAST and UPDLOCK". It's been years since I read about and played with this solution.
Original:
If you use the READPAST hint, then locked rows are skipped. You've used ROWLOCK so you should avoid lock escalation. You also need UPDLOCK, as I found out.
So process 1 locks 20 rows, process 2 will take the next 20, process 3 takes rows 41 to 60, etc
The update can also be written like this:
UPDATE TOP (20)
foo
SET
ProcessorID = @PROCID
FROM
OrderTable foo WITH (ROWLOCK, READPAST, UPDLOCK)
WHERE
ProcessorID = 0
Refresh, Oct 2011
This can be done more elegantly with the OUTPUT clause if you need a SELECT and an UPDATE in one go.
SQL Server update - is there a race condition?
If my_table_id
is the primary key of your table (i.e. the primary key constraint is set), then indeed it is impossible that you would have 2 rows affected by that update statement.
Through locking, the database engine ensures that a row is updated by one process at a time. At no moment in time will there be two records with the same value for my_table_id
, since the primary key constraint forbids such a situation.
Secondly, as you update the flag pending_flag
to 1, the record will not match if you run the same update
statement again, and so 0 records will have been updated.
can I use this command alone as a mechanism to guarantee that I don't process the record multiple times?
Yes! (with condition that you have my_table_id
as the primary key or at least with a unique constraint).
Related Topics
Using Bcp Utility to Export SQL Queries to a Text File
How to Restore SQL Server 2008 Backup in SQL Server 2005
Default Value of Guid in for a Column in MySQL
Identity_Insert Is Already on for Table 'X'. Cannot Perform Set Operation for Table 'Y'
Why Can't I Group by 1 When It's Ok to Order by 1
Group or Distinct After Join Returns Duplicates
Pass Extra Parameter to Postgresql Aggregate Final Function
Postgres Drop Table Syntax Error
How to Use the Results of a Stored Procedure from Within Another
How to Find Duplicate Consecutive Values in This Table
Select Average from MySQL Table with Limit
Use Google Bigquery to Build Histogram Graph
Insert Data from One Server to Another
Update Columns with Null Values
How to Deep Copy a Set of Data, and Change Fk References to Point to All the Copies
Does the Order of Tables in a Join Matter, When Left (Outer) Joins Are Used