Проблемът с изгубената актуализация възниква, когато 2 едновременни транзакции се опитат да прочетат и актуализират едни и същи данни. Нека разберем това с помощта на пример.
Да предположим, че имаме таблица с име „Продукт“, която съхранява идентификатор, име и ItemsinStock за продукт.
Използва се като част от онлайн система, която показва броя на артикулите на склад за конкретен продукт и затова трябва да се актуализира всеки път, когато се извършва продажба на този продукт.
Таблицата изглежда така:
Id | Име | ItemsinStock |
1 | Лаптопи | 12 |
Сега помислете за сценарий, при който потребител пристига и започва процеса на закупуване на лаптоп. Това ще започне транзакция. Нека наречем тази транзакция, транзакция 1.
В същото време друг потребител влиза в системата и инициира транзакция, нека наречем тази транзакция 2. Разгледайте следната фигура.

Транзакция 1 чете артикулите на склад за лаптопи, които са 12. Малко по-късно транзакция 2 чете стойността за ItemsinStock за лаптопи, които все още ще бъдат 12 в този момент. След това транзакция 2 продава три лаптопа, малко преди транзакция 1 да продаде 2 артикула.
След това транзакция 2 първо ще завърши изпълнението си и ще актуализира ItemsinStock до 9, тъй като продаде три от 12-те лаптопа. Транзакция 1 се ангажира сама. Тъй като транзакция 1 продаде два артикула, тя актуализира ItemsinStock до 10.
Това е неправилно, правилната цифра е 12-3-2 =7
Работен пример за проблем със загубена актуализация
Нека да разгледаме проблема с изгубената актуализация в действие в SQL Server. Както винаги, първо ще създадем таблица и ще добавим някои фиктивни данни в нея.
Както винаги, уверете се, че сте архивирани правилно, преди да играете с нов код. Ако не сте сигурни, вижте тази статия за архивиране на SQL Server.
Изпълнете следния скрипт на вашия сървър на база данни.
<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>
Сега отворете две копия на студио за управление на SQL сървър един до друг. Ще изпълним една транзакция във всеки от тези случаи.

Добавете следния скрипт към първия екземпляр на 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>
Това е скриптът за транзакция 1. Тук започваме транзакцията и декларираме променлива от целочислен тип “@ItemsInStock”. Стойността на тази променлива се задава на стойността на колоната ItemsinStock за записа с Id 1 от таблицата с продукти. След това се добавя забавяне от 12 секунди, за да може транзакция 2 да завърши изпълнението си преди транзакция 1. След забавянето стойността на променливата @ItemsInStock се намалява с 2, което означава продажбата на 2 продукта.
И накрая, стойността на колоната ItemsinStock за записа с Id 1 се актуализира със стойността на променливата @ItemsInStock. След това отпечатваме стойността на променливата @ItemsInStock на екрана и извършваме транзакцията.
Във втория екземпляр на SSMS добавяме скрипта за транзакция 2, който е както следва:
<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>
Скриптът за транзакция 2 е подобен на транзакция 1. Тук обаче, в транзакция 2, забавянето е само за три секунди и намаляването на стойността за променлива @ItemsInStock е три, тъй като това е продажба на три артикула.
Сега стартирайте транзакция 1 и след това транзакция 2. Ще видите, че транзакция 2 първо завършва изпълнението си. И стойността, отпечатана за променливата @ItemsInStock, ще бъде 9. След известно време транзакция 1 също ще завърши изпълнението си и стойността, отпечатана за нейната променлива @ItemsInStock, ще бъде 10.

И двете от тези стойности са грешни, действителната стойност за колоната ItemsInStock за продукта с Id 1 трябва да бъде 7.
ЗАБЕЛЕЖКА:
Тук е важно да се отбележи, че проблемът с изгубената актуализация възниква само при нива на изолация на транзакциите за четене и незаети. При всички останали нива на изолация на транзакциите този проблем не възниква.
Прочетете ниво на изолация на повторяеми транзакции
Нека актуализираме нивото на изолация и за двете транзакции, за да се четат повтарящи се и да видим дали възниква проблемът с изгубената актуализация. Но преди това изпълнете следния оператор, за да актуализирате стойността за ItemsInStock обратно до 12.
Update products SET ItemsinStock = 12
Скрипт за транзакция 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>
Скрипт за транзакция 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>
Тук и в двете транзакции сме задали нивото на изолация на повтарящо се четене.
Сега стартирайте транзакция 1 и след това незабавно изпълнете транзакция 2. За разлика от предишния случай, транзакция 2 ще трябва да изчака транзакция 1 да се ангажира. След това се появява следната грешка за транзакция 2:
Съобщение 1205, ниво 13, състояние 51, ред 15
Транзакцията (идентификатор на процес 55) беше блокирана на ресурси за заключване с друг процес и беше избрана като жертва на безизходица. Изпълнете отново транзакцията.
Тази грешка възниква, защото повторяемото четене заключва ресурса, който се чете или актуализира от транзакция 1, и създава блокиране на друга транзакция, която се опитва да получи достъп до същия ресурс.
Грешката казва, че транзакция 2 има блокиране на ресурс с друг процес и че тази транзакция е била блокирана от блокирането. Това означава, че на другата транзакция е даден достъп до ресурса, докато тази транзакция е била блокирана и не е получил достъп до ресурса.
Също така пише да се изпълни отново транзакцията, тъй като ресурсът вече е безплатен. Сега, ако стартирате транзакция 2 отново, ще видите правилната стойност на артикулите на склад, т.е. 7. Това е така, защото транзакция 1 вече е намалила стойността на IteminStock с 2, транзакция 2 допълнително намалява това с 3, следователно 12 – (2+ 3) =7.
