Tenho um banco de dados SQL Azure com as seguintes tabelas:
CREATE TABLE [dbo].[UserBalances]
(
[UserId] UNIQUEIDENTIFIER UNIQUE NOT NULL DEFAULT NEWID(),
[AvailableMoney] INT NOT NULL DEFAULT(0)
);
GO
CREATE UNIQUE CLUSTERED INDEX [UserBalancesIndex]
ON [dbo].[UserBalances]([UserId] ASC);
GO
CREATE TABLE [dbo].[UserBalanceChanges]
(
[EntryId] UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(),
[UserId] UNIQUEIDENTIFIER NOT NULL,
[PriorBalance] INT NOT NULL,
[NewBalance] INT NOT NULL,
[Time] DATETIME DEFAULT(GETUTCDATE())
)
GO
CREATE CLUSTERED INDEX UserBalanceChangesIndex
ON [UserBalanceChanges]( EntryId )
GO
e o seguinte código sendo executado simultaneamente por meio de várias conexões com o banco de dados:
CREATE PROCEDURE [dbo].[usp_ChangeUserBalance] @userId UNIQUEIDENTIFIER,
@change INT
AS
BEGIN TRANSACTION
DECLARE @priorBalance INT;
DECLARE @newBalance INT;
DECLARE @updateTime DATETIME;
UPDATE dbo.UserBalances
SET @updateTime = GETUTCDATE(),
@priorBalance = AvailableMoney,
@newBalance = AvailableMoney = AvailableMoney + @change
WHERE UserId = @userId;
INSERT INTO UserBalanceChanges
(UserId,
PriorBalance,
NewBalance,
Time)
VALUES ( @userId,
@priorBalance,
@newBalance,
@updateTime );
COMMIT TRANSACTION
RETURN 0
e então eu executo a seguinte consulta:
SELECT TOP(1000) PriorBalance,
NewBalance
FROM UserBalanceChanges
WHERE UserId = SomeSpecificId
ORDER BY Time DESC
e muitas vezes vejo algo assim:
1000 995
1005 1000 <<identical
1005 1000 <<changes
1010 1005
1015 1010
que parece que as duas atualizações em execução simultânea leem o mesmo valor inicial e, em seguida, uma atualização é efetivamente perdida.
Atualizações perdidas são uma anomalia conhecida, mas atualizações perdidas são impossíveis no SQL Server e presumivelmente no SQL Azure também (pelo menos com uma única instrução "SELECT from UPDATE" que eu tenho.
No entanto, parece que realmente vejo atualizações perdidas.
Por que observo atualizações perdidas "impossíveis"?
O problema foi devido à atualização perdida de "outra interpretação" - quando os dados são lidos pela primeira vez na memória local e, em seguida, a tabela é atualizada com esses dados. Havia outro código destinado a redefinir os saldos de alguns usuários muito raramente. Era algo assim:
e esse código seria executado simultaneamente com o código em questão. O código destinava-se a
INSERT from SELECT
apenas uma pequena fração dos saldos do usuário e apenas ocasionalmente, mas por causa de uma condição incorreta naSELECT
parte, selecionaria uma grande parte dos saldos e com muita frequência. Os saldos que foram selecionados sem querer tinhamNewBalance
igual aPriorBalance
e assim o finalINSERT from SELECT
não registraria as alterações.O
UPDATE
meio desse código forçaria a redefiniçãoAvailableMoney
de muitos usuários para um valor pré-computado e isso introduziria uma atualização perdida de "segunda interpretação" que é possível no SQL Server.