【问题标题】:PHP, MySQL, PDO Transaction - Can rollBack() be used after commit() has been called?PHP、MySQL、PDO 事务 - 在调用 commit() 后可以使用 rollBack() 吗?
【发布时间】:2016-09-18 16:57:15
【问题描述】:

我查看了rollBack()commit()various transaction stuff 的资源,但我找不到在commit() 已经被调用之后是否可以调用rollBack()

情况是这样的:

我有两个不同的数据库:$dbu = new PDO(..db1..)$dbb = new PDO(..db2..)

两个数据库都有在一个函数中更新的表。操作是 all or none - 要么所有表都成功更新,要么没有。

使用两个单独的事务,如果$dbu 的事务成功完成,但$dbb 的事务失败,我必须撤消第一个事务中所做的操作:

代码块 1

$dbu->beginTransaction();
try{
    $stmt = $dbu->prepare(...);
    $stmt->execute();

    // stuff

    $dbu->commit();
}catch(Exception $e){
    // do stuff

    $dbu->rollBack();
    exit();
}

$dbb->beginTransaction();
try{
    $stmt = $dbb->prepare(...);
    $stmt->execute();

    // stuff

    $dbb->commit();
}catch(Exception $e){
    // do stuff

    $dbb->rollBack();

    // Need to undo what we did
    $dbu->beginTransaction();
    try{
        $stmt = $dbu->prepare(...);
        $stmt->execute();

        // opposite of whatever operation was in the first transaction

        $dbu->commit();
    }catch(Exception $e){
    }

    exit();
}

如果两个主要事务之间的连接发生问题,这会很混乱且不可靠。

所以我想做的是将第二个事务嵌套在第一个事务中。我能够这样做似乎是合乎逻辑的,因为$dbu$dbb 是两个唯一的 PDO 对象,它们指向两个单独的数据库。它看起来像:

代码块 2

$dbu->beginTransaction();
try{
    $stmt = $dbu->prepare(...);
    $stmt->execute();

    // stuff

    $dbb->beginTransaction();
    try{
        $stmt = $dbb->prepare(...);
        $stmt->execute();

        // stuff

        $dbb->commit();
    }catch(Exception $e){
        // do stuff

        $dbb->rollBack();
        $dbu->rollBack(); // Since $dbu was first part of transaction, it needs to be rolled back too
        exit();
    }

    $dbu->commit();
}catch(Exception $e){
    // do stuff

    $dbu->rollBack();
    $dbb->rollBack(); // **THIS IS THE TRICKY LINE!**
    exit();
}

由于$dbucommit() 在整个$dbb 事务之后被调用,因此可能会出现$dbb 成功而$dbu 失败的情况。如果发生这种情况,我需要撤消在 $dbb 事务中所做的操作。

那么...

我可以打电话给$dbb->rollBack();(接近代码块2的末尾)之后$dbb->commit();已经运行了吗?还是我陷入了与最初相同的情况,我必须手动撤销$dbb 事务中发生的任何事情?同样,这并不理想。如果连接在此过程中断开,我可能会在 $dbb 表中留下不应该存在的数据(因为 $dbu 事务失败)。

也许我可以将两个交易合并成一个try/catch 块?

代码块 3

$dbu->beginTransaction();
$dbb->beginTransaction();

try{
    $stmt = $dbu->prepare(...);
    $stmt->execute();

    $stmt2 = $dbb->prepare(...);
    $stmt2->execute();

    // stuff

    $dbu->commit();
    $dbb->commit();
}catch(Exception $e){
    // do stuff

    $dbu->rollBack();
    $dbb->rollBack(); // **THIS IS THE TRICKY LINE!**
    exit();
}

但这看起来与 代码块 2 并没有太大的不同,因为我们仍然可以遇到 $dbu->commit(); 成功但 $dbb->commit(); 失败的情况。如果发生这种情况,那么在其合作伙伴提交已被处理后,我们仍会尝试调用 $dbu->rollBack();

如果我不能commit() 之后调用rollBack(),有没有常用的方法来解决这个2-DB 问题?与rollBack() 一样高效且不需要整个额外事务来撤消前一个操作的东西。

编辑 1

添加到 代码块 3,我可以在调用每个执行时验证它们吗?

代码块 4

