STEP 3. トランザクションの分離と Isolation Level
3.3 反復読み取り不可: Non Repeatable Read
Let's Try
それでは、これを試してみましょう。予約システムを例に、ダブル ブッキングが発生してしまう 状況を確認してみましょう。
1. まずは、sampleDB データベース内へ「予約」テーブルを作成します。
USE sampleDB CREATE TABLE 予約 ( 席番 int PRIMARY KEY ,予約者名 varchar(50) )
INSERT INTO 予約 VALUES(1, NULL) INSERT INTO 予約 VALUES(2, 'sato_t') INSERT INTO 予約 VALUES(3, NULL) INSERT INTO 予約 VALUES(4, NULL) SELECT * FROM 予約
2. 次に、ツールバーの[新しいクエリ]から、2つの接続を作って、それぞれから SELECT ス テートメントを実行して、予約状況を確認します(COMMIT TRAN を省略して、トランザ クション中とします)。
USE sampleDB BEGIN TRAN
SELECT * FROM 予約
3. 確認後、1つ目の接続から「席番=3」を予約します。
-- 座席3 を予約 UPDATE 予約
SET 予約者名 = 'xxx' WHERE 席番 = 3
-- コミット(確定)
COMMIT TRAN
COMMIT TRAN を実行して、予約を確定することで、1 つ目の接続によって、「席番=3」
が予約された状態になりました。
4. 次に、2つ目の接続から、同じ「席番=3」を予約してみます。
-- 座席 3 を予約 UPDATE 予約
SET 予約者名 = 'yyy' WHERE 席番 = 3
-- コミット(確定)
COMMIT TRAN
予約確定
ダブル ブッキング が発生!
2 つ目の接続からも問題なく、予約を確定することができるので、ダブル ブッキングが発生 してしまいます。
このように、デフォルトの分離レベルでは、反復読み取りができない(Non Repeatable Read)
状態なので、読み取ったデータが別のユーザーによって更新されてしまう可能性があるのです。
5. 最後に、「席番=3」のデータを NULL へ戻しておきます。
-- 座席 3 を NULL へ戻す UPDATE 予約
SET 予約者名 = NULL WHERE 席番 = 3
反復読み取り不可の回避 ~Repeatable Read~
反復読み取り不可を回避するには、「Repeatable Read」(反復読み取り可能)という分離レベル を利用します。その名のとおり、反復読み取りが可能なレベルという意味です。これは、内部的に は、次のようにロックの動作を変更することで実現しています。
共有ロックをトランザクションが完了するまで保持
このように共有ロックをトランザクションが完了するまで保持するようにすれば、データの更新時 に発生する排他ロックを、共有ロックでブロックできるようになります。
これにより、データが同時に更新されることを防ぐことができ、ダブル ブッキングを防ぐことが できます。しかし、この防止方法は、デッドロックを発生させることで、ダブル ブッキングを防 いでいる点に注意する必要があります。デッドロックの発生によって、1つのトランザクションは ロールバックし、1つだけをコミットすることで、2つ同時ではなく、1つだけが予約確定できる ようにしています。なお、このタイプのデッドロックは、共有ロックを排他ロックへ変換しようと しているときに発生することから「変換デッドロック」とも呼ばれています。
Let's Try
それでは、これを試してみましょう。
1. 前の手順と同じように、ツールバーの[新しいクエリ]から、2つの接続を作って、それぞれ から SELECT ステートメントを実行して、予約状況を確認します(COMMIT TRAN を省略 して、トランザクション中とします)。このとき、分離レベルへ「Repeatable Read」を指 定するようにします。
USE sampleDB BEGIN TRAN
SELECT * FROM 予約 WITH(RepeatableRead)
2. 確認後、1つ目の接続から「席番=3」を予約してみます。
-- 座席 3 を予約 UPDATE 予約
SET 予約者名 = 'xxx' WHERE 席番 = 3
-- コミット COMMIT TRAN
Repeatable Read を指定
結果は、ロック待ちになります。
3. 次に、2つ目の接続から、同じように「席番=3」を予約してみます。
-- 座席 3 を予約 UPDATE 予約
SET 予約者名 = 'yyy' WHERE 席番 = 3
-- コミット COMMIT TRAN
結果は、数秒間、お互いにロック待ちの状態(デッドロック)になり、SQL Server によって デッドロックが検出されると、どちらかのトランザクションがロールバックされます。
4. 1つのトランザクションは、正常に終了しているので、これを確認しておきましょう。
SELECT * FROM 予約
ロック待ち 状態
正常に終了
(予約確定)
デッドロック発生の犠牲者
(ロールバックされた側)
このように、分離レベルを「Repeatable Read」へ変更した場合は、共有ロックをトランザ クションが完了するまで保持することで、データの矛盾が発生しないようにしています。しか し、この防止方法は、デッドロックを発生させることで、ダブル ブッキングを防いでいる点 に注意する必要があります。
また、この手順では、同じ「席番=3」のデータを同時に更新する例でしたが、次のように、
お互いに異なる席番(1と4)を指定した場合にも、同様の動作が発生します。
2つのトランザクションが異なる席を予約しようとしたにも関わらず、デッドロックが発生し てしまうのです(排他ロックが共有ロックにブロックされるため)。これでは、予約が失敗し た側が、再度予約状況を確認したときに、予約エラーになった席が「空席」として表示される ことになるので、利用している人にとっては、そのシステムに不満を抱くことになるでしょう。
5. 最後に、「席番 IN(1, 3, 4)」のデータを NULL へ戻しておきます。
-- 座席 1, 3, 4 を NULL へ戻す UPDATE 予約
SET 予約者名 = NULL WHERE 席番 IN(1, 3, 4)
正常に終了
(予約確定)
席番1を予約 席番4 を予約
異なる席を予約したのに デッドロックが発生!
Repeatable Read の注意点 ~デッドロックの多発~
Repeatable Read レベルは、ダブル ブッキングを防げるという意味では、利用価値がある分離レ
ベルに見えるかもしれません。しかし、デッドロックを発生させているという点に大きな落とし穴 があります。これは、次に説明する分離レベル「Serializable」でも発生するのですが、デッドロ ックが多発するという事態です。具体的には、次のように、複数の予約端末がある場合に発生しま す。
この図は、複数の端末(複数のユーザー)が、それぞれ同時に異なる座席を予約しようとしている 場合です。異なる座席を指定しているので、何の問題もなく、すぐに予約完了させたい状況ですが、
分離レベルに「Repeatable Read」を利用している場合は、トランザクションが完了するまで共 有ロックを保持するので、実際の予約時(UPDATE 実行時)にデッドロックが発生してしまいま す。
図のように、4 台で同時にデッドロックが発生した場合は、SQL Server は、まず 4つのうち 1 つのトランザクションだけをロールバックします。残りの 3 つは、依然としてデッドロック中に なりますが、これが SQL Server に検出されるのは、次の検出のタイミング(5 秒後)です。検 出後は、また 1つのトランザクションだけをロールバックします。
席番 予約者名
1 なし
2 sato_t
3 なし
4 なし
5 なし
6 なし
端末X
予約テーブル
端末Y
端末A
端末B 予約したい3番 を
予約したい4番 を
予約したい5番 を
予約したい6番 を
それぞれの端末が読み取り時 に獲得した共有ロックが トランザクションが完了する まで保持され、デッドロック発生
↓
残りの 2 つは、依然としてデッドロック中で、これが SQL Server に検出されるのは、また 5 秒後になります。検出後は、また 1つだけをロールバックし、1つはコミットさせます。これで、
1つの端末だけで正しく予約できたことになります。
しかし、たった 1つの予約を完了させるのに、4台でデッドロックが発生した場合は、検出時間だ けで 5秒× 3回の 15秒もかかってしまうのです。しかも、ロールバックされた 3台の端末には エラー(予約失敗)が通達され、予約し直す必要もあります。また、予約しようとしたはずの席が
「空席」として表示されることになるので、これでは利用者はそのシステムに大きな不満を抱くこ とになるでしょう。
このように、Repeatable Read レベルは、せっかくデータの矛盾が回避できても、デッドロック の多発という大きな問題が発生するので、パフォーマンス上のデメリットと、システム利用上のデ メリットがあります。したがって、実際に Repeatable Read レベルを利用する場面は、ほとんど ありません。このようなデータの矛盾を回避したい場合は、後述の「更新ロック」を利用するか、
「楽観的同時実行制御」を利用することをお勧めします。