PHP项目怎么解决重复数据插入?

wen PHP项目 10

本文目录导读:

PHP项目怎么解决重复数据插入?

  1. 数据库唯一约束(最推荐)
  2. 先查询后插入
  3. 使用 Redis 缓存/锁(高并发场景)
  4. 使用批量插入避免重复
  5. 使用事务(复杂业务逻辑)
  6. 使用唯一ID生成策略
  7. 最佳实践建议

在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 IGNOREON 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. 生产环境优先使用方案1(数据库唯一约束 + ON DUPLICATE KEY UPDATE
  2. 结合前后端验证
    • 前端通过 AJAX 实时验证唯一性
    • 后端二次验证
  3. 日志记录:记录重复插入的尝试
    function logDuplicateAttempt($pdo, $data) {
     $log = $pdo->prepare("INSERT INTO duplicate_logs (data, ip, time) VALUES (?, ?, NOW())");
     $log->execute([json_encode($data), $_SERVER['REMOTE_ADDR']]);
    }

根据你的具体业务场景选择最合适的方案,一般建议 数据库约束 + 业务逻辑验证 组合使用。

抱歉,评论功能暂时关闭!