$dbu->beginTransaction();
$dbb->beginTransaction();

try{
    $stmt = $dbu->prepare(...);
    if(!$stmt->execute()){
        throw new Exeption('something somethign');
    }

    $stmt2 = $dbb->prepare(...);
    if(!$stmt2->execute()){
        throw new Exeption('something two');
    }

    // stuff

    $dbu->commit();
    $dbb->commit();
}catch(PDOException $e){
    // do stuff

    $dbu->rollBack();
    $dbb->rollBack(); // **THIS IS THE TRICKY LINE!**
    exit();
}catch(Exception $e){
    // do stuff

    $dbu->rollBack();
    $dbb->rollBack(); // **THIS IS THE TRICKY LINE!**
    exit();
}

这是否有助于确保两个commit 语句有最大可能成功的机会?或者try/catch 块是否会在调用自定义块之前自动抛出PDOException?最好有一个简单的标识符来知道哪个事务失败,而不是整个$e->getMessage();

【问题讨论】:

    标签: php mysql pdo transactions


    【解决方案1】:

    您无法回滚已提交的更改。

    就像你的other question 代码块3 是要走的路。即使提交可能会失败,它也不会因为常见错误(例如错误的语法或违反约束或其他错误)而失败。假设整个 PHP 进程可能会在两次提交之间被杀死,重置后者让你没有机会修复代码中产生的错误。但是,您将不得不单独处理那些罕见的异常(例如备份),因为我看不到在代码中处理它们的有效方法。

    还请记住,在提交更改时已应用但尚未“发布”。所以提交本身很少会失败(仅出于特殊原因)。

    @EDIT 1

    处理错误的方式取决于您设置 PDO 实例的方式。请参阅documentation on how errors can be handled by PDO

    如果您使用默认模式(不明确设置错误模式或将其设置为 PDO::ERRMODE_SILENT),您的代码块 4 将起作用。

    【讨论】:

    • 好的,“提交本身很少失败”是有道理的,因为到那时所有的工作都已经完成了。如果您可以让我知道您的想法,如果它甚至值得添加的话,我已经在问题的末尾添加了一个编辑,并提出了一些有助于缓解问题的想法。
    • 啊,明白了。是的,我将它设置为setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);,所以我认为它甚至不会看到我的自定义异常。
    • 如果您将错误模式设置为抛出异常,您将不得不用它们自己的 try/catch 块包围每个 prepare/execute 语句以区分它们 - 这你可能已经猜到了。
    • 如果我运行具有上述属性(即PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)但没有try/catch 块的查询会发生什么?我假设只有在查询失败时才会遇到问题?
    • 是的。一旦出现错误(例如查询失败),它就会抛出异常。如果在调用堆栈中的任何地方都没有周围的try/catch,则有两种可能性:您安装了异常处理程序(php.net/manual/en/function.set-exception-handler.php),在这种情况下,未捕获的异常被传递给处理程序,或者 PHP 死于“未捕获的异常......”错误将立即停止执行(我不确定是否调用了析构函数和 co......)。请参阅php.net/manual/en/language.exceptions.php 了解更多信息。
    【解决方案2】:

    不可能在不同的数据库连接之间进行适当的事务。

    虽然你可以做一些尴尬的变通办法,但它不会让你的交易成为真正的交易。

    因此,您必须要么将所有操作保留在单个数据库中,要么忘记事务

    【讨论】:

    • 我想我要做的是保持它与 Code Block 3/4 相同,并定期在 cron 作业上运行脚本(甚至每天一次)绰绰有余)比较所有表,并删除由于错误而导致的任何不匹配。只花了几天时间将 1000 多个查询切换到像上面那样的分组事务,所以它会保持这样一段时间。也许我们将来必须更改架构和布局,或者完全选择不同的数据库。
    • @Birrel 当您想到不同的方式时,您应该明确地查看 CAP 定理 (en.wikipedia.org/wiki/CAP_theorem),因为我认为这与您的常识想要指出的内容有关。
    猜你喜欢
    • 2016-09-18
    • 2016-10-05
    • 2016-10-04
    • 2015-10-15
    • 1970-01-01
    • 2017-07-01
    • 2017-05-19
    • 1970-01-01
    • 2011-10-25
    相关资源
    最近更新 更多