You should make sure your SP is inside a transaction, otherwise the query that checks whether the card has been used could be executed by two separate threads before the update.
This will protect your update of the UserBalance
so it can only be updated once per card and also if anything fails after your CardsList
update but before your UserBalance
update, it will be rolled back.
e.g. imagine the following sequence of events with the same card
Request 1 Request 2
Check card 123 has not been used
Card has not been used
Check card 123 has not been used
Card has not been used
Update card to mark as used
Update card to mark as used
Update balance
Update balance
As you can see, depending on the order of events that happen from concurrent requests, the update could happen multiple times for the same card.
A transaction will lock the database tables until the transaction is committed, so the sequence of events will now be:
Request 1 Request 2
BEGIN TRANSACTION
Check card 123 has not been used
Card has not been used
BEGIN TRANSACTION
Check card 123 has not been used - SQL
Server will
suspend (pause) this thread as this
record
has been
locked by the other thread
Update card to mark as used
Update balance
COMMIT TRANSACTION Thread resumed
Check card 123 has not been used -
it has so
RAISERROR
TRANSACTION aborted (rolled back, but as no
changes were made it is simply marked as
complete)
Also Used
should also not be a parameter, just a variable within the SP.
ALTER procedure [dbo].[UseCard]
(@CardNumber varchar (10) , @UserId bigint)
AS
DECLARE @Used bit;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
if (@CardNumber NOT in (SELECT CardNumber FROM CardsList)) and
RAISERROR('Card not found',16,1)
else if (@Used in (select Used from CardsList WHERE CardNumber = @CardNumber))
RAISERROR('Card used before',16,1)
else
begin
Update CardsList
set
Used = 1 ,UserId = @UserId
where (CardNumber = @CardNumber)
UPDATE [Users]
SET
UserBalance = UserBalance + (select Amount from CardsList where CardNumber = @CardNumber)
WHERE (ID = @UserId)
end
COMMIT TRANSACTION;