Doctrine中的并发

put*_*uty 4 php mysql concurrency transactions doctrine-orm

我有一个应用程序,使用Doctrine2框架在php + mysql平台上运行.我需要在一个http请求期间执行3个db查询:第一个INSERT,第二个SELECT,第三个UPDATE.UPDATE取决于SELECT查询的结果.并发http请求的概率很高.如果发生这种情况,并且DB查询混淆(例如,INS1,INS2,SEL1,SEL2,UPD1,UPD2),则会导致数据不一致.如何确保INS-SEL-UPD操作的原子性?我需要使用某种锁,还是交易就足够了?

Ged*_*nas 8

来自@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;
}
Run Code Online (Sandbox Code Playgroud)

获取更新的每个后续请求将等到此事务完成一个已获得锁定的进程.事务成功完成或失败后,Mysql将自动释放锁.默认情况下,innodb锁定超时为50秒.因此,如果您的进程在50秒内未完成事务,它将回滚并自动释放锁定.您的实体不需要任何其他字段.


Ran*_*eed 2

全表范围LOCK保证在所有情况下都能工作。但它们非常糟糕,因为它们在某种程度上阻止了并发,而不是处理它。但是,如果您的脚本在很短的时间内持有锁,那么这可能是一个可接受的解决方案。

如果你的表使用InnoDB引擎(不支持MyISAM事务),事务是最高效的解决方案,但也是最复杂的。

对于您非常具体的需求(在同一个表中,第一个 INSERT,第二个 SELECT,第三个 UPDATE 取决于 SELECT 查询的结果):

  1. 开始交易
  2. 插入您的记录。在您自己的事务提交之前,其他事务不会看到这些新行(除非您使用非标准隔离级别
  3. 使用SELECT...LOCK IN SHARE MODE选择您的记录。您现在对这些行有一个读锁,其他人不能更改这些行。(*)
  4. 计算您需要计算的任何内容以确定是否需要更新某些内容。
  5. 如果需要,更新行。
  6. 犯罪
  7. 随时预计会出现错误。如果检测到死锁,MySQL 可能会决定回滚您的事务以避免死锁。如果另一个事务正在更新您尝试读取的行,您的事务可能会被锁定一段时间,甚至超时。

如果您按照这种方式进行,则可以保证事务的原子性。

(*) 一般来说,此 SELECT返回的行仍可能被插入到并发事务中,也就是说,在整个事务过程中不能保证不存在,除非采取适当的预防措施