
本文解析 PHP+MySQL 环境下因竞态条件、SQL 注入风险及函数设计缺陷,导致 unique_key 重复插入的根本原因,并提供基于 INSERT ... ON DUPLICATE KEY UPDATE 的安全、原子化替代方案。
本文解析 php+mysql 环境下因竞态条件、sql 注入风险及函数设计缺陷,导致 `unique_key` 重复插入的根本原因,并提供基于 `insert ... on duplicate key update` 的安全、原子化替代方案。
在您提供的代码中,看似严谨的“先查后更(或插)”逻辑(即 SELECT → IF EXISTS THEN UPDATE ELSE INSERT)却产生了重复记录,这并非偶然,而是典型的竞态条件(Race Condition) 问题:当两个并发请求几乎同时执行 sql_check_row() 并均得到“不存在”的结果时,二者都会进入 INSERT 分支,最终写入两条相同 unique_key 的记录。
更严重的是,当前实现存在多重安全隐患与设计缺陷:
- ❌ SQL 注入高危漏洞:所有函数(如
sql_check_row、sql_update_row、sql_insert_row)均直接拼接用户输入(如$_POST['unique_key']),未做任何转义或预处理,攻击者可轻易构造恶意输入执行任意 SQL; - ❌ 过时且错误的关闭函数:
sql_close()调用的是已废弃的mysql_close()(属于旧mysql_*扩展),而连接实际使用的是mysqli_connect(),将导致连接无法正确关闭,甚至引发致命错误; - ❌ 非原子操作:
SELECT与后续UPDATE/INSERT是两次独立查询,中间无事务或锁保护,无法保证数据一致性; - ❌ 冗余多次查询:为一次更新需执行 1 次
SELECT+ 2 次UPDATE(分别更新nick和sc),效率低下且易出错。
✅ 推荐解决方案:使用 INSERT ... ON DUPLICATE KEY UPDATE(MySQL 原生 UPSERT)
前提是:数据库表中 unique_key 字段必须定义为 UNIQUE 或 PRIMARY KEY(这是该语句生效的前提)。请先确认并执行(如未设置):
ALTER TABLE `xxx` ADD UNIQUE KEY `uk_unique_key` (`unique_key`);
然后重写 send_score.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']; // 强制转为整型,防注入
// 使用预处理语句防止 SQL 注入
$con = mysqli_connect($servername, $username, $password, $database);
if (!$con) {
die("Connection failed: " . mysqli_connect_error());
}
$stmt = mysqli_prepare($con,
"INSERT INTO `$tabla` (`unique_key`, `nick`, `sc`) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE `nick` = VALUES(`nick`), `sc` = VALUES(`sc`)"
);
mysqli_stmt_bind_param($stmt, "sii", $unique_key, $nick, $puntos);
if (mysqli_stmt_execute($stmt)) {
echo "OK UPSERT"; // 无论插入或更新均成功
} else {
echo "ERROR: " . mysqli_error($con);
}
mysqli_stmt_close($stmt);
mysqli_close($con);
?>? 关键优势说明:
- ✅ 原子性保障:整个操作在单条 SQL 内完成,数据库引擎自动处理“存在则更新,不存在则插入”,彻底规避竞态;
- ✅ 零 SQL 注入风险:全程使用
mysqli_prepare+bind_param,参数与 SQL 逻辑严格分离; - ✅ 高效简洁:一次查询完成全部逻辑,无需多次往返数据库;
- ✅ 健壮类型控制:对
puntos强制(int)转换,避免字符串注入或非法数值。
⚠️ 重要提醒:
- 切勿再使用原始代码中的
mysql_*函数(已从 PHP 7.0+ 移除); -
sql_close()中的mysql_close()必须删除,改用mysqli_close(); - 若无法修改表结构添加
UNIQUE约束,请优先修复此根本问题——否则任何应用层逻辑都无法真正保证唯一性; - 生产环境务必启用错误报告(
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT))并捕获异常,而非仅靠if ($sql)粗粒度判断。
通过采用原生 UPSERT 语句并配合预处理机制,您不仅能彻底解决重复行问题,更能大幅提升代码安全性、可维护性与执行性能。

















