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

PHP PDO 事务异常时如何避免自动提交半成品

来源:17golang原创

时间:2026-09-12 18:48:24 476浏览 收藏

订单保存经常不是一条 SQL:先写订单主表,再写明细、扣减库存,最后记录操作日志。只要中间一步失败,前面的写入就不能留在数据库里。PHP PDO 的正确做法是把这些语句放进显式事务,成功路径只在最后调用 commit(),异常路径调用 rollBack(),不要依赖“某条 SQL 报错后数据库会自动替你收尾”。

要点速览
  • 连接后启用 PDO::ERRMODE_EXCEPTION,让失败可被同一个 catch 捕获。
  • beginTransaction()commit() 是成功边界,任何异常都应进入回滚分支。
  • 事务能否真正回滚还取决于驱动、表引擎和是否执行了会隐式提交的 DDL。

先把“半成品”变成一个可回滚单元

PDO 新建连接时通常处于自动提交模式,每条写语句可能拥有自己的隐式事务。需要原子性时,必须显式调用 beginTransaction()。下面用一个订单和库存的简化场景说明边界;示例中的表名、数量和异常都是演示数据,不代表已经在本机执行。

 PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]
);

$pdo->beginTransaction();

try {
    // 主表和明细必须处在同一个事务里,避免只生成订单头。
    $order = $pdo->prepare(
        'INSERT INTO orders (user_id, total_amount) VALUES (:user_id, :total)'
    );
    $order->execute(['user_id' => 42, 'total' => 199.00]);
    $orderId = (int) $pdo->lastInsertId();

    // 库存不足或 SQL 失败时,异常会跳到 catch,不会提前提交订单。
    $stock = $pdo->prepare(
        'UPDATE inventory SET quantity = quantity - :qty
         WHERE sku = :sku AND quantity >= :qty'
    );
    $stock->execute(['qty' => 1, 'sku' => 'KB-100']);
    if ($stock->rowCount() !== 1) {
        throw new RuntimeException('库存不足,取消订单');
    }

    // 只有全部写操作成功,才把半成品变成正式数据。
    $pdo->commit();
} catch (Throwable $error) {
    // 事务可能已被数据库或驱动终止,先判断活动状态再回滚。
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }
    // 生产环境记录内部错误,返回给用户的消息应保持简洁。
    throw $error;
}
?>
PHP PDO 事务示意中的异常模式、beginTransaction 和订单库存写操作边界
图1:PHP PDO 事务的操作示意图,展示异常模式、事务入口和订单库存写操作如何被同一边界包住。

这里把 beginTransaction() 放在 try 外,是为了让“启动事务失败”和“事务内 SQL 失败”分开理解。实际项目也可以把它放进更大的 try,但捕获后不要无条件回滚:没有活动事务时,rollBack() 本身可能再次抛出异常。

异常模式解决的是“能不能回滚”,不是业务判断

PDO::ERRMODE_EXCEPTION 让执行失败抛出 PDOException,从而不会因为忘记检查 execute() 返回值而继续走到提交。它并不会替你判断库存是否足够、金额是否正确,也不会把业务规则失败自动变成异常,所以示例中仍然主动抛出 RuntimeException

写入代码建议保持一个清晰的节奏:准备参数、执行语句、检查业务结果;所有成功条件满足后才提交。不要在每一步后分别调用 commit(),否则后续失败只能回滚后半段,主表和明细就会失去原子性。

三个容易让回滚失效的边界

边界表现处理方式
表引擎或驱动不支持事务方法看似成功,数据却没有按预期回滚确认 PDO 驱动和数据库表引擎;MySQL 写表通常使用 InnoDB
事务外写入某条 SQL 在 beginTransaction() 之前执行把所有相关写操作移到事务入口之后
事务内执行 DDLCREATE TABLE、DROP TABLE 等可能触发隐式 COMMIT把结构变更和业务写入拆开,避免把 DDL 当作可回滚业务步骤

PDO 只能在驱动层判断是否支持事务,不能保证当前数据库条件一定具备回滚能力;例如 MySQL 的非事务表可能让 beginTransaction() 返回成功,但写入仍无法像 InnoDB 那样撤销。另一个常见误区是把“脚本结束自动回滚”当成业务设计。它只能作为异常退出时的安全兜底,成功路径仍应显式提交,异常路径仍应显式回滚和记录。

PHP PDO 事务结果示意中展示 commit 成功、rollBack 异常分支和 DDL 隐式提交警告
图2:PHP PDO 事务的结果示意图,对比统一提交、异常回滚和 DDL 隐式提交三种结果状态。

上线前用三组场景确认边界

  1. 成功场景:订单、明细和库存都满足条件,检查三张相关表都留下对应记录。
  2. 异常场景:在库存更新后人为抛出异常,检查订单和明细没有残留,库存也没有被扣减。
  3. 重复提交场景:故意再次调用 commit() 或重复进入事务,确认应用能记录错误,而不是静默产生第二份数据。

测试时不要只看 PHP 日志里的异常。还要在数据库连接断开后查询结果,并确认连接复用前已经结束事务。若使用多个数据库连接,PDO 事务只约束当前连接,不能自然覆盖另一个连接上的写操作。

常见问题

PDO 抛出异常后还需要手动 rollBack() 吗?

需要。异常模式负责把控制流送进 catch,不会替你完成业务层清理;在确认 inTransaction() 为真后显式回滚更清楚。

只调用 rollBack() 能撤销事务开始前的 INSERT 吗?

不能。回滚只作用于当前 beginTransaction() 之后、提交之前的操作,事务开始前的写入已经按自动提交处理。

为什么 MySQL 回滚了但数据仍在?

先检查表是否使用支持事务的引擎,再检查中间是否执行了会隐式提交的 DDL;最后确认所有语句使用的是同一个 PDO 连接。

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