【问题标题】:Correctly generating a unique invoice id正确生成唯一发票 ID
【发布时间】:2014-08-06 20:05:28
【问题描述】:

有人要求我清理其他人的控制器代码,该代码会生成发票,但我遇到了一些我不知道如何解决的问题。有问题的代码如下(这是使用 EF 6: Code First):

var invid = db.TransportJobInvoice.Where(c => c.CompanyId == CompanyId)
                .Max(i => i.InvoiceId);
var invoiceId = invid == null ? 1 : (int)invid + 1;

代码应该根据为其创建发票的公司生成invoiceId。所以这个小表可能如下所示:

------------------------------
| Id | CompanyId | InvoiceId |
------------------------------
|  1 |         1 |         1 |
------------------------------
|  2 |         1 |         2 |
------------------------------
|  3 |         1 |         3 |
------------------------------
|  4 |         2 |         1 |
------------------------------
|  5 |         2 |         2 |
------------------------------

如您所见,invoiceId 将根据相关公司的当前发票数量生成。但是,我认为建议两个线程可以在评估此行之前执行查询是合理的:

var invoiceId = invid == null ? 1 : (int)invid + 1;

这将导致为两张不同的发票生成相同的invoiceId

是否有一个简单的解决方案,可能利用 Entity Framework 自动执行此操作?

【问题讨论】:

  • 我删除了我的答案,因为我确实忽略了这一点。如果将 UPDATE 代码与增加发票 ID 的部分一起移动到存储过程,它将与事务一起使用。否则,恐怕你没有锁就无法逃脱。
  • @MarcelN。我非常感谢您的帮助。

标签: c# asp.net-mvc multithreading entity-framework


【解决方案1】:

我建议使用身份作为主键,非常重要!

然后,我会为“CustomerInvoiceID”添加一列,并在 CustomerID 和 CustomerInvoiceID 上放置一个复合唯一键。

然后,创建一个存储过程,在插入字段 CustomerInvoiceID 后填充它,这里是一些伪代码:

CREATE PROCEDURE usp_PopulateCustomerInvoiceID 
    @PrimaryKey INT, --this is your primary key identity column
    @CustomerID INT
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @cnt INT;

    SELECT @CNT = COUNT(1)
    FROM TBL 
    WHERE CustomerID = @CustomerID
        AND PrimaryKeyColumn <= @PrimaryKey

    UPDATE tbl
    SET CustomerInvoiceID = @cnt + 1
    WHERE PrimaryKeyColumn = @PrimaryKey
END

【讨论】:

  • 我需要调查一下,因为这看起来像是我想要的。我想知道是否有办法直接使用代码来做到这一点。
  • 在我看来,使用 SQL 存储过程是可行的方法,它会自动为您处理事务。此解决方案也不需要锁定(应尽可能避免锁定,尤其是在高流量网站中)。
  • 如果意图获得MAX,我会避免使用COUNT。使用正确的索引(CustomerId asc,CustomerInvoiceId desc)应该更快地获得最大值,并且如果有任何已删除的值,您将不会被绊倒。这也需要在适当的transaction isolation level 的事务中完成,以使其在其他活动中正常运行。
  • -1:存储过程不会自动将其语句包装在事务中。
  • @jhiden - 如果两个线程大约在同一时间调用此存储过程,它们都可以获得相同的 ID,因为您没有使用事务来使其中一个线程阻塞,直到另一个线程完成.
【解决方案2】:

两种可能:

服务器端:不要在客户端计算 max(ID)+1。相反,作为 INSERT 语句的一部分,通过 INSERT..SELECT 语句计算 max(ID)+1。

客户端:在客户端生成一个 GUID,而不是递增的 int,并将其用作您的 InvoiceID。

【讨论】:

  • 感谢您的回答。它似乎与 jhilden 的答案一致,尽管不需要复合键。
  • 与@jhiden 的回答不太一样。对于我的服务器端解决方案,您只需将 INSERT 语句更改为 INSERT..SELECT。不需要更改 db 架构,也不需要事务,因为它是执行 SELECT 和 INSERT 的单个 SQL 语句。
【解决方案3】:

如果您在 SQL Server 中使用 Identity 字段,则会自动处理。

【讨论】:

  • 但它需要按公司递增,而不是全局表。
  • 您介意详细说明一下吗? invoiceId 不是密钥的一部分。
  • 如何强制Identity每个 公司生成序列号?
  • 对不起,我错过了每个公司的部分。何必呢。只要每个 invoiceId 都是唯一的,它们也将是每个公司的。会有差距,但这不应该成为问题。
  • 您绝对应该使用 Identity 作为表上的主键。您应该使用仅对客户唯一的单独列。
【解决方案4】:

我不知道您是否可以自动生成发票 ID,除非它被威胁为外键(我认为不是)。

您的多线程问题可以使用 lock 语句来解决。

lock (myLock)
{
     var invid = db.TransportJobInvoice.Where(c => c.CompanyId == CompanyId)
            .Max(i => i.InvoiceId);
     var invoiceId = invid == null ? 1 : (int)invid + 1;
}

这将保证只有线程在执行这些语句。

但要小心,当这些语句被并行执行很多并且查询需要一些很长的时间来执行时,这可能会导致性能问题。

【讨论】:

  • 谢谢乔迪。我更愿意保持数据完整性并牺牲一些性能,而不是相反。
  • 注意 - 在 ASP.NET 中使用 lock 时必须小心,因为锁定仅适用于单个进程。
【解决方案5】:

一种完全不同的方法是为每个CustomerId 创建一个带有NextId 的单独表。随着新客户的添加,您将在此表中添加一个新行。它的优点是即使您允许删除发票,分配给发票的编号也可以保持唯一。

create procedure GetInvoiceIdForCustomer
  @CustomerId as Int,
  @InvoiceId as Int Output
as
begin
  set nocount on

  begin transaction

  update CustomerInvoiceNumbers 
    set @InvoiceId = NextId, NextId += 1
    where CustomerId = @CustomerId

  if @@RowCount = 0
    begin
    set @InvoiceId = 1
    insert into CustomerInvoiceNumbers ( CustomerId, NextId ) values ( @CustomerId, @InvoiceId + 1 )
    end

  commit transaction
end       

end

【讨论】:

  • 感谢您的建议。很抱歉没有尽快回复您,但由于各种原因,我已经能够测试任何东西。如果可能,我会对此进行调查。
猜你喜欢
  • 1970-01-01
  • 2015-09-01
  • 2012-02-22
  • 1970-01-01
  • 2015-09-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多