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
相关产品推荐
相关产品推荐

