In SQL Server, what is the lock acquisition order? How is it decided which transaction is run first?
I know that this is a wide topic, so link to relevant material would be useful.
My experiment (code below) shows, that transaction-2 (lock all rows), waits also for transaction-3, which started later.
Originally, I thought, that there is some kind of FIFO principal for which transactions gets to run.
But now it seems that transactions locking many rows, could wait for very very long time.
(or maybe page/table locks act differently)
-- I ran 3 transactions in parallel
-- transaction1 - lock ONE row
BEGIN TRAN PRINT SYSUTCDATETIME();
SELECT * FROM mytable WITH (UPDLOCK, ROWLOCK) WHERE id = 58;
PRINT SYSUTCDATETIME(); WAITFOR DELAY '00:00:05'; PRINT SYSUTCDATETIME();
COMMIT TRAN
-- transaction2 - lock ALL rows
BEGIN TRAN
PRINT SYSUTCDATETIME();
SELECT * FROM mytable WITH (UPDLOCK, ROWLOCK)
PRINT SYSUTCDATETIME(); WAITFOR DELAY '00:00:05'; PRINT SYSUTCDATETIME();
COMMIT TRAN
-- transaction3 - lock ONE row
BEGIN TRAN
PRINT SYSUTCDATETIME();
SELECT * FROM mytable WITH (UPDLOCK, ROWLOCK) WHERE id = 201;
PRINT SYSUTCDATETIME(); WAITFOR DELAY '00:00:05'; PRINT SYSUTCDATETIME();
COMMIT TRAN
-- Results were:
-- transaction1
2022-04-22 13:16:49.0830498
2022-04-22 13:16:49.0830498
2022-04-22 13:16:54.0941580
-- transaction2
2022-04-22 13:16:49.6759131
2022-04-22 13:16:55.5587416
2022-04-22 13:17:00.5708426
-- transaction3
2022-04-22 13:16:50.5450067
2022-04-22 13:16:50.5450067
2022-04-22 13:16:55.5587416
Transaction 2 did not lock all rows; only rows that were touched before the query was blocked were locked.
Note the Transaction 2 scan was blocked by transaction1 when it reached key 58. Keys higher than 58 were not yet accessed by the scan so there was no conflict when transaction 3 read key 201.
This explanation assumes id is an incremental identity column primary key.
Related
I'm working in SQL Server 2016. I have two tables, one is a queue of work items (TQueue), and the second is the work items (TWork) that are being processed.
I have a script that grabs the top 100 items from TQueue that do not have a record in TWork, and then inserts those items into TWork to be processed.
For performance reasons, I want to run multiple instances of the script simultaneously. The challenge is that Script 1 grabs 100 items, and before the transaction to insert these items into TWork is committed, Script 2 grabs the same set of items and inserts them as well.
Question
I would like to block the reading of TQueue until insert transaction into TWork has completed. Is this possible?
You may use table hints to achieve this goal.
For example:
Create Table Val (ID Int)
Insert Into Val (ID)
Values (0),(1),(2),(3),(4),(5)
First session:
Set Transaction Isolation level Read Committed
Begin Transaction
Select Top 2 * From Val With (ReadPast, XLock, RowLock)
-- Return 0,1
-- Commit has been commented for illustrative purposes.
-- Don't forget to commit the transaction later.
-- Commit
Second session:
Set Transaction Isolation level Read Committed
Begin Transaction
Select * From Val With (ReadPast, XLock, RowLock)
-- Return 2,3,4,5
Commit
Recently I have gone through with Hints and Locks in SQL server. While google about this topic I have read one blog where some query have been written which I am not bale to understand. Here it is
BOL states: Use update locks instead of shared locks while reading a table, and hold locks until the end of the statement or transaction. I have some trouble translating this. Does this mean that the update locks are released after the execution of the SELECT statement, unless the SELECT statement in within a transaction?
I other words, are my assumptions in the following 2 scenario's correct?
Scenario 1: no transaction
SELECT something FROM table WITH (UPDLOCK)
/* update locks released */
Scenario 2: with transaction
BEGIN TRANSACTION
SELECT something FROM table WITH (UPDLOCK)
/* some code, including an UPDATE */
COMMIT TRANSACTION
/* update locks released */
Example for scenario 2 (referred for stackoverflow blog)
BEGIN TRAN
SELECT Id FROM Table1 WITH (UPDLOCK)
WHERE AlertDate IS NULL;
UPDATE Table1 SET AlertDate = getutcdate()
WHERE AlertDate IS NULL;
COMMIT TRAN
Please help to understand the above query.
My second question is: once execution of select statement completed at same time UPDLOCK get released or not?
Your assumption in scenario 2 is correct.
To answer your second question, no. The Update locks are held on the selected row(s) until the transaction ends, or until converted to exclusive locks when the update statement modifies those row(s). Step through each statement one at a time using SSMS to verify.
BEGIN TRAN
-- execute sp_lock in second session - no locks yet
SELECT Id FROM Table1 WITH (UPDLOCK) WHERE AlertDate IS NULL;
-- execute sp_lock in second session - update locks present
UPDATE Table1 SET AlertDate = getutcdate() WHERE AlertDate IS NULL;
-- execute sp_lock in second session - update (U) locks are replace by exclusive locks (X) for all row(s) returned by SELECT and modified by the UPDATE (Lock Conversion).
-- Update locks (U) continue to be held for any row(s) returned by the SELECT but not modified by the UPDATE
-- exclusive locks (X) are also held on all rows not returned by SELECT but modified by UPDATE. Internally, lock conversion still occurs, because UPDATE statements must read and write.
COMMIT TRAN
-- sp_lock in second session - all locks gone.
As for what is going on in scenario 1, all T-SQL statements exist either in an implicit or explicit transaction. Senario 1 is implicitly:
BEGIN TRAN
SELECT something FROM table WITH (UPDLOCK)
-- execute sp_lock in second session - update locks (U) will be present
COMMIT TRAN;
-- execute sp_lock in second session - update locks are gone.
Does this mean that the update locks are released after the execution of the SELECT statement, unless the SELECT statement in within a transaction?
The locks will be released as soon as the row is read..but the lock hold will be U lock,so any parallel transaction trying to modify this will have to wait
if you wrap above select in a transaction,locks will be released only when the transaction is committed,so any parallel transaction acquiring locks incompatible with U lock will have to wait
begin tran
select * from t1 with (updlock)
for the below second scenario
BEGIN TRANSACTION
SELECT something FROM table WITH (UPDLOCK)
/* some code, including an UPDATE */
COMMIT TRANSACTION
Imagine, if your select query returned 100 rows,all will use U lock and imagine the update in same transaction affects 2 rows,the two rows will be converted to x locks .so now your query will have 98 u locks and 2 xlocks until the transaction is committed
I would like to think Updlock as repeatable read,any new rows can be added,but any parallel transaction can't delete or update existing rows
I've got a project that is trying to apply DDD (Domain Driven Design). Currently, we've got something like this:
begin tran
try
_manager.CreateNewEmployee(newEmployeeCmd);
tran.Commit();
catch
rollback tran
Internally, the CreateNewEmployee method uses a domain service for checking if there's already an employee with the memberId. Here's some pseudo code:
void CreateNewEmployee(NewEmployeeCmd cmd)
if(_duplicateMember.AlreadyRegistered(cmd.MemberId) )
throw duplicate
// extra stuff
saveNewEmployee()
end
Now, in the end, it's as if we have the following SQL instructions executed (pesudo code again):
begin sql tran
select count(*) from table where memberId=#memberId and status=1 -- active
--some time goes by
insert into table ...
end
NOw, when I started looking at the code, I've noticed that it was using the default SQL Server locking level. In practice, that means that something like this could happen:
--thread 1
(1)select ... --assume it returns 0
--thread 2
(2)select ... ---nothing found
(3)insert recordA
--thread 1
(4)insert record --some as before
(5) commit tran
--thread 1
(6) commit tran
So, we could end up having repeated records. I've tried playing with the transaction levels, but the only way I've managed to make it work like it's intended was by changing the select that is used to check if there's already a record in the table. I've ended up using a table lock hint which instructs sql to maintain a lock until the end of the transaction. That was the only way I've managed to get a lock when the select starts (changing the other isolation levels still wouldn't do what I needed since they all allowed the select to run)
So, I've ended up using a table lock which is held from the beginning until the end of the transaction. In practice, that means that step (2) will block until thread 1 ends its job.
Is there a better option for this kind of scenarios (that don't depend on using, say, indexes)?
Thanks.
Luis
You need to get the proper locks on the initial select, which you can do with the locking hints with (updlock, serializable). Once you do that, thread 2 will wait for thread 1 to finish if thread 2 is using the same key range in its where.
You could use the Sam Saffron upsert approach.
For example:
create procedure dbo.Employee_getset_byName (#Name nvarchar(50), #MemberId int output) as
begin
set nocount, xact_abort on;
begin tran;
select #MemberId = Id
from dbo.Employee with (updlock, serializable) /* hold key range for #Name */
where Name = #Name;
if ##rowcount = 0 /* if we still do not have an Id for #Name */
begin;
/* for a sequence */
set #MemberId = next value for dbo.IdSequence; /* get next sequence value */
insert into dbo.Employee (Name, Id)
values (#Name, #MemberId);
/* for identity */
insert into dbo.Employee (Name)
values (#Name);
set #MemberId = scope_identity();
end;
commit tran;
end;
go
I have two test transactions in two session respectively. Assuming these two transaction will run simultaneously. What I attempt to do is to let one transaction insert invoice number correctly after the other transaction is done. No duplicate. I did it as below. But if I remove with (tablockx) in session 2, they won't work any more. I checked on line book but no answer. Anybody would help? Serializable won't work since two SELECT want to be exclusive to each other here. Thanks.
In session 1:
begin transaction
declare #i int
select #i=MAX(InvNumber) from Invoice
with(tablockx)
where LocName='A'
waitfor delay '00:00:10'
set #i=#i+1
insert into Invoice values('A',#i);
commit
In session 2:
begin transaction
declare #i int
select #i=MAX(InvNumber) from Invoice
with(tablockx)
where LocName='A'
set #i=#i+1
insert into Invoice values('A',#i);
commit
That will work but also completely block all other access to the table.
You can potentially lock at a lower granularity (than table) and mode (than exclusive) if you do WITH(UPDLOCK, HOLDLOCK).
HOLDLOCK gives serializable semantics so can just lock the range at the top of the index (if you have one on LocName,InvNumber).
UPDLOCK ensures two concurrent transactions can't both hold the same lock but, unlike exclusive, doesn't block other (normal) readers that aren't using the hint.
BEGIN TRANSACTION
DECLARE #i INT
SELECT #i = MAX(InvNumber)
FROM Invoice WITH(UPDLOCK, HOLDLOCK)
WHERE LocName = 'A'
WAITFOR delay '00:00:10'
SET #i=#i + 1
INSERT INTO Invoice
VALUES ('A',
#i);
COMMIT
Alternatively you could just use sp_getapplock to serialize access to that code.
I put transactions in all my "set" procedures. No problems. Everything works.
In this case, I need one set procedure, to call another, thankfully, only once, or that would potentially complicate things further.
So the happy bath would be.
I'm in ProcA and start a transaction.
It calls ProcB and it starts a transaction.
ProcB is successful and commits.
ProcA is successful and commits.
However, what happens if ProcB fails, rollsback, and rethrows the error. It should cause ProcA to rollback as well correct?
What if ProcB succeeds, commits, then ProcA subsequently fails, and rollsback...will what happened in ProcB be rolled back? or is it commited?
I need these two to work together, either both succeed, or fail and both be rolled back. What's the best way to ensure this happens?
I'm working with Microsoft SQL Server 2008 R2 (SP1)
Note: If ProcB requires a transaction because it can be called without ProcA wrapping it. And technically, ProcA won't always call ProcB (depends on input).
Here's a simple demo to show what happens with nested transations:
CREATE TABLE TranTest (Field1 INTEGER)
BEGIN TRANSACTION
SELECT ##TRANCOUNT -- 1 open transaction
INSERT TranTest VALUES (1)
BEGIN TRANSACTION
SELECT ##TRANCOUNT -- 2 open transactions
INSERT TranTest VALUES (2)
ROLLBACK TRANSACTION -- this rolls back ALL transaction
SELECT ##TRANCOUNT -- 0 open transactions (you may have expected 1?)
SELECT * FROM TranTest -- No rows
Instead f the ROLLBACK above, if you did a COMMIT TRANSACTION, this actual does nothing other then decrement ##TRANCOUNT. So you then would need to to either COMMIT the outer transaction (which would COMMIT both rows to the table), or do a ROLLBACK which would result in no rows being committed to the table.
Here's the MSDN ref on nested transactions: http://msdn.microsoft.com/en-us/library/ms189336.aspx
Just use XACT_ABORT ON, and you are all set. Run the following script and see for yourself:
CREATE DATABASE ak_test;
GO
USE ak_test;
GO
CREATE TABLE dbo.a(i INT CONSTRAINT a_CannotInsertNegative CHECK(i>=0));
GO
CREATE TABLE dbo.b(i INT CONSTRAINT b_CannotInsertNegative CHECK(i>=0));
GO
CREATE PROCEDURE dbo.innerProc #i INT
AS
SET XACT_ABORT ON ;
BEGIN TRAN
INSERT b(i)VALUES(#i);
COMMIT;
GO
CREATE PROCEDURE dbo.outerProc #i1 INT, #i2 INT, #i3 INT
AS
SET XACT_ABORT ON ;
BEGIN TRAN
INSERT a(i)VALUES(#i1);
EXEC innerProc #i=#i2;
INSERT a(i)VALUES(#i3);
COMMIT;
GO
-- succeeds
EXEC dbo.outerProc 1, 2, 3;
SELECT * FROM dbo.a;
SELECT * FROM dbo.b;
GO
-- inner proc fails
EXEC dbo.outerProc 2, -3, 4;
GO
SELECT * FROM dbo.a;
SELECT * FROM dbo.b;
GO
-- second insert in outer proc fails
EXEC dbo.outerProc 3, 4, -5;
GO
SELECT * FROM dbo.a;
SELECT * FROM dbo.b;
I'm paranoid about transactions (there was this transaction left open on Production once that no one noticed for half an hour...) so I'd warp the potentially inner transaction like so:
CREATE PROCEDURE etcetc
...
DECLARE #IsTransaction bit = 0
IF ##trancount > 0
BEGIN
BEGIN TRANSACTION
SET #IsTransaction = 1
END
...
IF #IsTransaction = 1
BEGIN
COMMIT
-- or ROLLBACk, as necessary
END
All transaction processing (and handling of errors that occur within the transaction) must then be dealt with at whatever level launched the transaction.
(And did anyone else notice how BOL doesn't actually say what happens when you issue a ROLLBACK to a named transaction that isn't the outermost transaction? They do spell out every other permutation...)