Problém ztracené aktualizace nastane, když se 2 souběžné transakce pokusí číst a aktualizovat stejná data. Pojďme to pochopit pomocí příkladu.
Předpokládejme, že máme tabulku s názvem „Produkt“, která ukládá ID, název a položku ItemsinStock pro produkt.
Používá se jako součást online systému, který zobrazuje počet položek na skladě pro konkrétní produkt, a proto je nutné jej aktualizovat při každém prodeji tohoto produktu.
Tabulka vypadá takto:
ID | Jméno | ItemsinStock |
1 | Laptopy | 12 |
Nyní zvažte scénář, kdy přijde uživatel a zahájí proces nákupu notebooku. Tím zahájíte transakci. Nazvěme tuto transakci, transakce 1.
Současně se do systému přihlásí jiný uživatel a zahájí transakci, nazvěme tuto transakci 2. Podívejte se na následující obrázek.
Transakce 1 přečte položky na skladě pro notebooky, kterých je 12. O něco později transakce 2 přečte hodnotu položky ItemsinStock pro notebooky, kterých bude v tuto chvíli stále 12. Transakce 2 pak prodá tři notebooky, krátce před transakcí 1 prodá 2 položky.
Transakce 2 poté nejprve dokončí svou realizaci a aktualizuje ItemsinStock na 9, protože prodala tři z 12 notebooků. Transakce 1 se sama zaváže. Protože transakce 1 prodala dvě položky, aktualizuje ItemsinStock na 10.
Toto je nesprávné, správný údaj je 12-3-2 =7
Pracovní příklad problému se ztrátou aktualizace
Pojďme se podívat na problém ztracené aktualizace v akci v SQL Server. Jako vždy nejprve vytvoříme tabulku a přidáme do ní nějaká fiktivní data.
Jako vždy se před hraním s novým kódem ujistěte, že jste správně zálohováni. Pokud si nejste jisti, přečtěte si tento článek o zálohování serveru SQL.
Na svém databázovém serveru spusťte následující skript.
<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>
Nyní otevřete dvě instance SQL Server Management Studio vedle sebe. V každém z těchto případů provedeme jednu transakci.
Přidejte následující skript do první instance 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>
Toto je skript pro transakci 1. Zde zahájíme transakci a deklarujeme proměnnou typu integer „@ItemsInStock“. Hodnota této proměnné je nastavena na hodnotu sloupce ItemsinStock pro záznam s Id 1 z tabulky produktů. Poté se přidá zpoždění 12 sekund, aby transakce 2 mohla dokončit své provedení před transakcí 1. Po zpoždění se hodnota proměnné @ItemsInStock sníží o 2, což znamená prodej 2 produktů.
Nakonec je hodnota pro sloupec ItemsInStock pro záznam s ID 1 aktualizována hodnotou proměnné @ItemsInStock. Poté vytiskneme hodnotu proměnné @ItemsInStock na obrazovku a potvrdíme transakci.
Ve druhé instanci SSMS přidáme skript pro transakci 2, který je následující:
<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>
Skript pro transakci 2 je podobný transakci 1. Avšak zde v transakci 2 je zpoždění pouze tři sekundy a snížení hodnoty proměnné @ItemsInStock je tři, protože se jedná o prodej tří položek.
Nyní spusťte transakci 1 a poté transakci 2. Nejprve uvidíte, jak transakce 2 dokončí své provedení. A hodnota vytištěná pro proměnnou @ItemsInStock bude 9. Po nějaké době transakce 1 také dokončí své provádění a hodnota vytištěná pro proměnnou @ItemsInStock bude 10.
Obě tyto hodnoty jsou chybné, skutečná hodnota ve sloupci ItemsInStock pro produkt s ID 1 by měla být 7.
POZNÁMKA:
Zde je důležité poznamenat, že problém se ztracenou aktualizací nastává pouze u úrovní izolace transakcí potvrzeného čtení a nepotvrzeného čtení. U všech ostatních úrovní izolace transakcí k tomuto problému nedochází.
Přečtěte si úroveň izolace opakovatelných transakcí
Pojďme aktualizovat úroveň izolace pro obě transakce tak, aby byly opakovatelné, a uvidíme, zda dojde k problému se ztracenou aktualizací. Předtím však proveďte následující příkaz a aktualizujte hodnotu položky ItemsInStock zpět na 12.
Update products SET ItemsinStock = 12
Skript pro transakci 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>
Skript pro transakci 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>
Zde v obou transakcích jsme nastavili úroveň izolace na opakovatelné čtení.
Nyní spusťte transakci 1 a poté okamžitě spusťte transakci 2. Na rozdíl od předchozího případu bude muset transakce 2 počkat, než se transakce 1 sama potvrdí. Poté dojde k následující chybě pro transakci 2:
Msg 1205, Level 13, State 51, Line 15
Transakce (ID procesu 55) uvízla na prostředcích zámku s jiným procesem a byla vybrána jako oběť zablokování. Spusťte transakci znovu.
K této chybě dochází, protože opakovatelné čtení uzamkne prostředek, který je čten nebo aktualizován transakcí 1, a vytvoří uváznutí na jiné transakci, která se pokouší o přístup ke stejnému prostředku.
Chyba říká, že transakce 2 uvázla na prostředku s jiným procesem a že tato transakce byla zablokováním zablokována. To znamená, že druhé transakci byl udělen přístup ke zdroji, zatímco tato transakce byla zablokována a nebyl jí udělen přístup ke zdroji.
Také říká, že je třeba znovu spustit transakci, protože zdroj je nyní volný. Nyní, když znovu spustíte transakci 2, uvidíte správnou hodnotu položek na skladě, tj. 7. Je to proto, že transakce 1 již snížila hodnotu IteminStock o 2, transakce 2 ji dále snižuje o 3, tedy 12 – (2+ 3) =7.