6

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

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

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

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

从本质上讲,这个冗长的问题归结为:两个同时的数据库事务是否可以决定在这里输入if(@currentNumber is null)相同的块@invoiceID

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

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选项,如果出现任何异常,它当然会回滚。

4

1 回答 1

1

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

SET TRANSACTION ISOLATION LEVEL READ COMMITTED

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

还有一个重要的注意事项,当更新InvoiceNumbers表时,确保它在一个事务中,你希望 ACID 原则在这里应用并且一切都是原子的(作为一个整体提交或事务回滚)。

于 2013-10-03T17:41:30.037 回答