登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  php教程

PHP PDO 事务异常怎样保证 rollback

来源:17golang原创

时间:2026-10-20 00:35:21 474浏览 收藏

PDO 事务异常收尾的可靠写法,是由同一层拥有事务,在 try 中开始、执行和提交,在 catch (Throwable) 中检查活动事务并尝试 rollBack(),最后重新抛出原异常。回滚还需要独立保护,避免清理异常把业务错误覆盖。

这里“保证”的是异常路径有明确的回滚尝试和错误传播,不是保证任何网络、数据库或进程故障下都能成功回滚。尤其提交阶段连接中断时,客户端不能仅凭异常断言数据未提交。

PHP PDO 官方文档地址:https://www.php.net/manual/en/book.pdo.php

先确定事务由当前函数独占管理

示例面向 PHP 8 的同步调用:使用独占连接,驱动与所操作的表支持事务,进入函数前配置 PDO::ERRMODE_EXCEPTION。不能把同一个连接同时交给其他协程或任务,也不能在框架已经管理事务时再套这一层。

事务所有者负责 beginTransaction()、commit() 与 rollBack();业务回调只执行事务内工作,不执行 DDL、不调用事务控制语句、不改连接错误模式,也不吞掉本应导致整体失败的异常。这些是调用契约,不是包装器能自动强制的 SQL 安全限制。事务开始行为见 PDO::beginTransaction。

事务所有者与PDO连接及业务回调的静态职责关系
图1:事务归属结构图。看“事务所有者”分组与“业务工作”分组的职责,关系线不是执行步骤或运行证据。

用 Throwable 捕获业务与运行时异常

只捕获 PDOException 会漏掉业务主动抛出的异常;只捕获 Exception 又不能覆盖 Error 类型。Throwable 是 Error 与 Exception 的共同基础接口,适合这个收尾边界。它不意味着能够捕获进程被强制终止、所有致命环境故障或外部服务失败。

下面是完整的原创事务包装器示例,未在本文中连接数据库或执行。业务回调应返回完全取出的结果,不返回需要提交后继续读取的游标或延迟任务。

inTransaction()) {
        // 不接管框架或调用者已有的外层事务
        throw new LogicException('Existing transaction is not owned here');
    }

    $started = false;
    try {
        if (!$db->beginTransaction()) {
            throw new RuntimeException('beginTransaction failed');
        }
        $started = true;
        $result = $work($db); // 回调不得自行提交、回滚或重开事务
        if (!$db->commit()) {
            throw new RuntimeException('commit failed');
        }
        $started = false;
        return $result; // 仅在提交返回成功后交付业务结果
    } catch (Throwable $primary) {
        if ($started) {
            try {
                // 活动状态检查本身也在保护区内,防止它覆盖原异常
                if ($db->inTransaction() && !$db->rollBack()) {
                    throw new RuntimeException('rollBack returned false');
                }
            } catch (Throwable $cleanup) {
                try {
                    // 示例仅记录清理异常类型;不输出SQL、凭据或业务参数
                    error_log('transaction cleanup failed: ' . get_class($cleanup));
                } catch (Throwable $logFailure) {
                    // 日志组件故障不能取代业务最初抛出的异常
                }
            }
        }
        throw $primary; // 原对象继续交给上层,不能返回“操作成功”
    }
}

$started 只在开始事务成功后置为 true。因此开始失败时不会误回滚;回调正常返回仍要等 commit 成功才能返回上层。错误模式前提可参阅 PDO 错误处理;Throwable 的范围见 PHP Throwable。

回滚失败不能覆盖最初的异常

原异常是 $primary;清理异常是 $cleanup;受控日志只记录清理异常类型;上层处理器最终收到原异常。日志失败同样留在内部保护区,避免异常传播的主线被改写。生产日志可以通过请求关联标识把两次故障关联起来,但不应直接把 SQL、连接字符串或业务数据发给客户端。

inTransaction() 表示驱动当前是否有活动事务,不是历史事务归属或数据库结果证明。若没有活动事务,不再盲目调用 rollBack;这个分支也不能输出“回滚成功”。当回滚返回 false、抛错或无法获知状态时,上层应将连接按异常策略丢弃或隔离,不继续接下一单业务。参考 inTransaction 状态说明与 rollBack 返回值及异常。

原异常清理异常受控日志与上层处理器的关联
图2:异常保留结构图。原异常关联上层处理器,清理异常关联受控日志,两类错误不能混为同一个成功状态。

提交异常与隐式提交要单独判断

位置处理原则不能宣称
开始失败不执行回调,传播失败本函数已拥有事务
业务抛错活动事务尝试回滚,保留原异常catch 已运行所以一定撤销
提交失败按活动状态尝试清理,向上抛错业务一定没有提交
回滚失败附带清理诊断,停止复用异常连接可以继续返回成功

部分数据库的 DDL 会引发隐式提交。这样已经提交的数据,不会因为随后进入 catch 就重新变成可撤销;应把这类语句移出该事务包装范围。相关限制在 事务与自动提交中说明。

提交失败还可能涉及服务端完成提交但客户端没有收到确认。这个包装器没有提供幂等重试或事务结果对账协议,所以不能自动重跑整个回调。需要按具体数据库错误和业务记录判断,而不是统一重试。

收尾之后再释放连接与业务结果

不要在 finally 中无条件回滚:它可能面对已经提交的事务,也可能与外层管理器冲突。finally 更适合放置不改变事务结论、并且不覆盖原异常的资源清理。由谁持有连接引用、是否使用持久连接或连接池,也应由对应连接管理层处理,不能把变量置空当成服务器事务已经撤销的证明。

事务回滚只针对其覆盖的数据库修改,不撤回已经发送的邮件、HTTP 请求或文件写入。示例回调应避免这些不可随事务撤销的副作用;若确有需要,应另设计提交后处理及可靠交付机制。

回调捕获异常并返回 false 会回滚吗?

不会自动回滚。该包装器把正常返回视为可提交,因此需要失败时抛出异常,或明确增加业务结果判定规则。

业务函数可以自己 commit 吗?

不可以,这是事务所有者的职责。自行提交会破坏包装器对后续异常的收尾约定。

进程退出时自动回滚能代替 catch 吗?

不要把它当成正常错误处理方式。需要显式收尾和向上传播,且已经提交、外部副作用及连接不确定状态不因此消失。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>