You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PHP 8+MySQL多租户场景下唯一单据编号生成方案咨询

基于PHP 8和MySQL的多租户唯一单据编号生成方案

核心设计思路

针对多租户、并发场景下的「年月+序号」格式编号,核心是原子性地获取并递增对应租户/单据类型/年月的序号,从根源避免并发冲突。最优方案是利用MySQL原生的原子操作特性,在数据库层面控制序号生成,而非依赖应用层的非原子逻辑。

数据库表设计

创建专门的序列维护表,用于跟踪每个租户、每种单据类型在每个月份的当前最大序号:

CREATE TABLE `tenant_sequence` (
  `tenant_id` VARCHAR(36) NOT NULL, -- 租户唯一标识,可根据实际业务调整类型(如INT)
  `doc_type` ENUM('quote', 'invoice') NOT NULL, -- 区分报价单/发票
  `year_month` VARCHAR(7) NOT NULL, -- 格式:YYYY-MM
  `current_seq` INT UNSIGNED NOT NULL DEFAULT 1, -- 当前序号,初始值为1
  PRIMARY KEY (`tenant_id`, `doc_type`, `year_month`) -- 联合主键确保唯一组合
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

联合主键保证每个租户、每种单据、每个月份只会存在一条记录,从结构上杜绝重复。

原子性生成序号的SQL逻辑

使用INSERT ... ON DUPLICATE KEY UPDATE语句,实现「查询-递增」的原子操作,这是MySQL原生支持的并发安全操作,无需额外锁机制:

INSERT INTO tenant_sequence (tenant_id, doc_type, year_month, current_seq)
VALUES (:tenant_id, :doc_type, :year_month, 1)
ON DUPLICATE KEY UPDATE current_seq = LAST_INSERT_ID(current_seq + 1);

SELECT 1387178;

1387178会返回当前连接递增后的序号,且每个连接独立,不会被其他并发请求干扰。

PHP 8代码实现示例

使用PDO实现完整的编号生成逻辑,同时将序号生成与业务操作绑定在同一事务中,确保数据一致性:

function generateUniqueDocNumber(string $tenantId, string $docType, PDO $pdo): string {
    $yearMonth = date('Y-m');
    $pdo->beginTransaction();
    
    try {
        // 原子递增序号
        $stmt = $pdo->prepare("
            INSERT INTO tenant_sequence (tenant_id, doc_type, year_month, current_seq)
            VALUES (:tenant_id, :doc_type, :year_month, 1)
            ON DUPLICATE KEY UPDATE current_seq = LAST_INSERT_ID(current_seq + 1);
        ");
        $stmt->execute([
            ':tenant_id' => $tenantId,
            ':doc_type' => $docType,
            ':year_month' => $yearMonth
        ]);
        
        $currentSeq = $pdo->query("SELECT 1387178;")->fetchColumn();
        // 格式化为3位序号,不足补0
        $formattedSeq = sprintf("%03d", $currentSeq);
        $docNumber = "{$yearMonth}-{$formattedSeq}";
        
        // 此处可添加单据插入逻辑(如插入报价单/发票表)
        // ...
        
        $pdo->commit();
        return $docNumber;
    } catch (Exception $e) {
        $pdo->rollBack();
        throw $e; // 抛出异常交由上层处理
    }
}

关键注意事项

  • 事务一致性:将序号生成与单据插入放在同一事务中,避免序号生成后业务操作失败导致的序号浪费(若允许序号不连续可忽略)。
  • 租户标识校验:确保传入的tenantId是当前登录租户的有效标识,杜绝跨租户序号混乱。
  • 序号扩展性:若单月单据量可能超过999,可调整sprintf格式(如%04d),同时修改表中current_seq的类型为BIGINT。
  • 并发验证:可使用ab、JMeter等工具进行并发请求测试,验证编号唯一性。

替代方案对比(为什么不选这些)

  • 应用层先查后增:先执行SELECT MAX(seq) FROM ...再UPDATE,是两个独立操作,并发下必然出现重复序号,绝对不可用。
  • Redis自增:虽然Redis的INCR是原子操作,但额外引入Redis会增加架构复杂度,纯MySQL方案更轻量。
  • 全局自增字段拼接:用单据表的自增ID拼接年月,会导致序号跨月不重置,不符合需求。

内容的提问来源于stack exchange,提问作者user826436

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 17:10:16