I have a transaction where
- Transaction start
- I run an update on a row with a given id that is marked currently active and mark it inactive
- Add a new row with a audit_id from the above updated row
- Commit the transaction.
However, when two concurrent transactions ran, ended up with a scenario where #2 happened twice on the same row and #3 got executed twice resulting in two extra rows being inserted instead of just 1.
TLDR; No two rows should have same audit ids. If transactions ran as intended, row 3 should have never appeared.
id x_id is_active audit_id. status
1. 9 false. null inprogress
2. 9 true 1 done
3. 9 true 1 done
How do I avoid this?
I was looking at ROWLOCKS, UPDATELOCKS and HOLDLOCKs
For the above scenario, looks like I need a HOLDLOCK..
- Transaction begin
- Select id from tableA where x_id = 1 and is_active = true and status = inprogress with (HOLDLOCK)
- Update is_active = false where id = (id from #2)
- Insert into tableA x_id=1, is_active = true, audit_id = (id from #2), status = done
- Commit transaction
Is the above right? According to what I understand, if I use UPDLOCK, say in another concurrent transaction #2 was already executed, both transactions will end up executing #3, not at once but one after the other in overlapping transactions and I'll end up with two rows having same audit_id and status = done.
4条答案
按热度按时间xeufq47z1#
With READ COMMITTED SNAPSHOT on you need to opt-in to a lock when reading. Otherwise you'll simply read the "last-known-good" version of the row.
So use UPDLOCK, and add HOLDLOCK if you also need to lock empty ranges (ie where the row doesn't exist and you intend to insert it).
EG
idfiyjo82#
I wrote a test and looks like UPDLOCK does the job! Thank you everyone for the inputs.
In query tab 1
In query tab2
Result
To reproduce the problem in question, Remove
with(updlock)
from both selects and follow the steps mentioned above.Problematic result:
References:
lymgl2op3#
Azure works differently than "default mode" SQL Server here. Because Snapshot Isolation is normally on, reads will not block during an open write transaction even at the "READ COMMITTED" isolation level -- Azure will "take a snapshot" of the old data instead. If you want to avoid this behavior, you can either turn off snapshot isolation ("READ_COMMITTED_SNAPSHOT OFF"), upgrade to an update lock (as per the other answer), or set the transaction isolation level to serializable just for this one transaction.
However, the better solution is to avoid this contortion entirely. To do this without a multi-statement explicit transaction, use the "OUTPUT" clause on the update (which is atomic) to return the Audit ID you wish to insert. If your INSERT contains the where clause "..WHERE Active=1", it can't go wrong. An example of using OUTPUT to audit rows in atomic fashion like this can be found at:
SQLServerCentral: The Output Clause
xfyts7mz4#
For step #2 UPDLOCK does not work for SELECT statements.
Correction
: It acctually does work when both SELECTs have UPDLOCK.You could use an exclusive lock, so that other SELECTs (without any hint) will wait for the transaction to finish:
Note: On Azure SQL the default isolation level (READ COMMITTED) behaves differently than on SQL Server, and does not wait on reads even when the lock is exclusive. On Azure SQL to wait in SELECTs on locked records, you should set SET TRANSACTION ISOLATION LEVEL REPEATABLE READ or SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
It would be much simpler if you could avoid SELECT before UPDATE with something like this: