在多租户MySQL数据库中生成(和保存)增量发票编号的最佳方式

mfuanj7w  于 2023-08-02  发布在  Mysql
关注(0)|答案(4)|浏览(120)

我发现了两种不同的方法,首先获取下一个发票编号,然后将发票保存在多租户数据库中,当然,每个租户都有自己的发票,其递增编号不同。

  • 我的第一个(也是实际的)方法是这样的(效果很好):

1.向发票表中添加新记录。无论发票编号是否(例如,0或空)
1.插入后,我将获得创建记录的唯一ID
1.现在我做一个"SELECT table where ID = $lastcreatedID **FOR UPDATE**"
1.在这里,我使用"SELECT @A:=MAX(NUMBER)+1 FROM TABLE WHERE......"获取最新保存的发票编号
1.最后,我用"UPDATE table SET NUMBER = $mynumber WHERE ID = $lastcreatedID"更新先前保存的记录,其中包含该发票号
这工作正常,但我不知道是否真的需要“for update”,或者由于性能等原因,这是否是在多租户数据库中执行此操作的正确方法。

*第二种(也是更简单的)方法是这样的(也有效,但我不知道它是否是一种 * 安全 * 的方法):

  1. INSERT INTO table (NUMBER,TENANT) SELECT COALESCE(MAX(NUMBER),0)+1,$tenant FROM table WHERE....
    1.就这样
    这两种方法都在工作,但我想知道它们之间的差异,关于速度,性能,如果它可能会创建重复等。
    或者有更好的办法吗
    我使用MySQL和PHP。该应用程序是一个发票/销售云软件,将由许多客户(租户)使用。
    谢啦,谢啦
tvz2xvvm

tvz2xvvm1#

不管您是否使用这些值作为数据库ID,重用ID实际上肯定会在某些时候导致问题。即使您没有重用ID,也会遇到两个发票创建请求同时运行并获得相同的MAX()+1结果的情况。
要解决所有这些问题,您需要重新实现一个简单的序列生成器,该生成器在发出值时锁定其存储。例如:

CREATE TABLE client_invoice_serial (
  -- note: also FK this back to the client record
  client_id INTEGER UNSIGNED NOT NULL PRIMARY KEY,
  serial INTEGER UNSIGNED NOT NULL DEFAULT 0
);

个字符
在一个非常基本的层面上,这就是MySQL在后台为AUTOINCREMENT列所做的事情。

jei2mxaa

jei2mxaa2#

不要使用MAX(id)+1。总有一天,它会咬你的。会有两张号码相同的发票,我们需要花几段时间来解释为什么会发生。
相反,按预期的方式使用AUTO_INCREMENT

INSERT INTO Invoices (id, ...) VALUES (NULL, ...);
SELECT LAST_INSERT_ID();   -- specific to the conne ction

字符串
这是安全的,即使多个连接正在做同样的事情。不需要FOR UPDATEBEGIN等。(您可能需要用于其他目的)。
并且,* 永远不要 * 删除行。相反,使用标准的商业实践,使不良发票无效。想象一下被审计。
尽管如此,仍然存在一个潜在的问题。在ROLLBACK或系统崩溃后,ID可能会被“烧毁”。另外,像INSERT IGNORE这样的东西在检查是否需要id之前分配它。
如果你能接受这些警告,请使用AUTO_INCREMENT
如果没有,则创建一个1行2列的表来模拟序列号生成器:http://mysql.rjweb.org/doc.php/index_cookbook_mysql#sequence
或者使用MariaDB的SEQUENCE

fxnxkyjh

fxnxkyjh3#

如果您允许用户修改自动生成的号码,则可能需要使用混合方法。选择最大前一个参考编号和最大现有行ID(自动递增的唯一列)。然后比较哪个最低,加1,并将其用作新的参考编号。
在本例中,引用号列是一个varchar,因此在进行比较之前,我还使用了一个regexp来从引用号中删除任何非数字字符。

$db->query("select max(t.id) maxid,max(cast(t.reference as unsigned)) maxreference
                  from transactions t
                 where t.companyid = :companyid
                   and transactiontype = 'C'
                   and t.reference REGEXP '^[0-9]+$'");
  $db->bind(":companyid",$companyid);

  if ($row = $db->single()){
    $maxreference = $row["maxreference"];
    $maxid = $row["maxid"];
    if($maxreference <> ''){
      // Get lower of two numbers
      $number = min($maxreference, $maxid);

      // Make sure it is at least 4 characters long
      $number = str_pad($number + 1,4,"0",STR_PAD_LEFT);
    }else{
      // If max reference # is blank, start with 0001
      $number = "0001";
    }
  }else{
    // If no row is found, start with 0001
    $number = "0001";
  }

字符串
这种方法的优点是,用户可以从较低的4位数开始(即使您已经有数百万行),但如果他们无意中手动输入一个巨大的数字用作其参考编号,下一个自动生成的数字将不会使用该数字,而是使用较小的最大行ID。

enxuqcxy

enxuqcxy4#

这两种方法都有效,但在高流量情况下,每种方法都有自己的缺点。
第一种方法为您创建的每个发票运行3个查询,给服务器带来额外的负载。
第二种方法可能导致在两个发票以非常小的时间差生成的事件中出现重复(使得SELECT查询为两个发票返回相同的最大数)。
这两种方法都可能在高业务量条件下导致问题。
下面列出了两个问题的解决方案:
1.使用生成列:Mysql支持生成的列,这些列基本上是使用每一行的其他列值派生的。参考this
1.即时计算发票号:由于您使用主键作为发票的一部分,因此让DB处理生成唯一主键的工作,然后在业务逻辑中使用每个发票的id动态生成发票编号。

相关问题