同時トランザクションの実行中に発生する最も一般的な問題の1つは、ダーティリードの問題です。ダーティリードは、あるトランザクションが、同時に実行されているがまだコミットされていない別のトランザクションによって変更されているデータの読み取りを許可されている場合に発生します。
データを変更するトランザクションがそれ自体をコミットする場合、ダーティリードの問題は発生しません。ただし、データを変更するトランザクションが、他のトランザクションがデータを読み取った後にロールバックされた場合、後者のトランザクションには、実際には存在しないダーティデータが含まれています。
いつものように、新しいコードを試す前に、十分にバックアップされていることを確認してください。よくわからない場合は、MSSQLデータベースのバックアップに関するこの記事を参照してください。
例を使ってこれを理解しましょう。商品のID、名前、ItemsinStockを格納する「Product」という名前のテーブルがあるとします。
表は次のようになります:
[テーブルID=20 /]
ユーザーが商品の購入と商品の表示を同時に行えるオンラインシステムがあるとします。次の図を見てください。
ユーザーが製品を購入しようとするシナリオを考えてみましょう。トランザクション1は、ユーザーの購入タスクを実行します。トランザクションの最初のステップは、ItemsinStockを更新することです。
取引前の在庫は12品目です。トランザクションはこれを11に更新します。トランザクションは外部の課金ゲートウェイと通信します。
この時点で、別のトランザクション、たとえばトランザクション2がラップトップ用のItemsInStockを読み取ると、11が読み取られます。ただし、その後、トランザクション1の背後にいるユーザーのアカウントに十分な資金がないことが判明した場合、トランザクション1がロールされます。戻ると、ItemsInStock列の値は12に戻ります。
ただし、トランザクション2のItemsInStock列の値は11です。これはダーティデータであり、この問題はダーティリード問題と呼ばれます。
ダーティリード問題の実例
SQLServerで動作しているダーティリードの問題を見てみましょう。いつものように、最初にテーブルを作成し、それにダミーデータを追加しましょう。データベースサーバーで次のスクリプトを実行します。
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)
次に、2つのSQL ServerManagementStudioインスタンスを並べて開きます。これらのインスタンスのそれぞれで1つのトランザクションを実行します。
次のスクリプトを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
上記のスクリプトでは、Idが1であるproductsテーブルの「ItemsInStock」列の値を更新する新しいトランザクションを開始します。次に、「WaitFor」および「Delay」関数を使用して、顧客への請求の遅延をシミュレートします。スクリプトには10秒の遅延が設定されています。その後、トランザクションをロールバックするだけです。
SSMSの2番目のインスタンスでは、次のSELECTステートメントを追加するだけです。
USE pos;
-- Transaction 2
SELECT * FROM products
WHERE Id = 1
ここで、最初に最初のトランザクションを実行します。つまり、SSMSの最初のインスタンスでスクリプトを実行し、次にSSMSの2番目のインスタンスでスクリプトをすぐに実行します。
両方のトランザクションが10秒間実行され続け、その後、2番目のトランザクションで示されているようにID1のレコードの「ItemsInStock」列の値がまだ12であることがわかります。最初のトランザクションはそれを11に更新しましたが、10秒間待機してから、12にロールバックしましたが、2番目のトランザクションによって示される値は11ではなく12です。
実際に起こったことは、最初のトランザクションを実行したときに、「ItemsinStock」列の値が更新されたことです。次に、10秒間待機してから、トランザクションをロールバックしました。
最初のトランザクションの直後に2番目のトランザクションを開始しましたが、最初のトランザクションが完了するまで待機する必要がありました。そのため、2番目のトランザクションも10秒間待機し、最初のトランザクションが実行を完了した直後に2番目のトランザクションが実行されました。
コミットされた分離レベルを読み取る
トランザクション2が実行される前にトランザクション1の完了を待たなければならなかったのはなぜですか?
答えは、トランザクション間のデフォルトの分離レベルは「読み取りコミット」であるということです。 Read Committed分離レベルは、データがコミット状態にある場合にのみトランザクションがデータを読み取ることができるようにします。
この例では、トランザクション1がデータを更新しましたが、ロールバックされるまでデータをコミットしませんでした。これが、トランザクション2がデータを読み取る前にトランザクション1がデータをコミットするか、トランザクションをロールバックするのを待たなければならなかった理由です。
現在、実際のシナリオでは、単一のデータベースで同時に複数のトランザクションが発生することが多く、すべてのトランザクションが順番を待つ必要はありません。これにより、データベースが非常に遅くなる可能性があります。一度に1つのトランザクションしか処理できない大規模なWebサイトからオンラインで何かを購入することを想像してみてください!
コミットされていないデータの読み取り
この問題の答えは、トランザクションがコミットされていないデータで機能できるようにすることです。
コミットされていないデータを読み取るには、トランザクションの分離レベルを「コミットされていないデータを読み取る」に設定するだけです。以下のスクリプトに従って分離レベルを追加して、トランザクション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を実行し、ID 1の「ItemsinStock」の値を12から11に更新してから、10秒間待機してからロールバックします。
一方、トランザクションwは、右側の結果ウィンドウに示されているように、11であるダーティデータを読み取ります。トランザクション1はロールバックされるため、これはテーブルの実際の値ではありません。実際の値は12です。トランザクション2をもう一度実行してみると、今回は12が取得されることがわかります。
コミットされていない読み取りは、ダーティ読み取りの問題がある唯一の分離レベルです。この分離レベルは、すべての分離レベルの中で最も制限が少なく、コミットされていないデータを読み取ることができます。
明らかに、Read Uncommittedを使用することには賛否両論があり、データベースがどのアプリケーションに使用されているかによって異なります。明らかに、ATMシステムやその他の非常に安全なシステムの背後にあるデータベースにこれを使用することは非常に悪い考えです。ただし、速度が非常に重要なアプリケーション(大規模なeコマースストアの実行)では、ReadUncommittedを使用する方が理にかなっています。