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

PHP自动生成注册编号并发问题求助:如何避免多表单加载同编号

解决注册编号并发预占问题的方案

核心问题分析

当前代码的问题在于查询最后编号→生成新编号这两步是非原子操作,并发请求会同时读取到相同的历史编号,进而生成重复的新编号。由于业务允许最终存在重复编号,但禁止同一编号被多个表单同时打开,所以核心要实现的是:给生成的编号添加临时占用状态,确保同一时间只有一个表单能持有该编号,直到表单提交或超时释放。


方案一:序列表+行级锁(推荐,适配高并发)

专门创建一个序列管理表,存储每个CI-年月对应的最新序列值,利用数据库行级锁保证并发时只有一个请求能生成新编号。

1. 创建序列表

CREATE TABLE iap_sequence (
    ci_month_year VARCHAR(20) PRIMARY KEY, -- 格式示例:L5-0923
    current_seq INT NOT NULL DEFAULT 0
) ENGINE=InnoDB;

2. 修改生成编号的PHP代码

<?php
$CI = "L5";
$month = date("m");
$year = date("y");
$ci_key = "$CI-$month$year"; // 序列表的主键标识

// 开启数据库事务
mysqli_begin_transaction($conn);

try {
    // 锁定对应行,其他并发请求会等待锁释放
    $query = "SELECT current_seq FROM iap_sequence WHERE ci_month_year = ? FOR UPDATE";
    $stmt = mysqli_prepare($conn, $query);
    mysqli_stmt_bind_param($stmt, "s", $ci_key);
    mysqli_stmt_execute($stmt);
    $result = mysqli_stmt_get_result($stmt);
    $row = mysqli_fetch_assoc($result);

    if (!$row) {
        // 该年月序列未初始化,创建新序列
        $new_seq = 1;
        $insert_stmt = mysqli_prepare($conn, "INSERT INTO iap_sequence (ci_month_year, current_seq) VALUES (?, ?)");
        mysqli_stmt_bind_param($insert_stmt, "si", $ci_key, $new_seq);
        mysqli_stmt_execute($insert_stmt);
    } else {
        // 序列已存在,自增更新
        $new_seq = $row['current_seq'] + 1;
        $update_stmt = mysqli_prepare($conn, "UPDATE iap_sequence SET current_seq = ? WHERE ci_month_year = ?");
        mysqli_stmt_bind_param($update_stmt, "is", $new_seq, $ci_key);
        mysqli_stmt_execute($update_stmt);
    }

    // 提交事务,释放锁
    mysqli_commit($conn);

    // 生成最终编号
    $number = "$CI-$month$year-" . str_pad($new_seq, 4, 0, STR_PAD_LEFT);
    echo $number;

} catch (Exception $e) {
    // 出错回滚事务
    mysqli_rollback($conn);
    echo "生成编号失败,请重试";
}
?>

这个方案通过SELECT ... FOR UPDATE实现行级锁,彻底避免并发场景下的重复编号生成,性能和可靠性都更优。


方案二:预占编号表+过期清理(适配原有逻辑)

新增一个预占表,记录已被表单打开占用的编号及过期时间,防止用户打开表单后不提交导致编号长期被占用。生成编号时尝试预占,失败则自动生成下一个编号重试。

1. 创建预占表

CREATE TABLE reserved_patientno (
    patientno VARCHAR(50) PRIMARY KEY,
    reserved_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    expires_at DATETIME NOT NULL
) ENGINE=InnoDB;

2. 修改生成编号的PHP代码

<?php
$CI = "L5";
$CIcnt = strlen($CI);
$month = date("m");
$year = date("y");
$expire_minutes = 30; // 预占有效期30分钟,超时自动释放

// 先清理过期的预占记录
mysqli_query($conn, "DELETE FROM reserved_patientno WHERE expires_at < NOW()");

// 循环尝试生成并预占编号
while (true) {
    // 查询最新历史编号
    $result = mysqli_query($conn, "SELECT patientno FROM iap2 ORDER BY patientno DESC");
    $row = mysqli_fetch_assoc($result);
    $lastid = $row['patientno'] ?? null;

    // 生成新编号
    if (empty($lastid) || substr($lastid, $CIcnt + 1, 2) != $month || substr($lastid, $CIcnt + 3, 2) != $year) {
        $number = "$CI-$month$year-0001";
    } else {
        $offset = $CIcnt + 6;
        $idd = substr($lastid, $offset);
        $id = str_pad($idd + 1, 4, 0, STR_PAD_LEFT);
        $number = "$CI-$month$year-$id";
    }

    // 尝试预占该编号
    $expires_at = date("Y-m-d H:i:s", strtotime("+$expire_minutes minutes"));
    $stmt = mysqli_prepare($conn, "INSERT INTO reserved_patientno (patientno, expires_at) VALUES (?, ?)");
    mysqli_stmt_bind_param($stmt, "ss", $number, $expires_at);
    
    if (mysqli_stmt_execute($stmt)) {
        // 预占成功,返回编号
        echo $number;
        break;
    } else {
        // 预占失败(编号已被占用),继续生成下一个
        continue;
    }
}
?>

3. 表单提交后的处理

用户提交表单成功后,可删除预占记录(保留也不影响,因为业务允许最终重复):

// 提交成功后执行
$stmt = mysqli_prepare($conn, "DELETE FROM reserved_patientno WHERE patientno = ?");
mysqli_stmt_bind_param($stmt, "s", $number);
mysqli_stmt_execute($stmt);

注意事项

  • 方案一性能更优,适合高并发场景;方案二更灵活,无需修改原有业务表结构。
  • 两种方案都要求数据库使用InnoDB引擎,MyISAM不支持行级锁和事务。
  • 方案二建议通过数据库定时事件或脚本定期清理过期预占记录,避免表数据冗余。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 14:25:28