web-dev-qa-db-ja.com

非クラスター化インデックスで異なる行を更新するときのデッドロック

デッドロックの問題を解決していますが、idフィールドでクラスター化インデックスと非クラスター化インデックスを使用すると、ロックの動作が異なることに気付きました。 clustedインデックスまたは主キーがidフィールドに適用されている場合、デッドロックの問題は解決されたようです。

さまざまな行に対して1つ以上の更新を行うさまざまなトランザクションがあります。トランザクションAはID = aの行のみを更新し、tx BはID = bの行のみを更新します。

そして、インデックスがない場合、更新はすべての行の更新ロックを取得し、必要に応じて排他ロックに変換し、最終的にデッドロックにつながることを理解しています。しかし、非クラスター化インデックスを使用すると、デッドロックがまだ存在する理由がわかりません(ただし、ヒット率は低下しているようです)。

データ表:

CREATE TABLE [dbo].[user](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [userName] [nvarchar](255) NULL,
    [name] [nvarchar](255) NULL,
    [phone] [nvarchar](255) NULL,
    [password] [nvarchar](255) NULL,
    [ip] [nvarchar](30) NULL,
    [email] [nvarchar](255) NULL,
    [pubDate] [datetime] NULL,
    [todoOrder] [text] NULL
)

デッドロックトレース

deadlock-list
deadlock victim=process4152ca8
process-list
process id=process4152ca8 taskpriority=0 logused=0 waitresource=RID: 5:1:388:29 waittime=3308 ownerId=252354 transactionname=user_transaction lasttranstarted=2014-04-11T00:15:30.947 XDES=0xb0bf180 lockMode=U schedulerid=3 kpid=11392 status=suspended spid=57 sbid=0 ecid=0 priority=0 trancount=2 lastbatchstarted=2014-04-11T00:15:30.953 lastbatchcompleted=2014-04-11T00:15:30.950 lastattention=1900-01-01T00:00:00.950 clientapp=.Net SqlClient Data Provider hostname=BOOD-PC hostpid=9272 loginname=getodo_sql isolationlevel=read committed (2) xactid=252354 currentdb=5 lockTimeout=4294967295 clientoption1=671088672 clientoption2=128056
executionStack
frame procname=adhoc line=1 stmtstart=62 sqlhandle=0x0200000062f45209ccf17a0e76c2389eb409d7d970b0f89e00000000000000000000000000000000
update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@owner
frame procname=unknown line=1 sqlhandle=0x00000000000000000000000000000000000000000000000000000000000000000000000000000000
unknown
inputbuf
(@para0 nvarchar(2)<c/>@owner int)update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@owner
process id=process4153468 taskpriority=0 logused=4652 waitresource=KEY: 5:72057594042187776 (3fc56173665b) waittime=3303 ownerId=252344 transactionname=user_transaction lasttranstarted=2014-04-11T00:15:30.920 XDES=0x4184b78 lockMode=U schedulerid=3 kpid=7272 status=suspended spid=58 sbid=0 ecid=0 priority=0 trancount=2 lastbatchstarted=2014-04-11T00:15:30.960 lastbatchcompleted=2014-04-11T00:15:30.960 lastattention=1900-01-01T00:00:00.960 clientapp=.Net SqlClient Data Provider hostname=BOOD-PC hostpid=9272 loginname=getodo_sql isolationlevel=read committed (2) xactid=252344 currentdb=5 lockTimeout=4294967295 clientoption1=671088672 clientoption2=128056
executionStack
frame procname=adhoc line=1 stmtstart=60 sqlhandle=0x02000000d4616f250747930a4cd34716b610a8113cb92fbc00000000000000000000000000000000
update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@uid
frame procname=unknown line=1 sqlhandle=0x00000000000000000000000000000000000000000000000000000000000000000000000000000000
unknown
inputbuf
(@para0 nvarchar(61)<c/>@uid int)update [user] WITH (ROWLOCK) set [todoOrder]=@para0 where id=@uid
resource-list
ridlock fileid=1 pageid=388 dbid=5 objectname=SQL2012_707688_webows.dbo.user id=lock3f7af780 mode=X associatedObjectId=72057594042122240
owner-list
owner id=process4153468 mode=X
waiter-list
waiter id=process4152ca8 mode=U requestType=wait
keylock hobtid=72057594042187776 dbid=5 objectname=SQL2012_707688_webows.dbo.user indexname=10 id=lock3f7ad700 mode=U associatedObjectId=72057594042187776
owner-list
owner id=process4152ca8 mode=U
waiter-list
waiter id=process4153468 mode=U requestType=wait

