Един от най-често срещаните проблеми, които възникват при изпълнение на едновременни транзакции, е проблемът с мръсното четене. Мръсно четене възниква, когато на една транзакция е разрешено да чете данни, които се променят от друга транзакция, която се изпълнява едновременно, но която все още не се е ангажирала.
Ако транзакцията, която променя данните, се ангажира сама, проблемът с мръсното четене не се появява. Ако обаче транзакцията, която променя данните, бъде върната назад, след като другата транзакция е прочела данните, последната транзакция има мръсни данни, които всъщност не съществуват.
Както винаги, уверете се, че сте добре архивирани, преди да експериментирате с нов код. Вижте тази статия за архивиране на MS SQL бази данни, ако не сте сигурни.
Нека разберем това с помощта на пример. Да предположим, че имаме таблица с име „Продукт“, която съхранява идентификатор, име и ItemsinStock за продукта.
Таблицата изглежда така:
[table id=20 /]
Да предположим, че имате онлайн система, където потребителят може да купува продукти и да разглежда продукти едновременно. Разгледайте следната фигура.
Помислете за сценарий, при който потребителят се опитва да закупи продукт. Транзакция 1 ще изпълни задачата за покупка за потребителя. Първата стъпка в транзакцията ще бъде актуализирането на ItemsinStock.
Преди сделката има 12 артикула на склад; транзакцията ще актуализира това до 11. Транзакцията вече ще комуникира с външен шлюз за фактуриране.
Ако в този момент друга транзакция, да кажем Транзакция 2, чете ItemsInStock за лаптопи, тя ще прочете 11. Въпреки това, ако впоследствие потребителят зад Транзакция 1 се окаже, че няма достатъчно средства в сметката си, Транзакция 1 ще бъде прехвърлена назад и стойността за колоната ItemsInStock ще се върне на 12.
Въпреки това, транзакция 2 има 11 като стойност за колоната ItemsInStock. Това са мръсни данни и проблемът се нарича проблем с мръсно четене.
Работен пример за проблем с мръсно четене
Нека да разгледаме проблема с мръсното четене в действие в SQL Server. Както винаги, първо, нека създадем нашата таблица и да добавим някои фиктивни данни към нея. Изпълнете следния скрипт на вашия сървър на база данни.
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, 'iPhone', 15),
(3, 'Tablets', 10)
Сега отворете две копия на студио за управление на SQL сървър един до друг. Ще изпълним една транзакция във всеки от тези случаи.
Добавете следния скрипт към първия екземпляр на SSMS.
USE pos;
SELECT * FROM products
-- Transaction 1
BEGIN Tran
UPDATE products set ItemsInStock = 11
WHERE Id = 1
-- Billing the customer
WaitFor Delay '00:00:10'
Rollback Transaction
В горния скрипт стартираме нова транзакция, която актуализира стойността за колоната „ItemsInStock“ на таблицата с продукти, където Id е 1. След това симулираме забавянето за таксуване на клиента, като използваме функциите „WaitFor“ и „Delay“. В скрипта е зададено забавяне от 10 секунди. След това просто връщаме транзакцията обратно.
Във втория екземпляр на SSMS просто добавяме следния оператор SELECT.
USE pos;
-- Transaction 2
SELECT * FROM products
WHERE Id = 1
Сега първо изпълнете първата транзакция, т.е. изпълнете скрипта в първия екземпляр на SSMS и след това незабавно изпълнете скрипта във втория екземпляр на SSMS.
Ще видите, че и двете транзакции ще продължат да се изпълняват за 10 секунди и след това ще видите, че стойността за колоната „ItemsInStock“ за записа с Id 1 все още е 12, както е показано от втората транзакция. Въпреки че първата транзакция я актуализира до 11, изчака 10 секунди и след това я върна до 12, стойността, показана от втората транзакция, е 12, а не 11.
Това, което всъщност се случи, е, че когато изпълнихме първата транзакция, тя актуализира стойността за колоната „ItemsinStock“. След това изчака 10 секунди и след това върна транзакцията.
Въпреки че започнахме втората транзакция веднага след първата, тя трябваше да изчака първата транзакция да завърши. Ето защо втората транзакция също изчака 10 секунди и защо втората транзакция се изпълни веднага след като първата транзакция завърши изпълнението си.
Прочетете ангажирано ниво на изолация
Защо транзакция 2 трябваше да изчака завършването на транзакция 1, преди да се изпълни?
Отговорът е, че нивото на изолация по подразбиране между транзакциите е „прочетено е извършено“. Нивото на изолация Read Committed гарантира, че данните могат да бъдат прочетени от транзакция само ако са в състояние на ангажимент.
В нашия пример транзакция 1 актуализира данните, но не ги ангажира, докато не бъде върната назад. Ето защо транзакция 2 трябваше да изчака транзакция 1 да поеме данните или да отмени транзакцията, преди да може да прочете данните.
Сега, в практически сценарии, често имаме множество транзакции, които се извършват в една база данни по едно и също време и не искаме всяка транзакция да чака своя ред. Това може да направи базите данни много бавни. Представете си, че купувате нещо онлайн от голям уебсайт, който може да обработва само една транзакция в даден момент!
Четене на незаети данни
Отговорът на този проблем е да позволите на вашите транзакции да работят с незаети данни.
За да прочетете незаети данни, просто задайте нивото на изолация на транзакцията на „четене без ангажимент“. Актуализирайте транзакцията 2, като добавите ниво на изолация съгласно скрипта по-долу.
USE pos;
-- Transaction 2
set transaction isolation level read uncommitted
SELECT * FROM products
WHERE Id = 1
Сега, ако стартирате транзакция 1 и след това незабавно изпълните транзакция 2, ще видите, че транзакция 2 няма да чака транзакция 1 да поеме данни. Транзакция 2 незабавно ще прочете мръсните данни. Това е показано на следната фигура:
Тук екземплярът отляво изпълнява транзакция 1, а екземплярът отдясно изпълнява транзакция 2.
Първо изпълняваме транзакция 1, която актуализира стойността на „ItemsinStock“ за идентификатор 1 до 11 от 12 и след това изчаква 10 секунди, преди да бъде върната назад.
Междувременно транзакцията w чете мръсните данни, които са 11, както е показано в прозореца с резултати вдясно. Тъй като транзакция 1 се връща назад, това не е действителната стойност в таблицата. Действителната стойност е 12. Опитайте да изпълните транзакция 2 отново и ще видите, че този път тя извлича 12.
Read uncommitted е единственото ниво на изолация, което има проблем с мръсното четене. Това ниво на изолация е най-малко ограничаващо от всички нива на изолация и позволява четене на незаети данни.
Очевидно има плюсове и минуси за използването на Read Uncommitted, зависи от това за какво приложение се използва вашата база данни. Очевидно би било много лоша идея да се използва това за базата данни зад системи за банкомат и други много сигурни системи. Въпреки това, за приложения, където скоростта е много важна (управление на големи магазини за електронна търговия), използването на Read Uncommitted е по-разумно.