删除,然后偶尔插入失败,重复键

时间:2018-03-26 19:59:52

标签: php mysql insert race-condition

我们有一个看起来像这样的表:

 appointment_id | team_id
----------------|---------
 1001           | 1
 1005           | 4
 1009           | 7

在此表中,appointment_id是主要索引,team_id只是常规索引。

创建表格的代码:

CREATE TABLE `appointment_primary_teams` (
  `appointment_id` int(11) NOT NULL,
  `team_id` int(11) NOT NULL,
  PRIMARY KEY (`appointment_id`),
  KEY `team_id` (`team_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

然而,偶尔会以下代码失败:

// Even though it looks like we are making 2 different PDO connections here
// the return is the same instance of PDO shared by 2 instances of a class for
// running queries. (It is how our system allows 2 different prepared queries
// at the same time)
$remove_query = database::connect('master_db');
$insert_query = database::connect('master_db');

$remove_query->prepare("
    DELETE FROM `appointment_primary_teams` WHERE appointment_id = :appointment_id
");
$insert_query->prepare("
    INSERT INTO `appointment_primary_teams` (
        `appointment_id`,
        `team_id`
    ) VALUES (
        :appointment_id,
        :team_id
    )
");

// Looping through a list of appointment data
foreach($appointments as $appointment) {
    // Runs fine
    $remove_query->bind(':appointment_id', $appointment['id'], CAST_INT);
    $remove_query->run();

    // Occasionlly errors saying $appointment['id'] already exists
    $insert_query->bind(':appointment_id', $appointment['id'], CAST_INT);
    $insert_query->bind(':team_id', $appointment['team_id'], CAST_INT);
    $insert_query->run();
}

确切的错误是:

Database Error: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '1001' for key 'PRIMARY'

起初,我认为这是我们的API中的竞争条件,用户双击提交按钮,但我们的系统记录所有请求,我可以确认用户只发送1个请求。

我假设这是由于MySQL中某种类型的竞争条件而失败,但是我不确定如何防止它。如果这是真的,我可以告诉脚本睡眠几毫秒,但这不是一个理想的解决方案,因为如果数据库挂起,问题就会出现回来。

我的问题:导致此问题的原因是什么,如何防止此错误?

这适用于Amazon RDS服务器(MySQL 5.6.27); PHP是在Amazon Linux AMI 2017.09版本的Ngnix 1.13.9上运行的7.0.27版本。

注意:部分代码已更改为删除专有信息并简化了问题,但我保留了代码的所有功能。

更新1

要清楚,尽管显示的代码只有一个PDO连接实例正在使用中。在运行this code之后,连接ID返回相同,这意味着它与MySQL的连接相同。

更新2

这最终在某种程度上是MySQL本身的竞争条件;我最好的猜测是竞争条件要么是在查询排队中(MySQL在查询完全运行之前返回PHP),要么是MySQL的内存中索引(其中MySQL不是按时更新索引)下一个查询运行)

我已经做了几个版本的测试,试图确保它们正在发生什么,并且所有的测试都指向了这一点。如果我不得不猜测这可能是由AWS的一个配置文件修复的,但此时我别无选择,只能采用tadman所建议的REPLACE INTO语法。

1 个答案:

答案 0 :(得分:1)

确定竞争条件的最可靠方法是首先避免出现排序问题。用一个查询替换这对查询:

INSERT INTO `appointment_primary_teams` (
    `appointment_id`,
    `team_id`
) VALUES (
    :appointment_id,
    :team_id
) ON DUPLICATE KEY UPDATE team_id=VALUES(team_id)

这是一个原子操作,它将插入记录或更新现有记录,不需要DELETE。这是维护这些关系记录的一种很好的通用方法。

另一种选择是更为严厉的REPLACE INTO方法:

REPLACE INTO `appointment_primary_teams` (
    `appointment_id`,
    `team_id`
) VALUES (
    :appointment_id,
    :team_id
)

这会踩踏任何现有记录。这样做的另一面是,它就像一个原子DELETE / INSERT对,如果它们是PRIMARY KEY,它们会分配新的AUTO_INCREMENT值。在你的情况下它不是,所以这不是问题。

这种竞争条件的方式是INSERT查询必须与DELETE查询同时运行。只有在有两个连接的情况下才有可能,这可能是因为同时收到的两个请求都试图改变记录,或者因为单个实例以某种方式运行两个查询并行。