理解PDO MySQL事务

17

PHP文档表示:

如果您以前从未遇到过事务,它们提供4个主要功能:原子性、一致性、隔离性和持久性(ACID)。通俗地说,在事务中进行的任何工作,即使是分阶段进行的,也保证在提交时安全地应用于数据库,并且不受其他连接的干扰。

问题:

这是否意味着我可以同时运行两个独立的php脚本来执行事务而互不干扰?


关于我所说的“干涉”的详细说明:

想象一下,我们有以下的employees表:

 __________________________
|  id  |  name  |  salary  |
|------+--------+----------|
|  1   |  ana   |   10000  |
|------+--------+----------|

如果我有两个脚本,它们的代码相似/相同,并且它们在完全相同的时间运行: script1.phpscript2.php(两者都具有相同的代码):
$conn->beginTransaction();

$stmt = $conn->prepare("SELECT * FROM employees WHERE name = ?");
$stmt->execute(['ana']);
$row = $stmt->fetch(PDO::FETCH_ASSOC);

$salary = $row['salary'];
$salary = $salary + 1000;//increasing salary

$stmt = $conn->prepare("UPDATE employees SET salary = {$salary} WHERE name = ?");
$stmt->execute(['ana']);

$conn->commit(); 

假设事件序列如下:
- script1.php 选择数据 - script2.php 选择数据 - script1.php 更新数据 - script2.php 更新数据 - script1.php commit() 发生 - script2.php commit() 发生
在这种情况下,Ana 的工资将是多少?
- 它会是11000吗?这是否意味着1个事务将重叠另一个事务,因为信息是在任何提交之前获得的? - 它会是12000吗?这是否意味着无论更新和选择数据的顺序如何,commit()函数都会强制单独发生这些操作?
请随意详细阐述事务和单独的脚本如何相互干扰(或不干扰)。

我非常确定其中一个脚本会出现“SQLSTATE[23000]:完整性约束冲突”错误。问题是,如果它们同时运行,哪一个会出错。 - icecub
3个回答

13

你在php或pdo文档中找不到答案,因为这与php或pdo无关。

mysql中的innodb表引擎根据sql标准提供了4种所谓的隔离级别。这些隔离级别与阻塞/非阻塞读取结合使用将决定上述示例的结果。您需要了解各种隔离级别的影响,并选择适合您需求的隔离级别。

总之:如果您使用串行化隔离级别并关闭自动提交,则结果将是12000。在所有其他隔离级别和启用自动提交的串行化隔离级别中,结果将是11000。如果开始使用锁定读取,则结果在所有隔离级别下均可达到12000。


9
根据给定的条件(一个孤立的DML语句),您不需要在这里使用事务,而是需要表锁。这是一个非常常见的混淆。
如果您需要确保所有的DML语句都被正确执行或者根本没有执行,那么您需要一个事务。
意味着:
  • 对于任意数量的SELECT查询,您都不需要事务
  • 如果只执行了一个DML语句,则不需要事务
虽然,正如Shadow的优秀答案中所指出的那样,您可以在此处使用具有适当隔离级别的事务,但这会令人困惑。您需要的是表锁定。InnoDB引擎允许您锁定特定行,而不是锁定整个表,因此应该优先选择。
如果您希望工资为1200,则使用表锁定。
或者-更简单的方法-只需运行一个原子更新查询:
UPDATE employees SET salary = salary + 1000 WHERE name = ?

在这种情况下,所有的工资都将被记录。
如果您的目标不同,请明确地表达出来。
但是需要注意:总体上讲,事务与单独脚本的执行无关。关于您所关心的“竞态条件”主题,您应该关注的不是事务,而是表/行锁定。这是非常常见的混淆,您最好了解清楚:
- 事务是为确保一个脚本中一组DML查询成功执行而设立的。 - 表/行锁定是为确保其他脚本执行不会干扰。
事务和锁定相互影响的唯一主题是死锁,但需要注意的是:只有在事务使用锁定的情况下才会出现死锁。

我认为问题的目的是为了理解ACID中的I,而不是以特定方式执行特定操作。至少这是我的理解。我不同意您关于表锁定的观点。在InnoDB中,您可以使用SELECT语句进行行级锁定,因此您不必锁定整个表才能最终获得12000。我同意在上面的代码中没有必要进行选择,因为整个操作可以在单个更新中执行。但是,如果这仅是旨在了解事务隔离的示例,则达到了目的。 - Shadow
我使用“表锁定”作为一个通用术语,但需要注意的是,在InnoDB中应该优先选择行级锁定。因此,我认为你的评论更多是一个术语问题。就问题意义而言,我可能是最擅长理解问题并已经回答了15年的人。对于OP来说,ACID问题是一个XY问题,他们只是在研究自己特定问题时遇到了它。无论如何,我认为我们两个的答案都相互印证,这对读者来说更好。 - Your Common Sense

5
哎呀,"无干扰"需要程序员的帮助。需要用BEGINCOMMIT来定义“事务”的范围。而且...
你的例子不充分。第一条语句需要SELECT ... FOR UPDATE。这告诉事务处理可能会对SELECT所获取的行进行UPDATE,这个提醒对于 "防止干扰" 是至关重要的。现在时间轴如下:
-
  • script1.php BEGINs
  • -
  • script2.php BEGINs
  • -
  • script1.php 选择数据 (FOR UPDATE)
  • -
  • script2.php 选择数据被阻塞,等待
  • -
  • script1.php 更新数据
  • -
  • script1.php commit() 发生
  • -
  • script2.php 选择数据 (将获得新提交的值)
  • -
  • script2.php 更新数据
  • -
  • script2.php commit() 发生
  • (注:这不是 '死锁',只是一个 '等待'。)

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接