1

読み取りコミットされた分離レベルを利用してトランザクションを処理する場合、可能ではないと思われる問題に遭遇したことを理解しようとしています。キューとして使用されているテーブルがあります。1つのスレッド(接続1)で、20レコードの複数のバッチを各テーブルに挿入します。20レコードの各バッチは、トランザクション内で実行されます。2番目のスレッド(接続2)で、更新を実行して、キューに挿入されたレコードのステータスを変更します。これは、トランザクション内でも発生します。同時に実行する場合、接続1はトランザクション内の20行のバッチでテーブル挿入に行を挿入するため、更新(接続2)の影響を受ける行数は20の倍数である必要があります。

しかし、私のテストでは、これが常に当てはまるとは限らず、接続1のバッチからレコードのサブセットを更新できる場合があります。これは可能でしょうか、それともトランザクション、同時実行性、分離レベルについて何かが足りないのでしょうか?以下は、T-SQLでこの問題を再現するために作成した一連のテストスクリプトです。

このスクリプトは、20のトランザクションバッチで20,000レコードをテーブルに挿入します。

USE ReadTest
GO

SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO

SET NOCOUNT ON

DECLARE @trans_id INTEGER
DECLARE @cmd_id INTEGER
DECLARE @text_str VARCHAR(4000)

SET @trans_id = 0
SET @text_str = 'Placeholder String Value'                

-- First empty the table
DELETE FROM TABLE_A

WHILE @trans_id < 1000 BEGIN
    SET @trans_id = @trans_id + 1
    SET @cmd_id = 0

    BEGIN TRANSACTION
--  Insert 20 records into the table per transaction
    WHILE @cmd_id < 20 BEGIN
        SET @cmd_id = @cmd_id + 1

        INSERT INTO TABLE_A ( transaction_id, command_id, [type], status, text_field ) 
            VALUES ( @trans_id, @cmd_id, 1, 1,  @text_str )
    END             
    COMMIT

END

PRINT 'DONE'

このスクリプトは、テーブル内のレコードを更新し、ステータスを1から2に変更してから、更新操作からの行数をチェックします。行数が20の倍数ではなく、printステートメントがこれと影響を受ける行数を示している場合。

USE ReadTest
GO

SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO

SET NOCOUNT ON
DECLARE @loop_counter INTEGER
DECLARE @trans_id INTEGER
DECLARE @count INTEGER

SET @loop_counter = 0

WHILE @loop_counter < 100000 BEGIN

    SET @loop_counter = @loop_counter + 1
    BEGIN TRANSACTION
        UPDATE TABLE_A SET status = 2 
        WHERE status = 1
            and type = 1
        SET @count = @@ROWCOUNT
    COMMIT

    IF ( @count % 20 <> 0 ) BEGIN
--      Records in concurrent transaction inserting in batches of 20 records before commit.
        PRINT '*** Rowcount not a multiple of 20. Count = ' + CAST(@count AS VARCHAR) + ' ***'
    END

    IF @count > 0 BEGIN
--      Delete the records where the status was changed.
        DELETE TABLE_A WHERE status = 2
    END
END

PRINT 'DONE'

このスクリプトは、ReadTestという新しいデータベースにテストキューテーブルを作成します。

USE master;
GO

IF EXISTS (SELECT * FROM sys.databases WHERE name = 'ReadTest')
  BEGIN;
  DROP DATABASE ReadTest;
  END;
GO

CREATE DATABASE ReadTest;
GO

ALTER DATABASE ReadTest
SET ALLOW_SNAPSHOT_ISOLATION OFF
GO

ALTER DATABASE ReadTest
SET READ_COMMITTED_SNAPSHOT OFF
GO

USE ReadTest
GO

