我有一个应用程序,在 php + mysql 平台上运行,使用 Doctrine2 框架。我需要在一个 http 请求期间执行 3 个 db 查询:第一个 INSERT,第二个 SELECT,第三个 UPDATE。UPDATE 取决于 SELECT 查询的结果。并发http请求的概率很高。如果出现这种情况,DB查询混淆(如INS1、INS2、SEL1、SEL2、UPD1、UPD2),就会导致数据不一致。如何确保 INS-SEL-UPD 操作的原子性?我是否需要使用某种锁,或者事务就足够了?
3 回答
@YaK 的答案实际上是一个很好的答案。您应该知道一般如何处理锁。
特别针对 Doctrine2,您的代码应如下所示:
$em->getConnection()->beginTransaction();
try {
$toUpdate = $em->find('Entity\WhichWillBeUpdated', $id, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE);
// this will append FOR UPDATE http://docs.doctrine-project.org/en/2.0.x/reference/transactions-and-concurrency.html
$em->persist($anInsertedOne);
// you can flush here as well, to obtain the ID after insert if needed
$toUpdate->changeValue('new value');
$em->persist($toUpdate);
$em->flush();
$em->getConnection()->commit();
} catch (\Exception $e) {
$em->getConnection()->rollback();
throw $e;
}
获取更新的每个后续请求都将等待该事务完成,以获取一个已获得锁的进程。事务成功或失败后,Mysql 会自动释放锁。默认情况下,innodb 锁定超时为 50 秒。因此,如果您的进程未在 50 秒内完成事务,它将自动回滚并释放锁。您不需要实体上的任何其他字段。
保证表范围LOCK
在所有情况下都可以工作。但它们非常糟糕,因为它们有点防止并发,而不是处理它。但是,如果您的脚本在很短的时间内持有锁,这可能是一个可以接受的解决方案。
如果你的表使用 InnoDB 引擎(不支持使用 MyISAM 的事务),事务是最有效的解决方案,但也是最复杂的。
对于您非常具体的需求(在同一个表中,第一个 INSERT,第二个 SELECT,第三个 UPDATE 取决于 SELECT 查询的结果):
- 开始交易
- 插入您的记录。在提交您自己的事务之前,其他事务不会看到这些新行(除非您使用非标准隔离级别)
- 使用SELECT...LOCK IN SHARE MODE选择您的记录。您现在对这些行有一个 READ 锁,其他人不能更改这些行。(*)
- 计算您需要计算的任何内容以确定您是否需要更新某些内容。
- 如果需要,更新行。
- 犯罪
- 随时期待错误。如果检测到死锁,MySQL 可能会决定回滚您的事务以逃避死锁。如果另一个事务正在更新您尝试从中读取的行,则您的事务可能会被锁定一段时间,甚至超时。
如果您以这种方式进行,则可以保证您的交易的原子性。
(*) 一般而言,此 SELECT未返回的行仍可能插入到并发事务中,也就是说,除非采取适当的预防措施,否则在整个事务过程中不能保证不存在
事务不会阻止线程 B 读取线程 A 尚未锁定的值
所以你必须使用锁来防止并发访问。
@Gediminas 解释了如何在 Doctrine 中使用锁。但是使用锁可能会导致死锁或锁超时。Doctrine 将这些 SQL 错误呈现为 RetryableExceptions。 如果您处于高并发环境中,这些异常通常是正常的。它们可能经常发生,您的应用程序应该正确处理它们。
每次 Doctrine 抛出 RetryableException 时,处理此问题的正确方法是重试整个事务。
看起来很容易,但有一个陷阱。Doctrine 2 EntityManager 在 RetryableException 之后变得不可用,您必须重新创建一个新的来重播整个事务。
我用一个完整的例子写了这篇文章。