Php 即使锁定表,并发请求也会失败

Php 即使锁定表,并发请求也会失败,php,mysql,Php,Mysql,我为我们的后端编程了一个“谁是唯一的”小部件。它正在工作,但有时系统会抛出一个SQLSTATE[23000]:完整性约束冲突:1062重复条目'42'的密钥'user_id'MySQL错误。我真的不明白为什么会发生这种情况,因为代码运行在一个锁定的表上 让我们从表结构开始: -- -- Table structure for table `locktest` -- DROP TABLE IF EXISTS `locktest`; CREATE TABLE IF NOT EXISTS `lock

我为我们的后端编程了一个“谁是唯一的”小部件。它正在工作,但有时系统会抛出一个SQLSTATE[23000]:完整性约束冲突:1062重复条目'42'的密钥'user_id'MySQL错误。我真的不明白为什么会发生这种情况,因为代码运行在一个锁定的表上

让我们从表结构开始:

--
-- Table structure for table `locktest`
--

DROP TABLE IF EXISTS `locktest`;
CREATE TABLE IF NOT EXISTS `locktest` (
  `id` int(10) unsigned NOT NULL,
  `user_id` int(10) unsigned NOT NULL,
  `last_access` datetime NOT NULL,
  `path` varchar(20) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Test table for locking test' AUTO_INCREMENT=1 ;


--
-- Indexes for table `locktest`
--
ALTER TABLE `locktest`
 ADD PRIMARY KEY (`id`), ADD UNIQUE KEY `user_id` (`user_id`);
以下是PHP代码:

$dbh = new \PDO(...);
$dbh->beginTransaction();

$dbh->exec('LOCK TABLES locktest WRITE');

$stmt = $dbh->prepare($sql_update);
$stmt->bindValue(':user_id', $user_id, \PDO::PARAM_INT);
$stmt->bindValue(':last_access', $last_access);
$stmt->bindValue(':path', $path);

$stmt->execute();

$rows_affected = $stmt->rowCount();

if ($rows_affected == 0) {
    // New data set
    $stmt = $dbh->prepare($sql_insert);
    $stmt->bindValue(':user_id', $user_id, \PDO::PARAM_INT);
    $stmt->bindValue(':last_access', $last_access);
    $stmt->bindValue(':path', $path);

    $stmt->execute();
}

$dbh->commit();
$dbh->exec('UNLOCK TABLES');
在这个例子中,我使用PDO。但是我在mysqli和Zend_Db(使用PDO和mysqli)上也做了同样的尝试。。。这没关系:并发请求会失败,我不明白为什么

我注意到,如果删除varchar列或用另一个int列替换它,则不会看到失败的请求

另外,当我在代码之前添加一个
sleep(1)
调用时,它也在工作。看起来像是时间问题?不我真的认为使用锁应该可以防止这样的错误

我还尝试了没有事务的示例,只是为了确保锁不会干扰事务。。。没有变化

我做错什么了吗

针对PHP5.5.13、5.3.28进行测试。 针对MySQL 5.1.73和5.6.17进行测试

是的,我在用MyISAM


我创建了一个完整的小测试应用程序:

我自己发现了问题:

首先,我的代码没有问题。看起来锁定不起作用,但确实起作用了

问题是,逻辑(当更新不影响任何行时插入)并不需要智能MySQL服务器:

MySQL似乎检测到有时不需要更新,因此会向应用程序报告$infected_rows=0

什么时候会发生这种情况?

想象一下后端用户请求/$module/$action(请求1)。在本例中,$module=article和$action=add。因此,用户可以看到用于添加新数据集的web表单。如果用户立即提交空表单(/article/check,request 2),控制器将检测到并将用户重定向回/article/add(request 3),告诉他/她所需的字段

如果在同一时间内发生这种情况,则请求2和请求3无需更新,因为请求1已设置$user\u id=$user\u id、$last\u access=time()和$path=$module

如前所述,这种情况并不经常发生,但如果对同一模块的两个调用将在同一时间内发生(),则可能会发生这种情况

解决问题的两种方法:

  • 确保每个UPDATE语句都是唯一的(使用microtime(),同时记录操作…),以便UPDATE语句将始终影响一行(如果已经有来自用户的数据集)。。。这样,逻辑将再次发挥作用
  • 当UPDATE语句不影响任何行时,在INSERT之前运行SELECT语句以确保没有数据集。。。(您也可以在插入之前运行DELETE语句,但为了限制写负载,我建议使用一个非常便宜的SELECT语句……在我的情况下,这只是另一个键查找)

谢谢你的评论。

不知道为什么会这样,但是为什么不使用
INSERT。。。在重复密钥更新中…
在一个查询中完成所有操作?我们以前在重复密钥更新中使用过,但我们已重写了查询以支持基于语句的复制。用户能否从两个不同的浏览器或计算机登录?这将提供不同的id,但相同的用户id,因此会创建一个Mysql错误。为什么要把用户的唯一id放在第一位?@NikosM:我想你误读了代码:id字段从来都不是问题所在。嗯,一个用户可能使用多个浏览器/机器。因为Who is online小部件只对用户最后一次看到的位置感兴趣,所以每个用户只需要一个数据集,因此我们在用户id上设置了唯一的索引。但是,当一个用户似乎更改位置太快时,我们在单个系统上的单个用户的单个选项卡中看到了错误。。。但这就是为什么我们要使用锁定,我们认为…好吧,我明白了,如果是这样的话(即使用户从一台机器上登录,这种情况也会发生),那么其他代码中是否可能存在计时问题。我假设代码定期检查,是否可以对用户计数两次?