CREATE TABLE [dbo].[TABLE_A](
    [ROWGUIDE] [uniqueidentifier] NOT NULL,
    [TRANSACTION_ID] [int] NOT NULL,
    [COMMAND_ID] [int] NOT NULL,
    [TYPE] [int] NOT NULL,
    [STATUS] [int] NOT NULL,
    [TEXT_FIELD] [varchar](4000) NULL
 CONSTRAINT [PK_TABLE_A] PRIMARY KEY NONCLUSTERED 
(
    [ROWGUIDE] ASC
) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [dbo].[TABLE_A] ADD  DEFAULT (newsequentialid()) FOR [ROWGUIDE]
GO
4

2 に答える 2

2

あなたの期待は完全に見当違いです。正確に 20 行を「デキュー」するという要件をクエリで表現したことはありません。statusUPDATEは0、19、20、21、または 1000 行を返すことができ、すべての結果は正しいものtypeです。どういうわけかあなたの質問では避けられましたが、明示的に述べられたことはありません)、「デキュー」操作にはORDER BY句が含まれている必要があります。このような明示的な要件を追加した場合、「デキュー」が常に「エンキュー」行のバッチ全体 (つまり、20 行の倍数) を返すという期待は、合理的な期待に一歩近づくことになります。現状では、私が言ったように、完全に見当違いです。

より長い議論については、Using Tables as Queues を参照してください。

1 つのトランザクションが 20 個の挿入されたレコードのバッチをコミットしている間、別の同時トランザクションはそれらのレコードのサブセットのみを更新でき、20 個すべてを更新できないことを心配する必要はありませんか?

基本的に、質問は、INSERT 中に SELECT すると、挿入された行がいくつ表示されますか? . 分離レベルが SERIALIZABLE として宣言されている場合にのみ、懸念する権利があります。他の分離レベルでは、UPDATE の実行中に挿入された行数が表示されることを予測することはできません。SERIALIZABLE のみが、結果が 2 つのステートメントを次々に実行した場合と同じでなければならないことを示しています (つまり、シリアル化されているため、この名前が付けられています)。方法の技術的な詳細UPDATE は、INSERT バッチの一部のみを「参照」します。物理的な順序と ORDER BY 句の欠如を考慮すると、説明は関係ありません。基本的な問題は、期待が保証されていないということです。適切な ORDER BY と適切なクラスター化インデックス キーを追加することで「問題」が「修正」されたとしても (上記のリンク先の記事で詳細が説明されています)、期待は依然として保証されません。UPDATE が 1 行、19 行、または 21 行を「参照」することは依然として完全に合法ですが、発生する可能性は低いでしょう。

READ COMMITTED はコミットされたデータのみを読み取ること、トランザクションのコミットはアトミック操作であり、トランザクションで発生したすべての変更を一度に利用できることを常に理解していると思います。

それは正しいです。間違っているのは、同時実行のSELECT (または更新) が、実行中のどこにあるかに関係なく、変更全体を確認することを期待することです。SSMS クエリを開き、次を実行します。

use tempdb;
go

create table test (a int not null primary key, b int);
go

insert into test (a, b) values (5,0)
go

begin transaction
insert into test (a, b) values (10,0)

新しい SSMS クエリを開き、次を実行します。

update test 
    set b=1
    output inserted.*
    where b=0

これは、コミットされていない INSERT の背後でブロックされます。最初のクエリに戻り、次を実行します。

insert into test (a, b) values (1,0)
commit

これがコミットされると、2 番目の SSMS クエリが終了し、3 行ではなく 2 行が返されます。QED。これはREAD COMMITTEDです。あなたが期待するのは、SERIALIZABLE の実行です (この場合、上記の例はデッドロックします)。

于 2012-04-23T19:50:27.200 に答える
0

次のように発生する可能性があります。

  1. ライター/インサーターは 20 行を書き込みます (コミットしません)。
  2. リーダー/アップデーターは 1 つの行を読み取ります (コミットされていないため、破棄されます)。
  3. ライター/挿入者がコミットする
  4. リーダー/アップデーターは 19 行を読み取り、現在コミットされているため表示されます

これは、シリアライズ可能な分離レベル (または、より同時性の高いスナップショット分離) によってのみ修正されると思います。

于 2012-04-23T19:27:36.127 に答える