在没有竞争条件的SQL Server中创建顺序发票号

时间:2013-10-03 17:35:57

标签: sql sql-server locking race-condition

我有一个法律要求,在我们的应用程序(使用SQL Server)发票集合中,我们的编号不会有差距。因此,如果这些是发票号,则不允许这样做:[1, 2, 3, 4, 8, 10]因为它不是连续的。为此,我们在InvoiceNumber表格中有一个Invoices列。除此之外,我们还有一个InvoiceNumbers表,其中包含每个组织的当前发票号(因为每个组织都需要有自己的序列)。然后,存储过程负责以原子方式填充InvoiceNumber Invoices;它会在InvoiceNumbers表中将当前计数器递增1并将该新值填入Invoices表,或者在发生错误时回滚事务。这很有效。

现在添加了一项新要求:某些订单必须共享相同的发票,因此必须使用相同的发票编号,而以前每个订单都是单独开具发票的。为此,我们在当天开始时创建发票,并将其与当前FinancialPeriod(工作日,基本上)相关联,这将是每个订单使用的发票。但是,组织可能不会创建任何需要共享发票的类型的订单,因此在最初创建的发票(因为第二天创建新发票)的一天中没有任何内容可以开具发票并创建间隙。

现在,对我来说最简单的解决方案是懒洋洋地填写当天开始时创建的共享发票上的InvoiceNumber。如果当天创建了订单且InvoiceNumber仍为NULL,则创建该号码。这将确保InvoiceNumber永远不会被使用(Invoice记录未被使用并不重要,它没有任何实际意义。)

为此,我创建了以下存储过程,对于现有Invoice,它会填充InvoiceNumber,但前提是它仍然是NULL。我只是不确定SQL Server如何锁定以及是否存在竞争条件的可能性,其中两个数据库事务决定InvoiceNumber仍然是NULL并且都会增加计数器并浪费一个数字,从而创建一个间隙。

基本上,这个冗长的问题归结为:两个同步数据库事务是否可以决定在此处if(@currentNumber is null)输入@invoiceID块?

你看到我从这里得到的锁定部分,但我不确定它是否适用于我的情况:

Pessimistic lock in T-SQL

CREATE PROCEDURE [dbo].[CreateInvoiceNumber]
    @invoiceID int,
    @appID int
AS
BEGIN

  SET NOCOUNT ON;

    if not exists (select 1 from InvoiceNumbers where ApplicationID = @appID) insert into InvoiceNumbers values (@appID, 1)

    declare @currentNumber int = null;

    select @currentNumber = convert(int, i.InvoiceNumber)
    from   Invoices i
    with   (HOLDLOCK, ROWLOCK)
    where i.ID = @invoiceID

    if(@currentNumber is null) 
    begin
        update InvoiceNumbers set @currentNumber = Value = Value + 1 
            where ApplicationID = @appID

        update Invoices set InvoiceNumber = @currentNumber where ID = @invoiceID        
    end

    select convert(nvarchar, @currentNumber)
END

修改

正如我的评论中所提到的,这些和其他写操作是从C#应用程序逻辑发起的数据库事务的一部分。只是BeginTransaction上的常规SqlConnection,带有默认选项,如果有任何例外,当然会回滚。

1 个答案:

答案 0 :(得分:1)

确保数据库隔离级别已设置为READ COMMITTED

SET TRANSACTION ISOLATION LEVEL READ COMMITTED

这是默认的隔离级别。它确保在读取行之前必须提交所有事务,因此不会发生脏读。

同样重要的是,在更新InvoiceNumbers表时,确保它在事务中,您希望ACID原则在此处应用,并且所有内容都是原子的(作为整个单元提交或事务回滚)