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

标签 php mysql

我为我们的后端编写了一个“Who is only”小部件。它可以工作,但有时系统会抛出 SQLSTATE[23000]: Integrityconstraint Violation: 1062 Duplicateentry '42' for key '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

最佳答案

我自己发现了问题:

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

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

MySQL 似乎检测到有时不需要更新,因此将 $affected_rows = 0 报告回应用程序。

什么时候发生?

想象一下后端用户请求/$module/$action(请求1)。在此示例中,$module =article$action = add。因此,用户会看到一个用于添加新数据集的 Web 表单。如果用户立即提交空表单(/article/check,请求 2), Controller 将检测到这一点并将用户重定向回 /article/add(请求 3) )告诉他/她必填字段。

当这种情况同时发生时,请求 2 和请求 3 无需更新任何内容,因为请求 1 已设置$user_id = $user_id$last_access = time()$path = $module

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

解决问题的两种方法:

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

感谢大家的评论。

关于php - 尽管锁定表,并发请求仍将失败,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24068856/

相关文章:

php - 使用 PHP 对照数组中的值检查值并返回键

PHP 将默认参数传递给函数

php - 如何在 laravel 5.1 中将类的方法作为 cron 任务运行

php - 在循环触发时在php中查询但给出一个变量的结果

php - 将无限播放列表(数组)保存到数据库的最佳方法? (php mysql)

mysql 连接具有特殊数据的表

android - SQLite 数据库中的表情符号

php - 字符为 '¡' 的奇怪字符串行为

php - 如果从另一个表连接时记录中包含重复项,则显示记录中的一个名称

mysql - 更改mysql数据库的位置