删除然后插入偶尔会因重复键而失败
Delete then insert occasionally fails with duplicate key
我们有一个 table 看起来像这样:
appointment_id | team_id
----------------|---------
1001 | 1
1005 | 4
1009 | 7
在这个table中,appointment_id
是主索引,team_id
只是一个普通索引。
创建table的代码:
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 是版本 7.0.27 运行ning on Ngnix 1.13.9 on Amazon Linux AMI release 2017.09.
注意:一些代码已更改以删除专有信息并简化问题,但我保留了代码的所有功能。
更新 1
需要说明的是,尽管显示了代码,但只有 1 个 PDO 连接实例在使用中。在 运行ning this code 之后,连接 ID 返回相同,这意味着它与 MySQL.
的连接相同
更新 2
这最终以某种方式成为 MySQL 内部的竞争条件;我最好的猜测是竞争条件要么在查询队列中(其中 MySQL 在查询完全 运行 之前返回到 PHP)要么是 MySQL 的内存中索引(其中 MySQL 没有在下一个查询 运行 时更新索引)
我已经完成了多个版本的测试,以尝试确保这是正在发生的事情,并且所有测试都指向这一点。如果我不得不猜测这可能会被 AWS 的配置文件之一修复,但此时我别无选择,只能求助于 tadman 建议的 REPLACE INTO
语法。
解决竞争条件的最可靠方法是首先避免出现顺序问题。用一个查询替换一对查询:
INSERT INTO `appointment_primary_teams` (
`appointment_id`,
`team_id`
) VALUES (
:appointment_id,
:team_id
) ON DUPLICATE KEY UPDATE team_id=VALUES(team_id)
这是一个原子操作,它将插入一条记录或更新一条现有记录,不需要 DELETE
。这是一种很好的 general-purpose 维护此类关系记录的方法。
另一种方法是 heavy-handed REPLACE INTO
方法:
REPLACE INTO `appointment_primary_teams` (
`appointment_id`,
`team_id`
) VALUES (
:appointment_id,
:team_id
)
这会破坏所有现有记录。 down-side 就像一个原子 DELETE
/INSERT
对,它分配新的 PRIMARY KEY
值,如果它们是 AUTO_INCREMENT
。在你的情况下不是,所以这不是问题。
获得竞争条件的方法是 INSERT
查询必须与 DELETE
查询同时为 运行。这只有在有两个连接时才有可能,这可能是因为同时接收到两个请求,它们都试图更改记录,或者因为单个实例以某种方式 运行 并行查询。
我们有一个 table 看起来像这样:
appointment_id | team_id
----------------|---------
1001 | 1
1005 | 4
1009 | 7
在这个table中,appointment_id
是主索引,team_id
只是一个普通索引。
创建table的代码:
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 是版本 7.0.27 运行ning on Ngnix 1.13.9 on Amazon Linux AMI release 2017.09.
注意:一些代码已更改以删除专有信息并简化问题,但我保留了代码的所有功能。
更新 1
需要说明的是,尽管显示了代码,但只有 1 个 PDO 连接实例在使用中。在 运行ning this code 之后,连接 ID 返回相同,这意味着它与 MySQL.
的连接相同更新 2
这最终以某种方式成为 MySQL 内部的竞争条件;我最好的猜测是竞争条件要么在查询队列中(其中 MySQL 在查询完全 运行 之前返回到 PHP)要么是 MySQL 的内存中索引(其中 MySQL 没有在下一个查询 运行 时更新索引)
我已经完成了多个版本的测试,以尝试确保这是正在发生的事情,并且所有测试都指向这一点。如果我不得不猜测这可能会被 AWS 的配置文件之一修复,但此时我别无选择,只能求助于 tadman 建议的 REPLACE INTO
语法。
解决竞争条件的最可靠方法是首先避免出现顺序问题。用一个查询替换一对查询:
INSERT INTO `appointment_primary_teams` (
`appointment_id`,
`team_id`
) VALUES (
:appointment_id,
:team_id
) ON DUPLICATE KEY UPDATE team_id=VALUES(team_id)
这是一个原子操作,它将插入一条记录或更新一条现有记录,不需要 DELETE
。这是一种很好的 general-purpose 维护此类关系记录的方法。
另一种方法是 heavy-handed REPLACE INTO
方法:
REPLACE INTO `appointment_primary_teams` (
`appointment_id`,
`team_id`
) VALUES (
:appointment_id,
:team_id
)
这会破坏所有现有记录。 down-side 就像一个原子 DELETE
/INSERT
对,它分配新的 PRIMARY KEY
值,如果它们是 AUTO_INCREMENT
。在你的情况下不是,所以这不是问题。
获得竞争条件的方法是 INSERT
查询必须与 DELETE
查询同时为 运行。这只有在有两个连接时才有可能,这可能是因为同时接收到两个请求,它们都试图更改记录,或者因为单个实例以某种方式 运行 并行查询。