代码之家  ›  专栏  ›  技术社区  ›  David Hall

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

  •  6
  • David Hall  · 技术社区  · 16 年前

    在我对 this SO question 我建议使用一个insert语句和一个select语句来增加一个值,如下所示。

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

    我提出这个是因为我 相信 该语句在并发性方面是安全的,因为如果同时运行同一对象id的另一个insert,则不可能有重复的版本号。

    我说得对吗?

    4 回复  |  直到 9 年前
        1
  •  12
  •   Heinzi    16 年前

    正如保罗所写: 不,这不安全 ,我想为其添加经验证据:创建一个表 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
    

    然后执行

    SELECT ID, COUNT(*) FROM Table_1 GROUP BY ID HAVING COUNT(*) > 1
    

    在我的SQL Server 2008上,一个ID( 662 )创建了两次。因此,应用于单个语句的默认隔离级别为 足够的


    编辑:显然,包装 INSERT 具有 BEGIN TRANSACTION COMMIT 不会修复它,因为事务的默认隔离级别仍然是 READ COMMITTED ,这是不够的。请注意,将事务隔离级别设置为 REPEATABLE READ 而且 这还不够。制作上述代码的唯一方法 安全 就是加上

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
    

    在顶端。然而,这在我的测试中时不时会造成死锁。

    编辑:我找到的唯一安全的解决方案 不产生死锁(至少在我的测试中)是显式地以独占方式锁定表(这里的默认事务隔离级别足够了)。不过要小心;这个解决方案可能会 杀死 性能:

    ...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...
    
        2
  •  4
  •   Paul Creasey    16 年前

    read Committed的默认隔离使其不安全,如果其中两个完全并行运行,您将得到一个副本,因为没有应用读锁。

    您需要可重复读取或可序列化的隔离级别来确保安全。

        3
  •  2
  •   Randy Minder    16 年前

    我认为你的假设是错误的。当您查询VersionNumber表时,您只是在该行上放置了一个读锁。这不会阻止其他用户从同一个表中读取同一行。因此,两个进程可以同时读取VersionNumber表中的同一行,并生成相同的VersionNumber值。

        4
  •  0
  •   gbn    16 年前
    • 您需要对(Id、VersionNumber)进行唯一约束才能强制执行它

    • 我会使用ROWLOCK、XLOCK提示来阻止其他人阅读你计算的锁定行

    • 或者用TRY/CATCH包装插入物。如果我得到一个副本,再试一次。。。