本文目录导读:

在PHP项目中解决重复数据插入,主要有以下几种常用方案,从简单到复杂:
数据库唯一约束(最推荐)
在数据库层面设置唯一索引,这是最根本的解决方案:
-- MySQL 示例 ALTER TABLE users ADD UNIQUE INDEX idx_email (email); -- 或联合唯一 ALTER TABLE orders ADD UNIQUE INDEX idx_user_product (user_id, product_id);
PHP代码中使用 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE:
// 方式1: INSERT IGNORE - 忽略重复
$sql = "INSERT IGNORE INTO users (email, name) VALUES (?, ?)";
$stmt = $pdo->prepare($sql);
$stmt->execute(['test@example.com', '张三']);
// 方式2: ON DUPLICATE KEY UPDATE - 重复时更新
$sql = "INSERT INTO users (email, name, login_count)
VALUES (?, ?, 1)
ON DUPLICATE KEY UPDATE login_count = login_count + 1";
$stmt = $pdo->prepare($sql);
$stmt->execute(['test@example.com', '张三']);
先查询后插入
适用于简单场景,但存在并发问题:
function insertIfNotExists($pdo, $email, $name) {
// 先检查是否存在
$check = $pdo->prepare("SELECT id FROM users WHERE email = ?");
$check->execute([$email]);
if ($check->rowCount() > 0) {
return ['success' => false, 'message' => '数据已存在'];
}
// 不存在则插入
$insert = $pdo->prepare("INSERT INTO users (email, name) VALUES (?, ?)");
$insert->execute([$email, $name]);
return ['success' => true, 'id' => $pdo->lastInsertId()];
}
使用 Redis 缓存/锁(高并发场景)
对于高并发场景,使用 Redis 实现分布式锁:
function insertWithRedisLock($pdo, $redis, $email, $name) {
$lockKey = "user_lock:" . md5($email);
$lockValue = uniqid('', true);
// 尝试获取锁(10秒超时)
if (!$redis->set($lockKey, $lockValue, ['NX', 'EX' => 10])) {
return ['success' => false, 'message' => '操作频繁'];
}
try {
// 再次检查数据库
$check = $pdo->prepare("SELECT id FROM users WHERE email = ?");
$check->execute([$email]);
if ($check->rowCount() === 0) {
$insert = $pdo->prepare("INSERT INTO users (email, name) VALUES (?, ?)");
$insert->execute([$email, $name]);
return ['success' => true, 'id' => $pdo->lastInsertId()];
}
return ['success' => false, 'message' => '数据已存在'];
} finally {
// 释放锁(只释放自己的锁)
$script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
$redis->eval($script, [$lockKey, $lockValue], 1);
}
}
使用批量插入避免重复
function batchInsertUnique($pdo, $data, $batchSize = 100) {
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare("INSERT IGNORE INTO users (email, name) VALUES (?, ?)");
$inserted = 0;
foreach (array_chunk($data, $batchSize) as $batch) {
foreach ($batch as $row) {
$stmt->execute([$row['email'], $row['name']]);
if ($stmt->rowCount() > 0) {
$inserted++;
}
}
}
$pdo->commit();
return ['success' => true, 'inserted' => $inserted];
} catch (Exception $e) {
$pdo->rollBack();
return ['success' => false, 'error' => $e->getMessage()];
}
}
使用事务(复杂业务逻辑)
function insertWithTransaction($pdo, $email, $name, $phone) {
$pdo->beginTransaction();
try {
// 使用 SELECT ... FOR UPDATE 加行锁
$check = $pdo->prepare("SELECT id FROM users WHERE email = ? OR phone = ? FOR UPDATE");
$check->execute([$email, $phone]);
if ($check->rowCount() > 0) {
$pdo->rollBack();
return ['success' => false, 'message' => '邮箱或手机号已存在'];
}
$insert = $pdo->prepare("INSERT INTO users (email, name, phone) VALUES (?, ?, ?)");
$insert->execute([$email, $name, $phone]);
$pdo->commit();
return ['success' => true, 'id' => $pdo->lastInsertId()];
} catch (Exception $e) {
$pdo->rollBack();
return ['success' => false, 'error' => $e->getMessage()];
}
}
使用唯一ID生成策略
// 生成基于时间的UUID,避免重复
function generateUniqueId($prefix = '') {
$time = microtime(true);
$rand = random_int(10000, 99999);
$uniq = uniqid();
return $prefix . md5($time . $rand . $uniq . $_SERVER['REMOTE_ADDR']);
}
// 使用 Snowflake 算法(分布式ID)
class SnowflakeIdGenerator {
private $workerId;
private $sequence = 0;
private $lastTimestamp = -1;
public function generate() {
$timestamp = intval(microtime(true) * 1000);
if ($timestamp === $this->lastTimestamp) {
$this->sequence = ($this->sequence + 1) & 4095;
if ($this->sequence === 0) {
while ($timestamp <= $this->lastTimestamp) {
$timestamp = intval(microtime(true) * 1000);
}
}
} else {
$this->sequence = 0;
}
$this->lastTimestamp = $timestamp;
return (($timestamp - 1609459200000) << 22) | ($this->workerId << 12) | $this->sequence;
}
}
最佳实践建议
- 生产环境优先使用方案1(数据库唯一约束 +
ON DUPLICATE KEY UPDATE) - 结合前后端验证:
- 前端通过 AJAX 实时验证唯一性
- 后端二次验证
- 日志记录:记录重复插入的尝试
function logDuplicateAttempt($pdo, $data) { $log = $pdo->prepare("INSERT INTO duplicate_logs (data, ip, time) VALUES (?, ?, NOW())"); $log->execute([json_encode($data), $_SERVER['REMOTE_ADDR']]); }
根据你的具体业务场景选择最合适的方案,一般建议 数据库约束 + 业务逻辑验证 组合使用。