1

我试图理解我遇到的一个问题,我认为在处理使用读提交隔离级别的事务时应该不可能。我有一个用作队列的表。在一个线程(连接 1)中,我将多批 20 条记录插入到每个表中。每批 20 条记录在一个事务中执行。在第二个线程(连接 2)中,我执行更新以更改已插入队列的记录的状态,这也发生在事务中。当同时运行时,我的期望是受更新影响的行数(连接 2)应该是 20 的倍数,因为连接 1 在事务中以 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 行的要求。UPDATE 可以返回 0、19、20、21 或 1000 行并且所有结果都是正确的,只要status是 1 和type1。如果您期望 'dequeue' 按 'enqueue' 的顺序发生(即在您的问题中以某种方式回避,但从未明确说明)那么您的“出队”操作必须包含一个ORDER BY子句。如果您添加了这样一个明确声明的要求,那么您期望“出队”总是返回一整批“入队”行(即 20 行的倍数)将更接近一个合理的期望。就目前的情况而言,正如我所说,完全错位了。

有关更长的讨论,请参阅使用表作为队列

我不应该担心,当一个事务提交一批 20 条插入的记录时,另一个并发事务只能更新这些记录的子集,而不是全部 20?

基本上问题归结为如果我在插入时选择,我会看到多少插入行?. 如果隔离级别被声明为 SERIALIZABLE,您只有权利担心。其他隔离级别都不会预测在 UPDATE 运行时插入的行数将是可见的。只有 SERIALIZABLE 声明结果必须与一个接一个地运行两个语句相同(即序列化,因此得名)。虽然技术细节如何一旦您考虑物理顺序和缺少 ORDER BY 子句,则 UPDATE '看到' INSERT 批处理的只有一部分很容易理解,解释是无关紧要的。根本问题是期望是没有根据的。即使通过添加正确的 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

提交后,第二个 SSMS 查询将完成,它将返回两行,而不是三行。QED。这是已提交的。您期望的是 SERIALIZABLE 执行(在这种情况下,上面的示例将死锁)。

于 2012-04-23T19:50:27.200 回答
0

它可能会这样发生:

  1. writer/inserter 写入 20 行(不提交)
  2. 读取器/更新器读取一行(未提交 - 它丢弃它)
  3. 作者/插入者提交
  4. 读取器/更新器读取 19 行,这些行现在已提交,因此可见

我相信只有可序列化的隔离级别(或更具并发性的快照隔离)才能解决此问题。

于 2012-04-23T19:27:36.127 回答