尽管锁定表格,并发请求仍将失败

时间:2014-06-05 19:40:26

标签: php mysql

我编写了一个"谁只是"我们后端的小部件。它正在工作,但有时系统会抛出 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)调用时,它也正常工作。看起来像计时问题?不?我真的以为使用LOCKS可以防止这样的错误...

我还尝试了没有TRANSACTIONS的示例,只是为了确保LOCK不会干扰TRANSACTIONS ......没有变化。

我做错了吗?

针对PHP 5.5.13,5.3.28进行了测试。 针对MySQL 5.1.73和5.6.17进行了测试。

是的,我正在使用MyISAM。

我创建了一个小型的完整测试应用程序:https://www.dropbox.com/s/77t9jy596vodmax/locktest.zip

1 个答案:

答案 0 :(得分:1)

我自己发现了这个问题:

首先,我的代码没有任何问题。看起来LOCKING不起作用,但确实如此。

问题是,逻辑(当UPDATE不影响任何行时INSERT)不期望智能MySQL服务器:

MySQL似乎发现有时无需更新,因此会将 $ affected_rows = 0 报告回应用程序。

什么时候发生?

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

如果在同一时间内发生这种情况,则无法更新请求2和请求3,因为请求1已设置 $ user_id = $ user_id $ last_access = time() $ path = $ module

如上所述,这种情况并不经常发生,但如果在相同的时间()内对同一模块进行两次调用,则可能会发生这种情况。

解决问题的两种方法:

  • 确保每个UPDATE语句都是唯一的(使用 microtime(),同时记录操作 ...),以便UPDATE语句将始终影响一行已经存在来自用户的数据集...以便逻辑再次起作用。
  • 当UPDATE语句不影响任何行时,在INSERT之前运行SELECT语句以确保没有数据集...(你也可以在INSERT之前运行DELETE语句但是限制WRITE加载我推荐一个非常便宜的SELECT语句...在我的情况下,它只是另一个关键的查找。)

感谢所有评论。