また、興味深い関連する可能性のある発見は、クラスター化インデックスと非クラスター化インデックスのロック動作が異なるように見えることです。

クラスター化インデックスを使用する場合、予想される更新時に、キーに排他ロックとRIDに排他ロックがあります。一方、非クラスター化インデックスが使用されている場合、2つの異なるRIDに2つの排他ロックがあるため、混乱します。

誰かがこれについても理由を説明できると助かります。

SQLのテスト:

use SQL2012_707688_webows;
begin transaction;
update [user] with (rowlock) set todoOrder='{1}' where id = 63501
exec sp_lock;
commit;

クラスタ化インデックスとしてIDを使用:

spid    dbid    ObjId   IndId   Type    Resource    Mode    Status
53  5   917578307   1   KEY (b1a92fe5eed4)                      X   GRANT
53  5   917578307   1   PAG 1:879                               IX  GRANT
53  5   917578307   1   PAG 1:1928                              IX  GRANT
53  5   917578307   1   RID 1:879:7                             X   GRANT

非クラスター化インデックスとしてのID

spid    dbid    ObjId   IndId   Type    Resource    Mode    Status
53  5   917578307   0   PAG 1:879                               IX  GRANT
53  5   917578307   0   PAG 1:1928                              IX  GRANT
53  5   917578307   0   RID 1:879:7                             X   GRANT
53  5   917578307   0   RID 1:1928:18                           X   GRANT

EDIT1:インデックスなしのデッドロックの詳細
2つのtx AとBがあり、それぞれに2つの更新ステートメントがあり、もちろん異なる行があるとします
tx A

update [user] with (rowlock) set todoOrder='{1}' where id = 63501
update [user] with (rowlock) set todoOrder='{2}' where id = 63501

tx B

update [user] with (rowlock) set todoOrder='{3}' where id = 63502
update [user] with (rowlock) set todoOrder='{4}' where id = 63502

{1}と{4}はデッドロックの可能性があります。

{1}では、テーブルスキャンを実行する必要があるため、行63502でUロックが要求され、条件に一致するため、行63501でXロックが保持されている可能性があります。

{4}で、行63501に対してUロックが要求され、Xロックは63502に対してすでに保持されています

したがって、txAは63501を保持して63502を待機し、txBは63502を待機して63502を待機します。これはデッドロックです。

EDIT2:テストケースのバグが原因でここで状況が異なる混乱して申し訳ありませんが、バグにより状況が異なり、最終的にデッドロックが発生するようです。

この場合、ポールの分析が本当に私を助けてくれたので、それを答えとして受け入れます。

テストケースのバグが原因で、2つのトランザクションtxAとtxBが同じ行を次のように更新できます。

tx A

update [user] with (rowlock) set todoOrder='{1}' where id = 63501
update [user] with (rowlock) set todoOrder='{2}' where id = 63501

tx B

update [user] with (rowlock) set todoOrder='{3}' where id = 63501

{2}および{3}は、次の場合にデッドロックの可能性があります。

txAは、RIDのXロックを保持している間、キーのUロックを要求します({1}の更新により)txBは、キーのUロックを保持している間、RIDのUロックを要求します

13
Bood

...なぜクラスタ化インデックスを使用しても、デッドロックがまだ存在します(ヒット率は低下しているようですが)

質問は正確には明確ではありませんが(たとえば、各トランザクションに含まれる更新の数とid値)、1つのトランザクション内で複数の単一行の更新が発生すると、1つの明らかなデッドロックシナリオが発生します。 _[id]_の値、およびIDは別の_[id]_の順序で更新されます。

_[T1]: Update id 2; Update id 1;
[T2]: Update id 1; Update id 2;
_

デッドロックシーケンス:T1(u2)、T2(u1)、T1(u1)wait、T2(u2)待機

このデッドロックシーケンスは、各トランザクション内でID順に厳密に更新することで回避できます(同じパスで同じ順序でロックを取得する)。

クラスタ化インデックスを使用する場合、更新時に、キーに排他ロックとRIDに排他ロックがありますが、これは予想されたものです。一方、非クラスター化インデックスが使用されている場合、2つの異なるRIDに2つの排他ロックがあるため、混乱します。

