Jednym z najczęstszych problemów, które występują podczas jednoczesnego uruchamiania transakcji, jest problem Brudnego odczytu. Brudny odczyt występuje, gdy jedna transakcja może odczytywać dane, które są modyfikowane przez inną działającą współbieżnie transakcję, która jeszcze się nie zatwierdziła.
Jeśli transakcja modyfikująca dane sama się zatwierdzi, problem z brudnym odczytem nie wystąpi. Jeśli jednak transakcja, która modyfikuje dane, zostanie wycofana po odczytaniu danych przez inną transakcję, ta ostatnia transakcja zawiera brudne dane, które w rzeczywistości nie istnieją.
Jak zawsze, upewnij się, że masz dobrą kopię zapasową, zanim zaczniesz eksperymentować z nowym kodem. Zobacz ten artykuł na temat tworzenia kopii zapasowych baz danych MS SQL, jeśli nie masz pewności.
Zrozummy to na przykładzie. Załóżmy, że mamy tabelę o nazwie „Produkt”, która przechowuje identyfikator, nazwę i ItemsinStock dla produktu.
Tabela wygląda tak:
[identyfikator tabeli=20 /]
Załóżmy, że masz system online, w którym użytkownik może jednocześnie kupować produkty i przeglądać produkty. Spójrz na poniższy rysunek.
Rozważ scenariusz, w którym użytkownik próbuje kupić produkt. Transakcja 1 wykona zadanie zakupu dla użytkownika. Pierwszym krokiem w transakcji będzie aktualizacja ItemsinStock.
Przed transakcją na magazynie znajduje się 12 pozycji; transakcja zaktualizuje to do 11. Transakcja będzie teraz komunikować się z zewnętrzną bramką rozliczeniową.
Jeśli w tym momencie inna transakcja, powiedzmy Transakcja 2, odczyta ItemsInStock dla laptopów, będzie to 11. Jeśli później okaże się, że użytkownik odpowiedzialny za Transakcję 1 ma niewystarczające środki na swoim koncie, Transakcja 1 zostanie zrzucona wstecz, a wartość kolumny ItemsInStock powróci do 12.
Jednak transakcja 2 ma 11 jako wartość kolumny ItemsInStock. To są brudne dane, a problem nazywa się problemem brudnego odczytu.
Przykład roboczy problemu z brudnym odczytem
Przyjrzyjmy się problemowi brudnego odczytu w akcji w SQL Server. Jak zawsze, najpierw stwórzmy naszą tabelę i dodajmy do niej kilka fikcyjnych danych. Wykonaj następujący skrypt na serwerze bazy danych.
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)
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.
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
W powyższym skrypcie rozpoczynamy nową transakcję, która aktualizuje wartość kolumny „ItemsInStock” w tabeli produktów, gdzie identyfikator wynosi 1. Następnie symulujemy opóźnienie w rozliczeniu klienta za pomocą funkcji „WaitFor” i „Delay”. W skrypcie ustawiono 10-sekundowe opóźnienie. Następnie po prostu cofamy transakcję.
W drugiej instancji SSMS po prostu dodajemy następującą instrukcję SELECT.
USE pos;
-- Transaction 2
SELECT * FROM products
WHERE Id = 1
Teraz najpierw uruchom pierwszą transakcję, tj. wykonaj skrypt w pierwszej instancji SSMS, a następnie natychmiast wykonaj skrypt w drugiej instancji SSMS.
Zobaczysz, że obie transakcje będą wykonywane przez 10 sekund, a potem zobaczysz, że wartość kolumny „ItemsInStock” dla rekordu o identyfikatorze 1 nadal wynosi 12, jak pokazuje druga transakcja. Chociaż pierwsza transakcja zaktualizowała ją do 11, odczekała 10 sekund, a następnie przywróciła ją do 12, wartość pokazana przez drugą transakcję to 12, a nie 11.
To, co faktycznie się wydarzyło, to to, że kiedy uruchomiliśmy pierwszą transakcję, zaktualizowała wartość kolumny „ItemsinStock”. Następnie odczekał 10 sekund, a następnie wycofał transakcję.
Chociaż drugą transakcję rozpoczęliśmy zaraz po pierwszej, musiała ona poczekać na zakończenie pierwszej transakcji. Dlatego druga transakcja również czekała 10 sekund i dlatego druga transakcja została wykonana natychmiast po zakończeniu pierwszej transakcji.
Odczytaj zatwierdzony poziom izolacji
Dlaczego transakcja 2 musiała czekać na zakończenie transakcji 1, zanim została wykonana?
Odpowiedź brzmi, że domyślnym poziomem izolacji między transakcjami jest „odczyt zatwierdzony”. Poziom izolacji Zatwierdzenie odczytu zapewnia, że dane mogą być odczytywane przez transakcję tylko wtedy, gdy jest ona w stanie zatwierdzonym.
W naszym przykładzie transakcja 1 zaktualizowała dane, ale nie zatwierdziła ich, dopóki nie została wycofana. Dlatego transakcja 2 musiała czekać, aż transakcja 1 zatwierdzi dane lub wycofa transakcję, zanim będzie mogła odczytać dane.
Teraz w praktycznych scenariuszach często mamy do czynienia z wieloma transakcjami jednocześnie na jednej bazie danych i nie chcemy, aby każda transakcja musiała czekać na swoją kolej. Może to bardzo spowolnić bazy danych. Wyobraź sobie, że kupujesz coś online z dużej witryny, która może przetwarzać tylko jedną transakcję na raz!
Czytanie niezatwierdzonych danych
Odpowiedzią na ten problem jest umożliwienie transakcji z niezatwierdzonymi danymi.
Aby odczytać niezatwierdzone dane, po prostu ustaw poziom izolacji transakcji na „odczyt niezatwierdzony”. Zaktualizuj transakcję 2, dodając poziom izolacji zgodnie z poniższym skryptem.
USE pos;
-- Transaction 2
set transaction isolation level read uncommitted
SELECT * FROM products
WHERE Id = 1
Teraz, jeśli uruchomisz transakcję 1, a następnie natychmiast uruchomisz transakcję 2, zobaczysz, że transakcja 2 nie będzie czekać, aż transakcja 1 zatwierdzi dane. Transakcja 2 natychmiast odczyta brudne dane. Jest to pokazane na poniższym rysunku:
Tutaj instancja po lewej uruchamia transakcję 1, a instancja po prawej uruchamia transakcję 2.
Najpierw uruchamiamy transakcję 1, która aktualizuje wartość „ItemsinStock” dla identyfikatora 1 do 11 z 12, a następnie czeka na 10 sekund przed wycofaniem.
Tymczasem transakcja w odczytuje brudne dane, czyli 11, jak pokazano w oknie wyników po prawej stronie. Ponieważ transakcja 1 jest wycofywana, nie jest to rzeczywista wartość w tabeli. Rzeczywista wartość to 12. Spróbuj ponownie wykonać transakcję 2, a zobaczysz, że tym razem pobiera 12.
Odczyt niezatwierdzony to jedyny poziom izolacji, w którym występuje problem z nieczystym odczytem. Ten poziom izolacji jest najmniej restrykcyjny ze wszystkich poziomów izolacji i umożliwia odczytywanie niezatwierdzonych danych.
Oczywiście istnieją zalety i wady korzystania z funkcji Read Uncommitted, zależy to od tego, do jakiej aplikacji jest używana Twoja baza danych. Oczywiście bardzo złym pomysłem byłoby użycie tego dla bazy danych systemów ATM i innych bardzo bezpiecznych systemów. Jednak w przypadku aplikacji, w których szybkość jest bardzo ważna (prowadząc duże sklepy e-commerce), użycie Read Uncommitted ma więcej sensu.