我正在尝试理解我遇到的一个问题,在处理使用读取提交的隔离级别的事务时,我认为不应该这样做。我有一个用作队列的表。在一个线程(连接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
答案 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)
可能会发生这样的事情:
我认为只有隔离级别的可序列化(或更快并发的快照隔离)才能解决这个问题。