7

SO を閲覧しているときに、まだ存在しないレコードを挿入するための「最良の」アプローチについて、次の質問/ディスカッションを見つけました。私を驚かせた声明の 1 つは、[Remus Rusanu] の次のような発言でした。

どちらのバリアントも正しくありません。重複する @value1、@value2、保証のペアを挿入します。

チェックがINSERTから「分離」されている構文については同意しますが(明示的なロック/トランザクション管理は存在しません)。このような他の提案された構文にこれが当てはまる理由と時期を理解するのに苦労しています

INSERT INTO mytable (x)
SELECT @x WHERE NOT EXISTS (SELECT * FROM mytable WHERE x = @x);

私は(別の)何が最善/最速の議論を始めたくありませんし、構文が一意のインデックス/制約(またはPK)を「置き換える」ことができるとは思いませんが、この構造がどのような状況でダブルスを引き起こす可能性があるかを本当に知る必要があります.過去にこの構文を使用しており、今後も使用し続けるのは安全ではないのではないかと考えています。

私が思うに、INSERT と SELECT は両方とも同じ (暗黙の) トランザクション内にあるということです。クエリは、関連するレコード (キー) に IX ロックを取得し、クエリ全体が終了するまで解放しないため、レコードが挿入された後にのみ解放されます。このロックは、挿入が完了するまで他のすべての接続がロックを取得できないため、他のすべての接続が同じ INSERT を作成するのをブロックします。そうして初めて、彼らはロックを取得し、レコードが既に存在するかどうかを自分で確認し始めます。

私見では、調べる最善の方法はテストすることなので、ラップトップで次のコードをしばらく実行しています。

テーブルを作成

CREATE TABLE t_test (x int NOT NULL PRIMARY KEY (x))

以下は、非常に多くの接続を並行して実行します)

SET NOCOUNT ON

WHILE 1 = 1
    BEGIN
        INSERT t_test (x)
        SELECT x = DatePart(ms, CURRENT_TIMESTAMP)
         WHERE NOT EXISTS ( SELECT *
                              FROM t_test old
                             WHERE old.x = DatePart(ms, CURRENT_TIMESTAMP) )
    END

これまでのところ、注意すべき点は次のとおりです。

  • エラーは発生していません (まだ)
  • CPU がかなり熱くなっています =)
  • テーブルはすぐに 300 レコードを保持しました (datetime の 3 ミリ秒の「精度」のため)。

アップデート:

上記の私の例は、私が意図したことをしていないことがわかりました。複数の接続が同じレコードを同時に挿入しようとする代わりに、最初の 1 秒後に既存のレコードを挿入しないようにしました。次の接続でクエリをコピーして貼り付けて実行するのにおそらく約1秒かかったため、重複の危険はありませんでした。一日中、ロバの耳をかぶっています...

とにかく、私は目前の問題に沿ったものになるようにテストを調整しました(同じ表を使用)

SET NOCOUNT ON

DECLARE @midnight datetime
SELECT @midnight = Convert(datetime, Convert(varchar, CURRENT_TIMESTAMP, 106), 106)

WHILE 1 = 1
    BEGIN
        INSERT t_test (x)
        SELECT x = DateDiff(ms, @midnight, CURRENT_TIMESTAMP)
         WHERE NOT EXISTS ( SELECT *
                              FROM t_test old
                             WHERE old.x = DateDiff(ms, @midnight, CURRENT_TIMESTAMP))
    END

そして、見よ、出力ウィンドウには、次の行に沿って多くのエラーが表示されるようになりました

メッセージ 2627、レベル 14、状態 1、行 8 PRIMARY KEY 制約 'PK__t_test__3BD019E521C3B7EE' の違反。>重複したキーをオブジェクト 'dbo.t_test' に挿入できません。重複キーの値は (57581873) です。

参考: Andomar が指摘したように、HOLDLOCK および/または SERIALIZABLE ヒントを追加すると、問題は実際に「解決」されますが、その後、多くのデッドロックが発生することが判明しました。

やるべきコードレビューがかなりあると思います...


このコードも同じでできますか.htaccess

RewriteEngine on
RewriteCond %{REMOTE_HOST}   \.amazonaws\. [NC]
RewriteRule ^ - [F]
4

2 に答える 2

4

別の質問を投稿していただきありがとうございます。あなたにはいくつかの誤解があります:

クエリは、関連するレコード (キー) に対して IX ロックを取得し、クエリ全体が完了するまで解放しません。

INSERT は、挿入された行をロックし、X ロックします (IX のようなインテント ロックは、ロック階層の親エンティティに対してのみ要求でき、レコードに対しては要求できません)。このロックは、トランザクションがコミットされるまで保持する必要があります (厳密な2 フェーズ ロックでは、常に X ロックをトランザクションの最後にのみ解放する必要があります)。

