事务并发隔离 - 为什么我可以更新其他事务记录的子集?

时间:2012-04-23 19:21:38

标签: sql-server sql-server-2008 sql-server-2005 concurrency transactions

我正在尝试理解我遇到的一个问题,在处理使用读取提交的隔离级别的事务时,我认为不应该这样做。我有一个用作队列的表。在一个线程(连接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,然后检查更新操作中的行计数。当rowcount不是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

2 个答案:

答案 0 :(得分:2)

你的期望完全错位了。您从未在查询中表达过“排队”正好20行的要求。只要status为1且type为1,UPDATE就可以返回0,19,20,21或1000行,并且所有结果都是正确的。如果您希望'dequeue'出现在'enqueue'的顺序(在你的问题中以某种方式被忽略,但从未明确说明)然后你的'dequeue'操作必须包含ORDER BY子句。如果您添加了这样一个明确规定的要求,那么您对“出队”总是返回整批“排队”行(即20行的倍数)的期望将更接近于合理的期望。就目前情况而言,就像我说的那样,完全是错位的。

如需更长时间的讨论,请参阅Using Tables as Queues

  

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

基本上问题归结为如果我在INSERT时选择SELECT,我会看到多少插入的行?。如果隔离级别声明为SERIALIZABLE,您只有权关注。没有其他隔离级别可以预测在UPDATE运行时插入的行数。只有SERIALIZABLE声明结果必须与一个接一个地运行两个语句相同(即序列化,因此名称)。虽然在考虑物理顺序和缺少ORDER BY子句时,如何 UPDATE'看到'只有部分INSERT批次的技术细节很容易理解,但解释是无关紧要的。根本问题是期望是无保证的。即使通过添加适当的ORDER BY和正确的聚集索引键(上面链接的文章解释详细信息)来“修复”'问题',期望仍然是无保证的。 UPDATE“看到”1,19或21行仍然是完全合法的,尽管它不太可能发生。

  

我想我总是理解READ COMMITTED只读read   数据,以及事务提交是一个原子操作,使所有   交易中发生的变化一次可用。

这是正确的。不正确的是期望并发 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。这是READ COMMITTED。你期望的是SERIALIZABLE执行(在这种情况下,上面的例子将会死锁)。

答案 1 :(得分:0)

可能会发生这样的事情:

  1. 编写器/插入器写入20行(不提交)
  2. 阅读器/更新程序读取一行(未提交 - 它将其丢弃)
  3. 作者/插入器提交
  4. 读取器/更新程序读取19行,这些行现在已提交,因此可见
  5. 我认为只有隔离级别的可序列化(或更快并发的快照隔离)才能解决这个问题。