Problem utraconej aktualizacji występuje, gdy 2 współbieżne transakcje próbują odczytać i zaktualizować te same dane. Zrozummy to na przykładzie.
Załóżmy, że mamy tabelę o nazwie „Produkt”, która przechowuje identyfikator, nazwę i ItemsinStock dla produktu.
Jest używany jako część systemu online, który wyświetla liczbę produktów w magazynie dla danego produktu, dlatego musi być aktualizowany za każdym razem, gdy dokonywana jest sprzedaż tego produktu.
Tabela wygląda tak:
Identyfikator | Nazwa | Przedmioty w magazynie |
1 | Laptopy | 12 |
Rozważmy teraz scenariusz, w którym pojawia się użytkownik i rozpoczyna proces zakupu laptopa. To zainicjuje transakcję. Nazwijmy tę transakcję transakcją 1.
W tym samym czasie inny użytkownik loguje się do systemu i inicjuje transakcję, nazwijmy ją 2. Spójrz na poniższy rysunek.
Transakcja 1 odczytuje pozycje w magazynie dla laptopów, czyli 12. Nieco później transakcja 2 odczytuje wartość ItemsinStock dla laptopów, która w tym momencie nadal będzie wynosić 12. Transakcja 2 następnie sprzedaje trzy laptopy, na krótko przed transakcją 1 sprzedaje 2 przedmioty.
Transakcja 2 najpierw zakończy realizację i zaktualizuje ItemsinStock do 9, ponieważ sprzedano trzy z 12 laptopów. Transakcja 1 zobowiązuje się. Ponieważ transakcja 1 sprzedała dwa przedmioty, aktualizuje ItemsinStock do 10.
To jest niepoprawne, prawidłowa liczba to 12-3-2 =7
Przykład roboczy problemu z utraconą aktualizacją
Przyjrzyjmy się problemowi utraconej aktualizacji w działaniu w SQL Server. Jak zawsze, najpierw utworzymy tabelę i dodamy do niej kilka fikcyjnych danych.
Jak zawsze, upewnij się, że masz odpowiednią kopię zapasową, zanim zaczniesz grać z nowym kodem. Jeśli nie masz pewności, zapoznaj się z tym artykułem o kopii zapasowej SQL Server.
Wykonaj następujący skrypt na serwerze bazy danych.
<span style="font-size: 14px;">CREATE DATABASE pos;
USE pos;
CREATE TABLE products
(
Id INT PRIMARY KEY,
Name VARCHAR(50) NOT NULL,
ItemsinStock INT NOT NULL
)
INSERT into products
VALUES
(1, 'Laptop', 12),
(2, 'Iphon', 15),
(3, 'Tablets', 10)</span>
Teraz otwórz obok siebie dwie instancje SQL Server Management Studio. W każdym z tych przypadków przeprowadzimy jedną transakcję.
Dodaj następujący skrypt do pierwszej instancji SSMS.
<span style="font-size: 14px;">USE pos;
-- Transaction 1
BEGIN TRAN
DECLARE @ItemsInStock INT
SELECT @ItemsInStock = ItemsInStock
FROM products WHERE Id = 1
WaitFor Delay '00:00:12'
SET @ItemsInStock = @ItemsInStock - 2
UPDATE products SET ItemsinStock = @ItemsInStock
WHERE Id = 1
Print @ItemsInStock
Commit Transaction</span>
To jest skrypt dla transakcji 1. Tutaj rozpoczynamy transakcję i deklarujemy zmienną typu integer „@ItemsInStock”. Wartość tej zmiennej jest ustawiona na wartość kolumny ItemsinStock dla rekordu o identyfikatorze 1 z tabeli products. Następnie dodawane jest opóźnienie 12 sekund, tak aby transakcja 2 mogła zakończyć swoją realizację przed transakcją 1. Po opóźnieniu wartość zmiennej @ItemsInStock jest zmniejszana o 2, co oznacza sprzedaż 2 produktów.
Na koniec wartość kolumny ItemsinStock dla rekordu o identyfikatorze 1 jest aktualizowana wartością zmiennej @ItemsInStock. Następnie wyświetlamy na ekranie wartość zmiennej @ItemsInStock i zatwierdzamy transakcję.
W drugiej instancji SSMS dodajemy skrypt dla transakcji 2, który wygląda następująco:
<span style="font-size: 14px;">USE pos;
-- Transaction 2
BEGIN TRAN
DECLARE @ItemsInStock INT
SELECT @ItemsInStock = ItemsInStock
FROM products WHERE Id = 1
WaitFor Delay '00:00:3'
SET @ItemsInStock = @ItemsInStock - 3
UPDATE products SET ItemsinStock = @ItemsInStock
WHERE Id = 1
Print @ItemsInStock
Commit Transaction</span>
Skrypt transakcji 2 jest podobny do transakcji 1. Jednak tutaj, w transakcji 2, opóźnienie wynosi tylko trzy sekundy, a spadek wartości zmiennej @ItemsInStock wynosi trzy, ponieważ jest to sprzedaż trzech przedmiotów.
Teraz uruchom transakcję 1, a następnie transakcję 2. Zobaczysz, że transakcja 2 kończy swoją realizację jako pierwsza. A wartość wydrukowana dla zmiennej @ItemsInStock będzie wynosić 9. Po pewnym czasie transakcja 1 również zakończy swoje wykonanie, a wartość wydrukowana dla jej zmiennej @ItemsInStock będzie wynosić 10.
Obie te wartości są nieprawidłowe, rzeczywista wartość kolumny ItemsInStock dla produktu o identyfikatorze 1 powinna wynosić 7.
UWAGA:
Należy tutaj zauważyć, że problem z utraconą aktualizacją występuje tylko w przypadku odczytu zatwierdzonego i odczytu niezatwierdzonych poziomów izolacji transakcji. Przy wszystkich innych poziomach izolacji transakcji ten problem nie występuje.
Odczytaj poziom izolacji transakcji powtarzalnych
Zaktualizujmy poziom izolacji dla obu transakcji, aby odczytać powtarzalne i zobaczmy, czy wystąpił problem z utraconą aktualizacją. Ale wcześniej wykonaj następującą instrukcję, aby zaktualizować wartość ItemsInStock z powrotem do 12.
Update products SET ItemsinStock = 12
Skrypt transakcji 1
<span style="font-size: 14px;">USE pos;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
-- Transaction 1
BEGIN TRAN
DECLARE @ItemsInStock INT
SELECT @ItemsInStock = ItemsInStock
FROM products WHERE Id = 1
WaitFor Delay '00:00:12'
SET @ItemsInStock = @ItemsInStock - 2
UPDATE products SET ItemsinStock = @ItemsInStock
WHERE Id = 1
Print @ItemsInStock
Commit Transaction</span>
Skrypt transakcji 2
<span style="font-size: 14px;">USE pos;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
-- Transaction 2
BEGIN TRAN
DECLARE @ItemsInStock INT
SELECT @ItemsInStock = ItemsInStock
FROM products WHERE Id = 1
WaitFor Delay '00:00:3'
SET @ItemsInStock = @ItemsInStock - 3
UPDATE products SET ItemsinStock = @ItemsInStock
WHERE Id = 1
Print @ItemsInStock
Commit Transaction</span>
Tutaj w obu transakcjach ustawiliśmy poziom izolacji na odczyt powtarzalny.
Teraz uruchom transakcję 1, a następnie natychmiast uruchom transakcję 2. W przeciwieństwie do poprzedniego przypadku, transakcja 2 będzie musiała poczekać, aż transakcja 1 zostanie zatwierdzona. Następnie pojawia się następujący błąd dla transakcji 2:
Msg 1205, poziom 13, stan 51, wiersz 15
Transakcja (identyfikator procesu 55) została zablokowana w zasobach blokady przez inny proces i została wybrana jako ofiara zakleszczenia. Ponownie uruchom transakcję.
Ten błąd występuje, ponieważ odczyt powtarzalny blokuje zasób, który jest odczytywany lub aktualizowany przez transakcję 1 i tworzy zakleszczenie w innej transakcji, która próbuje uzyskać dostęp do tego samego zasobu.
Błąd mówi, że transakcja 2 ma zakleszczenie w zasobie z innym procesem i że ta transakcja została zablokowana przez zakleszczenie. Oznacza to, że druga transakcja uzyskała dostęp do zasobu, podczas gdy ta transakcja została zablokowana i nie otrzymała dostępu do zasobu.
Mówi również, aby ponownie uruchomić transakcję, ponieważ zasób jest teraz wolny. Teraz, jeśli ponownie uruchomisz transakcję 2, zobaczysz poprawną wartość pozycji w magazynie, tj. 7. Dzieje się tak, ponieważ transakcja 1 zmniejszyła już wartość IteminStock o 2, transakcja 2 dodatkowo zmniejsza ją o 3, więc 12 – (2+ 3) =7.