更新が失われる問題は、2つの同時トランザクションが同じデータを読み取って更新しようとしたときに発生します。例を使ってこれを理解しましょう。
商品のid、name、ItemsinStockを格納する「Product」という名前のテーブルがあるとします。
これは、特定の商品の在庫数を表示するオンラインシステムの一部として使用されるため、その商品の販売が行われるたびに更新する必要があります。
表は次のようになります:
Id | | ItemsinStock |
1 | | 12 |
ここで、ユーザーが到着してラップトップを購入するプロセスを開始するシナリオを考えてみましょう。これにより、トランザクションが開始されます。このトランザクションをトランザクション1と呼びましょう。
同時に、別のユーザーがシステムにログインしてトランザクションを開始します。このトランザクションを2と呼びましょう。次の図を見てください。
トランザクション1は、ラップトップの在庫のアイテムである12を読み取ります。少し後のトランザクション2は、この時点でまだ12であるラップトップのItemsinStockの値を読み取ります。次に、トランザクション1が2つのアイテムを販売する直前に、トランザクション2は3つのラップトップを販売します。
次に、トランザクション2は最初に実行を完了し、12台のラップトップのうち3台を販売したため、ItemsinStockを9に更新します。トランザクション1はそれ自体をコミットします。トランザクション1は2つのアイテムを販売したため、ItemsinStockを10に更新します。
これは正しくありません。正しい数値は12-3-2=7
失われた更新の問題の実例
SQLServerで動作中の失われた更新の問題を見てみましょう。いつものように、最初にテーブルを作成し、それにダミーデータを追加します。
いつものように、新しいコードで遊ぶ前に、適切にバックアップされていることを確認してください。よくわからない場合は、SQLServerのバックアップに関するこの記事を参照してください。
データベースサーバーで次のスクリプトを実行します。
<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>
次に、2つのSQL ServerManagementStudioインスタンスを並べて開きます。これらのインスタンスのそれぞれで1つのトランザクションを実行します。
次のスクリプトを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」を宣言します。この変数の値は、productsテーブルのId1のレコードのItemsinStock列の値に設定されます。次に、トランザクション2がトランザクション1の前に実行を完了できるように、12秒の遅延が追加されます。遅延後、@ ItemsInStock変数の値は2だけデクリメントされ、2つの製品の販売を示します。
最後に、Id 1のレコードのItemsinStock列の値が、@ItemsInStock変数の値で更新されます。次に、@ ItemsInStock変数の値を画面に出力し、トランザクションをコミットします。
SSMSの2番目のインスタンスでは、トランザクション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では、遅延は3秒間のみであり、@ ItemsInStock変数の値の減少は3であるため、3つのアイテムの販売です。
>次に、トランザクション1を実行してから、トランザクション2を実行します。トランザクション2が最初に実行を完了していることがわかります。また、@ ItemsInStock変数に出力される値は9になります。しばらくすると、トランザクション1も実行を完了し、@ItemsInStock変数に出力される値は10になります。
これらの値は両方とも間違っています。ID1の製品のItemsInStock列の実際の値は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
トランザクション(プロセスID 55)は、別のプロセスとのロックリソースでデッドロックされ、デッドロックの犠牲者として選択されました。トランザクションを再実行します。
このエラーは、繰り返し可能な読み取りがトランザクション1によって読み取られている、または更新されているリソースをロックし、同じリソースにアクセスしようとする他のトランザクションにデッドロックを作成するために発生します。
このエラーは、トランザクション2で別のプロセスのリソースにデッドロックがあり、このトランザクションがデッドロックによってブロックされていることを示しています。これは、このトランザクションがブロックされ、リソースへのアクセスが許可されていないときに、他のトランザクションにリソースへのアクセスが許可されたことを意味します。
また、リソースが現在空いているため、トランザクションを再実行するように指示されています。ここで、トランザクション2を再度実行すると、在庫のあるアイテムの正しい値、つまり7が表示されます。これは、トランザクション1がすでにIteminStockの値を2減らし、トランザクション2がこれをさらに3減らし、したがって12 –(2+ 3)=7。