INSERT によって取得されたロックは、同じキーであってもそれ以上の挿入をブロックしないことに注意してください。重複を防ぐ唯一の方法は一意のインデックスであり、一意性を強制するメカニズムはロック ベースではありません。はい、主キーでは、その一意性により重複は防止されますが、ロックが役割を果たしたとしても、作用する力は異なります。

あなたの例では、新しく挿入された行での X 対 S ロックの競合により、SELECT が INSERT でブロックされるため、操作がシリアル化されます。考慮すべきもう 1 つの考えは、タイプ INT の 300 レコードが 1 ページに収まり、多くの最適化が開始され (たとえば、複数のシークの代わりにスキャンを使用する)、テスト結果が変更されることです。多くの肯定的で証拠のない仮説は、まだ推測に過ぎないことを忘れないでください...

問題をテストするには、INSERT が並行 SELECT をブロックしないことを確認する必要があります。RCSI またはスナップショット分離で実行することは、これを達成するための 1 つの方法です (本番環境で意図せず「達成」し、上記のすべての仮定を行ったアプリを壊す可能性があります...) WHERE 句は別の方法です。非常に大きなテーブルとセカンダリ インデックスは、さらに別の方法です。

だからここに私がそれをテストした方法があります:

set nocount on;
go

drop database test;
go

create database test;
go

use test;
go

create table test (id int primary key, filler char(200));
go

-- seed 10000 values, fill some pages
declare @i int = 0;
begin transaction
while @i < 10000
begin
    insert into test (id) values (@i);
    set @i += 1;
end
commit;

これをいくつかの並列接続から実行します(私は3つを使用しました):

use test;
go

set nocount on;
go

declare @i int;
while (1=1)
begin
    -- This is not cheating. This ensures that many concurrent SELECT attempt 
    -- to insert the same values, and all of them believe the values are 'free'
    select @i = max(id) from test with (readpast);
    insert into test (id)
    select id
        from (values (@i), (@i+1), (@i+2), (@i+3), (@i+4), (@i+5)) as t(id)
        where t.id not in (select id from test);
end

以下にいくつかの結果を示します。

Msg 2627, Level 14, State 1, Line 6
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130076).
The statement has been terminated.
Msg 2627, Level 14, State 1, Line 6
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130096).
The statement has been terminated.
Msg 2627, Level 14, State 1, Line 6
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130106).
The statement has been terminated.
Msg 2627, Level 14, State 1, Line 6
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130121).
The statement has been terminated.
Msg 2627, Level 14, State 1, Line 6
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130141).
The statement has been terminated.
Msg 2627, Level 14, State 1, Line 6
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130151).
The statement has been terminated.
Msg 2627, Level 14, State 1, Line 6
Violation of PRIMARY KEY constraint 'PK__test__3213E83FD9281543'. Cannot insert duplicate key in object 'dbo.test'. The duplicate key value is (130176).
The statement has been terminated.
Msg 2627, Level 14, State 1, Line 6

ロックがあっても、スナップショット分離も RCSI もありません。各 SELECT が @i+1...@i+5 を挿入しようとすると、値が存在しないことがすべて検出され、INSERT に進みます。1 つの幸運な勝者が成功し、残りはすべて PK 違反を引き起こします。頻繁に。意図的にを使用して@i=MAX(id)、競合の追跡を劇的に増やしましたが、それは必須ではありません。すべての違反が値 %5+1 で発生する理由を理解する問題は、演習として残します。

于 2013-09-30T14:05:37.163 に答える
3

単一の接続からテストしているため、並行性をまったくテストしていません。異なるウィンドウからスクリプトを 2 回実行すると、競合が発生し始めます。

競合には複数の理由があります。

  • デフォルトでは、(暗黙の) トランザクションが終了するまでロックは保持されません。with (holdlock)この動作を変更するには、クエリ ヒントを使用します。
  • クエリの同時実行の問題は、「ファントム読み取り」と呼ばれます。デフォルトのトランザクション分離レベルは「読み取りコミット」であり、ファントム読み取りから保護されません。クエリ ヒントを使用しwith (serializable)て分離レベルを上げます。(接続が接続プールに戻されるときに分離レベルがクリアされないset transaction isolation levelため、このコマンドは使用しないようにしてください。)

主キー制約は常に適用されます。そのため、クエリは重複行を挿入しようとし、重複キー エラーをスローして失敗します。

適切な方法は、クエリを使用して (99% の確率で動作します)、クライアントが時折発生する重複キーの例外を適切に処理するようにすることです。

ウィキペディアには、分離レベルの優れた説明があります

于 2013-09-30T13:45:38.410 に答える