简体   繁体   English

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

[英]Best way to generate (and save) incremental invoice numbers in a multi-tenant MySQL database

I have found two different ways to, first, get the next invoice number and, then, save the invoice in a multi-tenant database where, of course, each tenant will have his own invoices with different incremental numbers.我找到了两种不同的方法,首先,获取下一个发票编号,然后将发票保存在多租户数据库中,当然,每个租户都有自己的发票,其增量编号不同。

  • My first (and actual) approach is this (works fine):我的第一个(也是实际的)方法是这样的(工作正常):
  1. Add a new record to the invoices tables.将新记录添加到发票表中。 No matter the invoice number yet (for example, 0, or empty)无论发票编号是否(例如,0,或为空)
  2. I get the unique ID of THAT created record after insert插入后我得到了创建记录的唯一 ID
  3. Now I do a "SELECT table where ID = $lastcreatedID **FOR UPDATE**"现在我做了一个"SELECT table where ID = $lastcreatedID **FOR UPDATE**"
  4. Here I get the latest saved invoice number with "SELECT @A:=MAX(NUMBER)+1 FROM TABLE WHERE......"在这里,我使用"SELECT @A:=MAX(NUMBER)+1 FROM TABLE WHERE......"获得最新保存的发票号码
  5. Finally I update the previously saved record with that invoice number with an "UPDATE table SET NUMBER = $mynumber WHERE ID = $lastcreatedID"最后,我使用"UPDATE table SET NUMBER = $mynumber WHERE ID = $lastcreatedID"使用该发票编号更新先前保存的记录

This works fine, but I don't know if the "for update" is really needed or if this is the correct way to do this in a multi-tenant DB, due to performance, etc.这很好用,但我不知道是否真的需要“更新”,或者由于性能等原因,这是否是在多租户数据库中执行此操作的正确方法。

  • The second (and simpler) approach is this (and works too, but I don't know if it is a secure approach):第二种(也是更简单的)方法是这样的(并且也有效,但我不知道它是否是一种安全的方法):
  1. INSERT INTO table (NUMBER,TENANT) SELECT COALESCE(MAX(NUMBER),0)+1,$tenant FROM table WHERE....
  2. That's it而已

Both methods are working, but I would like to know the differences between them regarding speed, performance, if it may create duplicates, etc.两种方法都有效,但我想知道它们在速度、性能、是否可能会产生重复等方面的区别。

Or... is there any better way to do this?或者......有没有更好的方法来做到这一点?

I'm using MySQL and PHP.我正在使用 MySQL 和 PHP。 The application is an invoice/sales cloud software that will be used by a lot of customers (tenants).该应用程序是一个发票/销售云软件,将被许多客户(租户)使用。

Thanks谢谢

Regardless of if you're using these values as database IDs or not, re-using IDs is virtually guaranteed to cause problems at some point.无论您是否将这些值用作数据库 ID,实际上都可以保证重用 ID 在某些时候会导致问题。 Even if you're not re-using IDs you're going to run into the case where two invoice creation requests run at the same time and get the same MAX()+1 result.即使您不重复使用 ID,您也会遇到两个发票创建请求同时运行并获得相同MAX()+1结果的情况。

To get around all this you need to reimplement a simple sequence generator that locks its storage while a value is being issued.要解决所有这些问题,您需要重新实现一个简单的序列生成器,该生成器在发出值时锁定其存储。 Eg:例如:

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
);
$dbh = new PDO('mysql:...');
/* this defaults to 'on', making every query an implicit transaction. it needs to
be off for this. you may or may not want to set this globally, or just turn it off
before this, and back on at the end. */
$dbh->setAttribute(PDO::ATTR_AUTOCOMMIT,0);
// simple best practice, ensures that SQL errors MUST be dealt with. is assumed to be enabled for the below try/catch.
$dbh->setAttribute(PDO::ATTR_ERRMODE_EXCEPTION,1);

