
本文系统解析数据库重复插入的常见成因,涵盖前端ajax重复调用、php脚本隐式执行、sql注入式双重执行及并发与设计缺陷,并提供可落地的防重方案(prg模式、token机制、唯一约束、事务控制等)。
本文系统解析数据库重复插入的常见成因,涵盖前端ajax重复调用、php脚本隐式执行、sql注入式双重执行及并发与设计缺陷,并提供可落地的防重方案(prg模式、token机制、唯一约束、事务控制等)。
在Web开发中,「表单提交一次却插入两条记录」是高频且棘手的问题。它并非总是源于明显的逻辑错误,而常隐藏于看似无害的代码细节中——正如您提供的案例所示:一个AJAX请求调用 save.php,返回单个ID,但日志或数据库却显示两次插入。经过排查,真正元凶竟是PHP数据库封装函数中一句被忽视的 if (!$conn->query($sql)) { ... } ——该行代码在执行条件判断时,已对 $sql 完成了一次真实执行,导致后续 mysqli_query($conn, $sql) 再次执行,形成“静默双写”。
? 根本原因剖析:不止是前端重复点击
虽然用户连点、浏览器刷新、未禁用按钮等前端诱因广为人知,但您的案例揭示了一个更隐蔽的层级:服务端脚本自身的非幂等性。
1. PHP层:隐式SQL执行陷阱(核心问题)
查看 poke_db() 函数:
if( !$conn -> query($sql) ){ // ⚠️ 危险!此处已执行 $sql 一次!
die('[ERR] ' . $conn -> error);
}
// ... 后续又执行:
$result = mysqli_query($conn, $sql); // ✅ 再次执行 → 双插入!mysqli::query() 是执行方法,不是校验方法。此 if 判断不仅未提升健壮性,反而成为重复插入的直接推手。修复方式极其简单:
立即学习“PHP免费学习笔记(深入)”;
// ✅ 正确写法:仅执行一次,用返回值判断成败
$result = $conn->query($sql);
if (!$result) {
die('[ERR] ' . $conn->error);
}2. 前端层:AJAX绑定与事件冒泡风险
您的JS中使用了内联 onclick="opnActPag.addAct()",但需警惕:
- 若 .act-text-btn-ok 元素被多次初始化(如动态重绘、模块重复加载),opnActPag.addAct 可能被重复绑定;
- <div onclick="..."> 在某些场景下可能触发事件冒泡,导致多次捕获;
- 无提交按钮禁用机制:用户快速连点仍可能发出多个请求。
✅ 推荐加固写法(jQuery + 防抖 + 状态锁):
opnActPag['addAct'] = function () {
const btn = $('#act_subm_txt');
if (btn.hasClass('disabled')) return; // 防重复
btn.addClass('disabled').text('提交中...');
$.ajax({
type: 'POST',
url: 'save.php',
data: {
act_txt: $('#act_txt_area').val(),
id: $('#act_hid_val').val(),
usr: $('#act_hid_usr').val()
},
success: function(data) {
console.log('插入成功:', data);
},
error: function() {
alert('保存失败,请重试');
},
complete: function() {
btn.removeClass('disabled').text('✓'); // 恢复状态
}
});
};3. 后端层:安全与幂等双重缺失
- SQL注入漏洞:save.php 中直接拼接 $_POST 参数,极易被恶意利用,且 preg_replace 和 htmlentities 无法替代参数化查询;
- 无幂等校验:虽有 SELECT COUNT(*) 判断,但未加事务或锁,在高并发下仍可能因竞态条件(TOCTOU)导致重复插入;
- 缺少CSRF防护:无Token验证,开放接口易被跨站伪造请求。
✅ 安全重构建议(PDO + 预处理 + 事务):
<?php
session_start();
// 1. CSRF Token 验证(需前端同步传入)
if (!isset($_POST['token']) || $_POST['token'] !== $_SESSION['csrf_token']) {
http_response_code(403);
exit('Invalid request');
}
if (isset($_POST['act_txt']) && !empty(trim($_POST['act_txt']))) {
$pdo = new PDO("mysql:host=localhost;dbname=dbname", "root", "pass");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $pdo->prepare("SELECT COUNT(*) FROM actions WHERE user_name = ? AND info = ? AND level_3_id = ?");
$stmt->execute([$_POST['usr'], trim($_POST['act_txt']), (int)$_POST['id']]);
if ($stmt->fetchColumn() === "0") {
$pdo->beginTransaction();
try {
$insert = $pdo->prepare("INSERT INTO actions (user_name, info, level_3_id) VALUES (?, ?, ?)");
$insert->execute([$_POST['usr'], trim($_POST['act_txt']), (int)$_POST['id']]);
$pdo->commit();
echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]);
} catch (Exception $e) {
$pdo->rollback();
throw $e;
}
} else {
echo json_encode(['success' => false, 'message' => '记录已存在']);
}
}
?>?️ 四重防御体系:生产环境必备实践
| 层级 | 措施 | 说明 |
|---|---|---|
| 前端 | 按钮禁用 + 请求节流 + Token携带 | 阻断90%人为重复操作 |
| 传输层 | 使用 POST + PRG 模式(重定向) | 防止F5刷新重复提交,提升UX |
| 应用层 | CSRF Token + 幂等Key(如业务唯一ID) | 识别并拒绝重复请求 |
| 数据层 | 唯一索引 + 事务 + 行级锁 | 数据库兜底,强制一致性(例:ALTER TABLE actions ADD UNIQUE KEY uk_user_info_level (user_name, info, level_3_id);) |
? 关键提醒:唯一索引不仅是防重手段,更是数据质量的最后防线。即使所有逻辑层都失效,数据库也能通过 Duplicate entry 错误明确告知冲突,避免脏数据入库。
✅ 总结:一次排查,终身免疫
重复插入从来不是单一环节的故障,而是前端交互、网络行为、脚本执行、数据库设计、并发模型五者耦合的结果。解决问题不能只盯“哪里多了一次”,而要建立「防御纵深」:
- 立即修复 poke_db() 中的隐式执行;
- 为所有写操作添加 INSERT ... ON DUPLICATE KEY UPDATE 或 REPLACE INTO(配合唯一键);
- 在关键业务入口统一注入幂等ID(如UUIDv4),并在数据库建 idempotency_id 字段+唯一索引;
- 开启MySQL慢日志与通用查询日志,结合 error_log() 打印时间戳与请求标识,精准定位双写源头。
真正的健壮性,始于对每一行代码副作用的敬畏。



