idに一意のクラスター化インデックスを使用すると、クラスター化キーに排他ロックが適用され、行内データへの書き込みが保護されます。 LOB RID列への書き込みを保護するには、別のtext排他ロックが必要です。これは、デフォルトで別のデータページに格納されます。

テーブルがidに非クラスター化インデックスのみを持つヒープである場合、2つのことが起こります。まず、1つのRID排他ロックはヒープ内行データに関連し、もう1つは以前と同様にLOBデータのロックです。 2番目の効果は、より複雑な実行計画が必要になることです。

クラスター化インデックスと単純な単一値の等価述語の更新により、クエリプロセッサは、単一のパスを使用して、単一の演算子で更新(読み取りと書き込み)を実行する最適化を適用できます。

Single-operator update

行は単一のシーク操作で検索および更新され、排他ロックのみが必要です(更新ロックは必要ありません)。サンプルテーブルを使用したロックシーケンスの例:

_acquiring IX lock on OBJECT: 6:992930809:0 -- TABLE
acquiring IX lock on PAGE: 6:1:59104 -- INROW
acquiring X lock on KEY: 6:72057594233618432 (61a06abd401c) -- INROW
acquiring IX lock on PAGE: 6:1:59091 -- LOB
acquiring X lock on RID: 6:1:59091:1 -- LOB

releasing lock reference on PAGE: 6:1:59091 -- LOB
releasing lock reference on RID: 6:1:59091:1 -- LOB
releasing lock reference on KEY: 6:72057594233618432 (61a06abd401c) -- INROW
releasing lock reference on PAGE: 6:1:59104 -- INROW
_

非クラスター化インデックスのみでは、1つのbツリー構造から読み取り、別のbツリー構造を書き込む必要があるため、同じ最適化を適用できません。マルチパス計画には、個別の読み取りフェーズと書き込みフェーズがあります。

Multi-iterator update

これは、読み取り時に更新ロックを取得し、行が適格である場合は排他ロックに変換します。指定されたスキーマを使用したロックシーケンスの例:

_acquiring IX lock on OBJECT: 6:992930809:0 -- TABLE
acquiring IU lock on PAGE: 6:1:59105 -- NC INDEX
acquiring U lock on KEY: 6:72057594233749504 (61a06abd401c) -- NC INDEX
acquiring IU lock on PAGE: 6:1:59104 -- HEAP
acquiring U lock on RID: 6:1:59104:1 -- HEAP
acquiring IX lock on PAGE: 6:1:59104 -- HEAP convert to X
acquiring X lock on RID: 6:1:59104:1 -- HEAP convert to X
acquiring IU lock on PAGE: 6:1:59091 -- LOB
acquiring U lock on RID: 6:1:59091:1 -- LOB

releasing lock reference on PAGE: 6:1:59091 
releasing lock reference on RID: 6:1:59091:1
releasing lock reference on RID: 6:1:59104:1
releasing lock reference on PAGE: 6:1:59104 
releasing lock on KEY: 6:72057594233749504 (61a06abd401c)
releasing lock on PAGE: 6:1:59105 
_

LOBデータはreadであり、テーブル更新イテレーターで書き込まれることに注意してください。より複雑な計画と複数の読み取りおよび書き込みパスは、デッドロックの可能性を高めます。

最後に、テーブルの定義で使用されているデータ型に注意せざるを得ません。新しい作業に非推奨のtextデータ型を使用しないでください。代わりに、この列に最大2GBのデータを保存する機能が本当に必要な場合は、varchar(max)を使用します。 textvarchar(max)の重要な違いの1つは、textデータはデフォルトで行外に格納され、varchar(max)はデフォルトで行内に格納されることです。

柔軟性が必要な場合にのみUnicodeタイプを使用してください(たとえば、IPアドレスがUnicodeを必要とする理由がわかりにくいなど)。また、属性に適切な長さ制限を選択してください-どこでも255は正しくないようです。

追加の読み:
デッドロックとライブロックの共通パターン
Bart Duncanのデッドロックトラブルシューティングシリーズ

ロックのトレースは、さまざまな方法で実行できます。 SQL Server Express with Advanced Services2014&2012 SP1以降のみ)にはが含まれますプロファイラツール。これは、ロックの取得と解放の詳細を表示するためにサポートされている方法です。

16
Paul White 9