I have the following scenario: a bank context, where I do not want a particular customer to complete a withdraw from his account, unless a previous withdraw is not committed.
In order to achieve this, I have created the following table
CREATE TABLE [dbo].[Locks](
[CustomerID] [int] NOT NULL,
CONSTRAINT [PK_Locks] PRIMARY KEY CLUSTERED
(
[CustomerID] ASC
)
Now, whenever a withdraw starts, I have the following code that essentially inserts the customer in this helper table (if he does not exist), and then locks the row for the duration of the transaction so that other withdraw do not occur before the first transaction is committed
BEGIN TRAN
IF NOT EXISTS (SELECT * FROM Locks WHERE CustomerID=@customerId) --if customerid does not exist, insert the row
BEGIN
INSERT INTO Locks (CustomerID)
VALUES (@customerId)
END
SELECT CustomerID FROM Locks WITH (HOLDLOCK XLOCK ROWLOCK) WHERE CustomerID=@customerId --lock on row
--(check if customer has enough balance, then perform withdraw from customer account)
COMMIT
The above code seems to work in general with thousands of withdraws per day, but once every week or so, I do get a case where the lock did not work and the customer was found with negative balance because two withdraw operations happened at the same time.
Any ideas under which conditions the HOLDLOCK XLOCK ROWLOCK could fail to lock the transaction?