我有一个法律要求,在我们的应用程序(使用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
块?
你看到我从这里得到的锁定部分,但我不确定它是否适用于我的情况:
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
,带有默认选项,如果有任何例外,当然会回滚。
答案 0 :(得分:1)
确保数据库隔离级别已设置为READ COMMITTED
。
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
这是默认的隔离级别。它确保在读取行之前必须提交所有事务,因此不会发生脏读。
同样重要的是,在更新InvoiceNumbers
表时,确保它在事务中,您希望ACID原则在此处应用,并且所有内容都是原子的(作为整个单元提交或事务回滚)