
本文详解为何在检查 unique_key 是否存在后仍出现重复记录,并提供基于原子操作的可靠方案,避免竞态条件,同时修复原始代码中的 SQL 注入、函数误用等严重安全隐患。
本文详解为何在检查 `unique_key` 是否存在后仍出现重复记录,并提供基于原子操作的可靠方案,避免竞态条件,同时修复原始代码中的 sql 注入、函数误用等严重安全隐患。
在 Web 应用中,常见的“先查后改”逻辑(即 SELECT → IF EXISTS THEN UPDATE ELSE INSERT)看似合理,但在并发场景下极易导致数据重复——这正是你遇到问题的根本原因:竞态条件(Race Condition)。当两个请求几乎同时执行 sql_check_row() 并都得到“不存在”的结果时,二者都会进入 INSERT 分支,最终写入两条相同 unique_key 的记录。截图中重复行的存在,正是该逻辑缺陷的直接体现。
更严重的是,原始代码存在多个高危问题,必须立即修正:
✅ SQL 注入漏洞:所有拼接 SQL 字符串的操作(如 "WHERE ".$key." = ".$value)均未转义,攻击者可通过 unique_key=' OR '1'='1 等构造注入恶意语句;
❌ 函数误用:sql_close() 中调用了已废弃且不兼容的 mysql_close()(应为 mysqli_close());
⚠️ 类型与引号错误:sql_check_row() 中 $value 直接拼入 SQL,若 $value 是字符串但未加引号(如 '".$unique_key."'),会导致语法错误或隐式类型转换失败,使查询始终返回空结果,从而错误触发插入;
? 非原子性操作:检查与更新/插入是两次独立数据库交互,无法保证事务一致性。
✅ 推荐方案:使用 INSERT ... ON DUPLICATE KEY UPDATE(MySQL)
这是最简洁、高效且原子的安全方案,前提是 unique_key 字段已设置为 UNIQUE 或 PRIMARY KEY 约束(请先确认并执行):
ALTER TABLE `xxx` ADD UNIQUE (`unique_key`);
然后重写 PHP 逻辑(使用预处理语句防注入):
<?php header('Content-Type: text/plain');
header("Access-Control-Allow-Origin: *");
include "../../functions.php";
$servername = "xxx";
$database = "xxx";
$username = "xxx";
$password = "xxx";
$tabla = "xxx";
$unique_key = $_POST['unique_key'] ?? '';
$nick = $_POST['nick'] ?? '';
$puntos = (int)$_POST['puntos']; // 强制整型,防非法输入
$con = mysqli_connect($servername, $username, $password, $database);
if (!$con) {
die("Connection failed: " . mysqli_connect_error());
}
// 使用预处理语句 + INSERT ... ON DUPLICATE KEY UPDATE
$stmt = $con->prepare("INSERT INTO `$tabla` (unique_key, nick, sc) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE nick = VALUES(nick), sc = VALUES(sc)");
$stmt->bind_param("sii", $unique_key, $nick, $puntos);
if ($stmt->execute()) {
echo $stmt->affected_rows === 1 ? "OK INSERT" : "OK UPDATE";
} else {
error_log("SQL Error: " . $stmt->error);
echo "ERROR";
}
$stmt->close();
mysqli_close($con);
?>
? 关键改进说明:
- 原子性保障:单条 SQL 完成“存在则更新,否则插入”,彻底消除竞态;
-
SQL 注入免疫:
bind_param()自动转义所有参数; -
类型安全:显式绑定参数类型(
s字符串,i整型); - 错误处理:记录错误日志而非静默失败;
-
字段约束前置:
UNIQUE索引是此方案生效的前提,也是数据完整性的底层保障。
⚠️ 注意:切勿采用答案中建议的“全量拉取再 PHP 判断”方案——它在数据量增大时性能急剧下降,且仍无法解决并发插入问题(两次请求可能同时读到旧快照)。数据库原生的
ON DUPLICATE KEY UPDATE或标准 SQL 的MERGE(MySQL 8.0.20+ 支持REPLACE INTO或INSERT ... ON CONFLICT)才是工业级正确解法。
最后,请立即删除所有手动拼接 SQL 的自定义函数(如 sql_check_row),全面迁移到预处理语句。安全与健壮性,永远比“写起来简单”重要得多。










