在tsql中是一个带有Select语句的Insert在并发方面是否安全?

Dav*_*all 6 t-sql sql-server concurrency

在我对这个SO问题的回答中,我建议使用单个insert语句,使用select增加一个值,如下所示.

Insert Into VersionTable 
(Id, VersionNumber, Title, Description, ...) 
Select @ObjectId, max(VersionNumber) + 1, @Title, @Description 
From VersionTable 
Where Id = @ObjectId 
Run Code Online (Sandbox Code Playgroud)

我建议这是因为我认为这个语句在并发性方面是安全的,因为如果同时运行同一个对象id的另一个插入,则不可能有重复的版本号.

我对么?

Hei*_*nzi 12

正如保罗写道:不,这不安全,我想为此添加经验证据:创建一个Table_1包含一个字段ID和一个有价值记录的表0.然后在两个Management Studio查询窗口中同时执行以下代码:

declare @counter int
set @counter = 0
while @counter < 1000
begin
  set @counter = @counter + 1

  INSERT INTO Table_1
    SELECT MAX(ID) + 1 FROM Table_1 

end
Run Code Online (Sandbox Code Playgroud)

然后执行

SELECT ID, COUNT(*) FROM Table_1 GROUP BY ID HAVING COUNT(*) > 1
Run Code Online (Sandbox Code Playgroud)

在我的SQL Server 2008上,662创建了一个ID()两次.因此,施加到单个语句的缺省隔离级别是足够的.


编辑:显然,包装INSERTwith BEGIN TRANSACTION并且COMMIT不会修复它,因为事务的默认隔离级别仍然是READ COMMITTED,这是不够的.请注意,将事务隔离级别设置REPEATABLE READ也是不够的.使上述代码安全的唯一方法是添加

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
Run Code Online (Sandbox Code Playgroud)

在顶部.然而,这在我的测试中偶尔会导致死锁.

编辑:我发现唯一安全不会产生死锁的解决方案(至少在我的测试中)是显式锁定表(这里默认的事务隔离级别就足够了).要小心; 此解决方案可能会破坏性能

...loop stuff...
    BEGIN TRANSACTION

    SELECT * FROM Table_1 WITH (TABLOCKX, HOLDLOCK) WHERE 1=0

    INSERT INTO Table_1
      SELECT MAX(ID) + 1 FROM Table_1 

    COMMIT
...loop end...
Run Code Online (Sandbox Code Playgroud)

  • 在SERIALIZABLE模型下,死锁发生如下:T1选择MAX(...),结果将一个范围-S锁定从BOF到EOF(即包括虚拟开始和结束的整个表).T2选择MAX(...)并将相同的范围-S锁从BOF置于EOF.T1尝试插入,阻挡T2范围锁定.T2尝试插入,阻止T1的范围锁定.僵局. (3认同)

Ran*_*der 2

我认为你的假设是不正确的。当您查询 VersionNumber 表时,您仅对该行放置读锁。这不会阻止其他用户读取同一个表中的同一行。因此,两个进程有可能同时读取VersionNumber表中的同一行并生成相同的VersionNumber值。