TL; DR:要約すると、次のようになります。行を挿入する場合、生成の間に機会の窓がありますか新しいIdentity
値と、クラスター化インデックスの対応する行キーのlockingの外部監視者がnewerIdentity
値は同時トランザクションによって挿入されますか? (SQL Serverで。)
詳細バージョン
Identity
というCheckpointSequence
列を持つSQL Serverテーブルがあります。これは、テーブルのクラスター化インデックスのキーです(これには、追加の非クラスター化インデックスもいくつかあります)。行は、いくつかの並行プロセスとスレッドによって(分離レベルREAD COMMITTED
で、IDENTITY_INSERT
なしで)テーブルに挿入されます。同時に、クラスタ化インデックスから行を定期的にreadingし、そのCheckpointSequence
列(また、分離レベルREAD COMMITTED
)で並べられた行があります。 READ COMMITTED SNAPSHOT
オプションがオフになっている)。
私は現在、読み取りプロセスがチェックポイントを「スキップ」できないという事実に依存しています。私の質問は次のとおりです:このプロパティを信頼できますか?そして、そうでない場合、それを実現するために何ができますか?
例:ID値が1、2、3、4、および5の行が挿入される場合、リーダーは、値4の行を表示する前に、値5の行を表示してはなりません 。テストは、ORDER BY CheckpointSequence
句(およびWHERE CheckpointSequence > -1
句)を含むクエリが、行5が既にコミットされている場合でも、行4が読み取られるがまだコミットされていない場合は確実にブロックすることを示しています。 。
少なくとも理論的には、この仮定が破られる可能性のある競合状態がここにあると思います。残念ながら、Identity
に関するドキュメントでは、複数の同時トランザクションのコンテキストでIdentity
がどのように機能するかについてはあまり言及されておらず、「各新しい値は現在のシードと増分に基づいて生成されます。 」 「特定のトランザクションの各新しい値は、テーブル上の他の同時トランザクションとは異なります。」 ( [〜#〜] msdn [〜#〜] )
私の推論は、それはこのように何らかの形で機能する必要があります:
ステップ2と3の間には非常に小さなウィンドウがあり、
もちろん、この可能性は非常に低いようです。しかし、それでも-それが起こる可能性があります。それともできますか?
(コンテキストに関心がある場合:これは NEventStoreのSQL Persistence Engine)の実装です 。NEventStoreは、すべてのイベントが新しい昇順のチェックポイントシーケンス番号を取得する追加専用のイベントストアを実装します。クライアントはイベントを読み取りますすべての種類の計算を実行するためにチェックポイントで順序付けられたイベントストアから。チェックポイントXのイベントが処理されると、クライアントは「新しい」イベント、つまりチェックポイントX + 1以上のイベントのみを考慮するため、重要です。これらのイベントは再度考慮されることはないため、スキップすることはできません。現在、Identity
ベースのチェックポイント実装がこの要件を満たすかどうかを確認しようとしています。これらは使用される正確なSQLステートメント: Schema 、 Writer's query 、 Reader's Query 。)
私が正しいと上記の状況が発生する可能性がある場合、私はそれらに対処するための2つのオプションのみを見ることができますが、どちらも不十分です:
Identity
はもちろんギャップを生成する可能性があるため(たとえば、トランザクションがロールバックされたとき)、Xが来ない可能性があります。より良いアイデアはありますか?
行を挿入するときに、新しいIdentity値の生成とクラスター化インデックス内の対応する行キーのロックの間に、外部の監視者が同時トランザクションによって挿入された新しいIdentity値を確認できる機会がありますか?
はい
ID値の割り当ては、含まれているユーザートランザクションとは無関係です。これは、トランザクションがロールバックされてもID値が消費される1つの理由です。インクリメント操作自体は、破損を防ぐためにラッチによって保護されていますが、それが保護の範囲です。
実装の特定の状況では、ID割り当て(CMEDSeqGen::GenerateNewValue
の呼び出し)は、挿入のユーザートランザクションがアクティブになる前に行われる(などの前に)すべてのロックが取得されます)。
デバッガーを接続して2つの挿入を同時に実行し、ID値がインクリメントされて割り当てられた直後に1つのスレッドをフリーズできるようにすることで、次のようなシナリオを再現できました。
手順3の後で、コミット読み取りロックの下でrow_numberを使用したクエリは、次を返しました。
実装では、これによりチェックポイントID 3が誤ってスキップされます。
機会の窓は比較的小さいですが、それは存在します。デバッガーを接続するよりも現実的なシナリオを提供するには:実行中のクエリスレッドは、上記の手順1の後にスケジューラーを生成できます。これにより、元のスレッドが挿入の実行を再開する前に、2番目のスレッドがID値を割り当て、挿入してコミットできます。
明確にするために、ID値が割り当てられてから使用されるまでは、ID値を保護するロックやその他の同期オブジェクトはありません。たとえば、上記のステップ1の後、並行トランザクションは、行がテーブルに存在する前に(コミットされていない場合でも)、IDENT_CURRENT
などのT-SQL関数を使用して新しいID値を確認できます。
基本的に、アイデンティティ値に関しては documented 以外の保証はありません。
それは本当にそれです。
strictトランザクションFIFO処理が必要な場合は、手動でシリアル化する以外に選択肢がない可能性があります。アプリケーションの要件がそれほど重要でない場合は、オプション。その点に関しては、質問が100%明確ではありませんが、それでも、Remus Rusanuの記事 キューとしてのテーブルの使用 に役立つ情報が見つかるかもしれません。
ポールホワイトが完全に正しいと回答したため、一時的に「スキップされた」ID行が発生する可能性があります。これは、このケースを独自に再現するための小さなコードです。
データベースとテストテーブルを作成します。
create database IdentityTest
go
use IdentityTest
go
create table dbo.IdentityTest (ID int identity, c1 char(10))
create clustered index CI_dbo_IdentityTest_ID on dbo.IdentityTest(ID)
C#コンソールプログラムでこのテーブルに対して同時挿入と選択を実行します。
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Threading;
namespace IdentityTest
{
class Program
{
static void Main(string[] args)
{
var insertThreads = new List<Thread>();
var selectThreads = new List<Thread>();
//start threads for infinite inserts
for (var i = 0; i < 100; i++)
{
insertThreads.Add(new Thread(InfiniteInsert));
insertThreads[i].Start();
}
//start threads for infinite selects
for (var i = 0; i < 10; i++)
{
selectThreads.Add(new Thread(InfiniteSelectAndCheck));
selectThreads[i].Start();
}
}
private static void InfiniteSelectAndCheck()
{
//infinite loop
while (true)
{
//read top 2 IDs
var cmd = new SqlCommand("select top(2) ID from dbo.IdentityTest order by ID desc")
{
Connection = new SqlConnection("Server=localhost;Database=IdentityTest;Integrated Security=SSPI;Application Name=IdentityTest")
};
try
{
cmd.Connection.Open();
var dr = cmd.ExecuteReader();
//read first row
dr.Read();
var row1 = int.Parse(dr["ID"].ToString());
//read second row
dr.Read();
var row2 = int.Parse(dr["ID"].ToString());
//write line if row1 and row are not consecutive
if (row1 - 1 != row2)
{
Console.WriteLine("row1=" + row1 + ", row2=" + row2);
}
}
finally
{
cmd.Connection.Close();
}
}
}
private static void InfiniteInsert()
{
//infinite loop
while (true)
{
var cmd = new SqlCommand("insert into dbo.IdentityTest (c1) values('a')")
{
Connection = new SqlConnection("Server=localhost;Database=IdentityTest;Integrated Security=SSPI;Application Name=IdentityTest")
};
try
{
cmd.Connection.Open();
cmd.ExecuteNonQuery();
}
finally
{
cmd.Connection.Close();
}
}
}
}
}
このコンソールは、読み取りスレッドの1つがエントリを「見逃した」場合に、すべてのケースについて行を出力します。
ギャップを残す可能性のあるシナリオは多数あるため、IDが連続しているとは期待しないでください。アイデンティティを抽象的な番号のように考慮し、ビジネス上の意味を付けない方が良いでしょう。
基本的に、INSERT操作をロールバック(または明示的に行を削除)するとギャップが発生し、テーブルプロパティIDENTITY_INSERTをONに設定すると重複が発生する可能性があります。
次の場合にギャップが発生する可能性があります。
列のIDプロパティは保証されていません。
•独自性
•トランザクション内の連続する値。値が連続している必要がある場合、トランザクションはテーブルで排他ロックを使用するか、SERIALIZABLE分離レベルを使用する必要があります。
•サーバーの再起動後の連続した値。
•値の再利用。
これが原因でID値を使用できない場合は、現在の値を保持する別のテーブルを作成し、テーブルへのアクセスとアプリケーションでの番号割り当てを管理します。これは、パフォーマンスに影響を与える可能性があります。
https://msdn.Microsoft.com/en-us/library/ms186775(v = sql.105).aspx
https://msdn.Microsoft.com/en-us/library/ms186775(v = sql.110).aspx
サーバーに高負荷がかかると状況が悪化することもあると思います。次の2つのトランザクションを検討してください。
上記のシナリオでは、LAST_READ_IDは6になるため、5が読み取られることはありません。
このスクリプトを実行する:
BEGIN TRAN;
INSERT INTO dbo.Example DEFAULT VALUES;
COMMIT;
以下は、拡張イベントセッションによってキャプチャされたときに取得および解放されたロックです。
name timestamp associated_object_id mode object_id resource_type session_id resource_description
lock_acquired 2016-03-29 06:37:28.9968693 1585440722 IX 1585440722 OBJECT 51
lock_acquired 2016-03-29 06:37:28.9969268 7205759890195415040 IX 0 PAGE 51 1:1235
lock_acquired 2016-03-29 06:37:28.9969306 7205759890195415040 RI_NL 0 KEY 51 (ffffffffffff)
lock_acquired 2016-03-29 06:37:28.9969330 7205759890195415040 X 0 KEY 51 (29cf3326f583)
lock_released 2016-03-29 06:37:28.9969579 7205759890195415040 X 0 KEY 51 (29cf3326f583)
lock_released 2016-03-29 06:37:28.9969598 7205759890195415040 IX 0 PAGE 51 1:1235
lock_released 2016-03-29 06:37:28.9969607 1585440722 IX 1585440722 OBJECT 51
作成される新しい行のXキーロックの直前に取得されたRI_N KEYロックに注意してください。この有効期間の短いロックは、RI_Nロックに互換性がないため、同時挿入が別のRI_N KEYロックを取得するのを防ぎます。新しく生成されたキーの行ロックの前に範囲ロックが取得されるため、ステップ2と3の間に述べたウィンドウは問題ではありません。
あなたのSELECT...ORDER BY
は、目的の新しく挿入された行の前にスキャンを開始します。デフォルトのREAD COMMITTED
データベースが存在する限り、分離レベルREAD_COMMITTED_SNAPSHOT
オプションがオフになっています。
SQL Serverについての私の理解から、デフォルトの動作では、最初のクエリがコミットされるまで、2番目のクエリは結果を表示しません。最初のクエリがCOMMITではなくROLLBACKを実行する場合、列にIDがありません。
次の構造のデータベーステーブルを作成しました。
CREATE TABLE identity_rc_test (
ID4VALUE INT IDENTITY (1,1),
TEXTVALUE NVARCHAR(20),
CONSTRAINT PK_ID4_VALUE_CLUSTERED
PRIMARY KEY CLUSTERED (ID4VALUE, TEXTVALUE)
)
次のステートメントを使用して、データベースの分離レベルを確認しました。
SELECT snapshot_isolation_state,
snapshot_isolation_state_desc,
is_read_committed_snapshot_on
FROM sys.databases WHERE NAME = 'mydatabase'
これは私のデータベースに対して次の結果を返しました:
snapshot_isolation_state snapshot_isolation_state_desc is_read_committed_snapshot_on
0 OFF 0
(これはSQL Server 2012のデータベースのデフォルト設定です)
次のスクリプトは、標準のSQL Server SSMSクライアント設定と標準のSQL Server設定を使用して実行されました。
クライアントは、SSMSのクエリオプションに従ってトランザクション分離レベルREAD COMMITTED
を使用するように設定されています。
次のクエリは、SPID 57のクエリウィンドウで実行されました
SELECT * FROM dbo.identity_rc_test
BEGIN TRANSACTION [FIRST_QUERY]
INSERT INTO dbo.identity_rc_test (TEXTVALUE) VALUES ('Nine')
/* Commit is commented out to prevent the INSERT from being commited
--COMMIT TRANSACTION [FIRST_QUERY]
--ROLLBACK TRANSACTION [FIRST_QUERY]
*/
次のクエリは、SPID 58のクエリウィンドウで実行されました
BEGIN TRANSACTION [SECOND_QUERY]
INSERT INTO dbo.identity_rc_test (TEXTVALUE) VALUES ('Ten')
COMMIT TRANSACTION [SECOND_QUERY]
SELECT * FROM dbo.identity_rc_test
クエリは完了せず、ページで排他ロックが解放されるのを待っています。
このスクリプトは、2つのトランザクションのデータベースオブジェクトで発生するロックを表示します。
SELECT request_session_id, resource_type,
resource_description,
resource_associated_entity_id,
request_mode, request_status
FROM sys.dm_tran_locks
WHERE request_session_id IN (57, 58)
そしてここに結果があります:
58 DATABASE 0 S GRANT
57 DATABASE 0 S GRANT
58 PAGE 1:79 72057594040549300 IS GRANT
57 PAGE 1:79 72057594040549300 IX GRANT
57 KEY (a0aba7857f1b) 72057594040549300 X GRANT
58 KEY (a0aba7857f1b) 72057594040549300 S WAIT
58 OBJECT 245575913 IS GRANT
57 OBJECT 245575913 IX GRANT
結果は、クエリウィンドウ1(SPID 57)に、データベースの共有ロック(S)、オブジェクトの意図的排他(IX)ロック、挿入先のページの意図的排他(IX)ロック、および排他的ロックがあることを示しています。挿入されているがまだコミットされていないKEYのロック(X)。
コミットされていないデータのため、2番目のクエリ(SPID 58)には、DATABASEレベルの共有ロック(S)、OBJECTの意図的共有(IS)ロック、ページの意図的共有(IS)ロック、共有(S )要求ステータスWAITでKEYをロックします。
最初のクエリウィンドウのクエリは、コミットせずに実行されます。 2番目のクエリはREAD COMMITTED
データのみを送信できるため、タイムアウトが発生するか、最初のクエリでトランザクションがコミットされるまで待機します。
これは、Microsoft SQL Serverのデフォルトの動作を理解しているからです。
最初のステートメントがCOMMITする場合、SELECTステートメントによる後続の読み取りでは、IDが実際に順番に並んでいることに注意してください。
最初のステートメントがROLLBACKを実行すると、シーケンス内に欠落しているIDが見つかりますが、IDは昇順のままです(ID列にデフォルトまたはASCオプションを使用してINDEXを作成した場合)。
更新:
(率直に)はい、問題が発生するまで、ID列が正しく機能していることを信頼できます。 MicrosoftのWebサイトには SQL Server 2000とID列に関するHOTFIX が1つだけあります。
ID列が正しく更新されることが信頼できない場合は、MicrosoftのWebサイトにもっと多くの修正プログラムやパッチがあると思います。
マイクロソフトサポート契約を結んでいる場合は、常にアドバイザリーケースを開いて追加情報を求めることができます。