$dbh->beginTransaction();
try {
    // the below will lock the selected row
    $select = $dbh->prepare("SELECT * FROM client_invoice_serial WHERE client_id = ? FOR UPDATE;");
    $select->execute([$client_id]);

    if( $select->rowCount() === 0 ) {
        $insert = $dbh->prepare("INSERT INTO client_invoice_serial (client_id, serial) VALUES (?, 1);");
        $insert->execute([$client_id]);
        $invoice_id = 1;
    } else {
        $invoice_id = $select->fetch(PDO::FETCH_ASSOC)['serial'] + 1;
        $update = $dbh->prepare("UPDATE client_invoice_serial SET serial = serial + 1 WHERE client_id = ?");
        $update->execute([$client_id])
    }
    $dbh->commit();
} catch(\PDOException $e) {
    // make sure that the transaction is cleaned up ASAP, then let the exception bubble up into your general error handling.
    $dbh->rollback();
    throw $e; // or throw a more pertinent error/exception of your choosing.
}
// both committing and rolling back will release the lock

At a very basic level this is what MySQL is doing in the background for AUTOINCREMENT columns.在非常基本的层面上,这就是 MySQL 在后台为 AUTOINCREMENT 列所做的事情。

Do not use MAX(id)+1 .不要使用MAX(id)+1 It will, someday, bite you.总有一天,它会咬你一口。 There will be two invoices with the same number, and it will take us a few paragraphs to explain why it happened.会有两张相同编号的发票,我们用几段来解释为什么会这样。

Instead, use AUTO_INCREMENT the way it is intended.相反,请按照预期的方式使用AUTO_INCREMENT

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

That is safe even when multiple connections are doing the same thing.即使多个连接在做同样的事情,这也是安全的。 No FOR UPDATE , no BEGIN , etc is necessary.没有FOR UPDATE ,没有BEGIN等是必要的。 (You may want such for other purposes.) (您可能希望将其用于其他目的。)

And, never delete rows.而且,永远不要删除行。 Instead, use the standard business practice of invalidating bad invoices.相反,使用使不良发票无效的标准商业惯例。 Imagine being audited.想象一下被审计。

All that said, there is still a potential problem.综上所述,仍然存在潜在问题。 After a ROLLBACK or system crash, an id may be "burned".ROLLBACK或系统崩溃之后,一个 id 可能会被“烧毁”。 Also things like INSERT IGNORE allocate the id before checking to see whether it will be needed.此外,诸如INSERT IGNORE之类的事情会在检查是否需要之前分配 id。

If you can live with the caveats, use AUTO_INCREMENT .如果您可以接受这些警告,请使用AUTO_INCREMENT

If not, then create a 1-row, 2-column table to simulate a sequence number generator: http://mysql.rjweb.org/doc.php/index_cookbook_mysql#sequence如果没有,则创建一个 1 行 2 列的表来模拟序列号生成器: http://mysql.rjweb.org/doc.php/index_cookbook_mysql#sequence

Or use MariaDB's SEQUENCE或者使用 MariaDB 的SEQUENCE

Both the approaches do work, but each with its own demerits in high traffic situations.这两种方法都有效,但在交通繁忙的情况下,每种方法都有自己的缺点。

The first approach runs 3 queries for every invoice you create, putting extra load on your server.第一种方法为您创建的每张发票运行 3 个查询,给您的服务器增加了额外的负载。

The second approach can lead to duplicates in events where two invoices are generated with very little time difference (such that the SELECT query return same max number for both invoices).第二种方法可能会导致在生成两张发票的时间差很小的事件中出现重复(例如 SELECT 查询为两张发票返回相同的最大数量)。

Both the approaches may lead to problems in high traffic conditions.这两种方法都可能在高流量条件下导致问题。

Two solutions to the problems are listed below:下面列出了解决问题的两种方法:

  1. Use generated columns : Mysql supports generated columns, which are basically derived using other column values for each row.使用生成列:Mysql 支持生成列,这些列基本上是使用每一行的其他列值派生的。 Refer this参考这个

  2. Calculate invoice number on the fly : Since you're using the primary key as part of the invoice, let the DB handle generating unique primary keys, and then generate invoice numbers on the fly in your business logic using the id for each invoice.即时计算发票编号:由于您将主键用作发票的一部分,因此让数据库处理生成唯一主键,然后使用每张发票的 id 在您的业务逻辑中动态生成发票编号。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM