如何在SQL Server 2005中保证事务完整性

时间:2009-03-31 16:34:15

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

乍一看,我有一个非常简单的问题。我希望能够获得带前缀的唯一键值。我有一个包含'Prefix'和'Next_Value'列的表。

所以你认为你只是启动一个事务,从这个表中获取下一个值,递增表中的下一个值并提交,将前缀连接到值,并保证一系列独特的字母数字键。

然而,在负载下,各种服务器通过ADO.NET点击这个存储过程,我发现它会不时地将相同的密钥返回给不同的客户端。当密钥用作主键时,这当然会导致错误!

我天真地假设BEGIN TRAN ... COMMIT TRAN确保了范围内数据访问的原子性。在调查这个问题时,我发现了事务隔离级别,并添加了SERIALIZABLE作为最严格的限制 - 没有任何乐趣。

  Create proc [dbo].[sp_get_key] 
  @prefix nvarchar(3)
  as
  set tran isolation level SERIALIZABLE
  declare       @result nvarchar(32)  

  BEGIN TRY
      begin tran

      if (select count(*) from key_generation_table where prefix = @prefix) = 0 begin
         insert into key_generation_table (prefix, next_value) values (@prefix,1)
      end

      declare @next_value int

      select @next_value = next_value
      from key_generation_table
      where prefix = @prefix

      update key_generation_table
      set next_value = next_value + 1 
      where prefix = @prefix

      declare @string_next_value nvarchar(32)
      select @string_next_value = convert(nvarchar(32),@next_value)

      commit tran

      select @result = @prefix + substring('000000000000000000000000000000',1,10-len(@string_next_value)) + @string_next_value

      select @result

  END TRY
   BEGIN CATCH
        IF @@TRANCOUNT > 0 ROLLBACK TRAN

        DECLARE @ErrorMessage NVARCHAR(400);
        DECLARE @ErrorNumber INT;
        DECLARE @ErrorSeverity INT;
        DECLARE @ErrorState INT;
        DECLARE @ErrorLine INT;

        SELECT @ErrorMessage = N'{' + convert(nvarchar(32),ERROR_NUMBER()) + N'} ' + N'%d, Line %d, Text: ' + ERROR_MESSAGE();
        SELECT @ErrorNumber = ERROR_NUMBER();
        SELECT @ErrorSeverity = ERROR_SEVERITY();
        SELECT @ErrorState = ERROR_STATE();
        SELECT @ErrorLine = ERROR_LINE();
        RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)


    END CATCH  

这是密钥生成表...

CREATE TABLE [dbo].[Key_Generation_Table](
        [prefix] [nvarchar](3) NOT NULL,
       [next_value] [int] NULL,
  CONSTRAINT [PK__Key_Generation_T__236943A5] PRIMARY KEY CLUSTERED 
  (
    [prefix] ASC
  )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF,      
      ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
  ) ON [PRIMARY]

4 个答案:

答案 0 :(得分:1)

你的if块上有一些竞争条件。两个请求同时进入一个新的前缀,两个都可以通过if块。您应该更改此值以始终插入到您的表中,但在插入的where子句中执行检查以确保它不存在。另外我建议使用Exists而不是count(*)= 0。有了Exists,一旦sql找到一行,它就会停止查看。

同样的事情可能发生在你的选择中,你可以让两个线程都选择相同的值,然后一个被阻塞等待更新,但是当它返回时它将返回旧的id。

修改逻辑以首先更新行,然后获取更新它的值

   update key_generation_table          
     set next_value = next_value + 1
     where   prefix = @prefix

       select @next_value = next_value -1         
       from key_generation_table          
       where prefix = @prefix          

我还会考虑使用ouput语句而不是第二次选择。

修改

我可能会改变这个以使用输出,因为yoru在SQL2005上:

declare @keyTable as table  (next_value int)

UPDATE key_generation_Table
set next_value=next_value+1
OUTPUT DELETED.next_value into @keyTable(next_value)
WHERE prefix=@prefix

/* Update the following to use your formating */
select next_value from @keyTable 

答案 1 :(得分:1)

尝试使用UPDLOCK提示。

      select @next_value = next_value
      from key_generation_table WITH(UPDLOCK)
      where prefix = @prefix
理想情况下,

key_generation_table仅用于此特定存储过程。否则UPDLOCK会增加死锁的可能性。

答案 2 :(得分:0)

在盒子外面思考,你能不能在一个带有AUTO_INCREMENT id的表中添加一行,然后使用id?这保证在负载下是唯一的。然后你可以删除该行(以免表格无休止地增长)。

要回答有关正在发生的事情的问题,transactions aren't critical regions

  

<强> SERIALIZABLE

     

最具限制性的隔离级别。使用时,幻像值不会发生。它会阻止其他用户在完成事务之前将行更新或插入数据集。

此机制旨在防止的问题与您遇到的问题不同。

如果您想采用上面概述的方法,您应该获得关键区域的独占锁定。

答案 3 :(得分:0)

Serializable保存锁,但允许读取。因此,如果非常快速地并行调用proc,则加载中的select / update可能会产生相同的结果。 我想......

如果你这样做,使用有效的语法,你可以结合2。 tablock确保整个表被锁定。这与serializable是不同的,即并发性.tablock是粒度。 此外,您假设密钥在那里..如果需要,添加缺少的前缀。

update
    key_generation_table WITH (TABLOCK)
set
    @next_value = next_value, next_value = next_value + 1 
where
    prefix = @prefix

if @@ROWCOUNT = 0
begin
    set @next_value  = 1
    insert into key_generation_table (prefix, next_value) values (@prefix, 1)
end
select @string_next_value = convert(nvarchar(32),@next_value)