当前位置:首页 > 文章列表 > 文章 > php教程 > PHP PDO 事务回滚为什么没生效:异常捕获、返回值与提交边界

PHP PDO 事务回滚为什么没生效:异常捕获、返回值与提交边界

来源:17golang原创 2026-07-21 13:03:55 0浏览 收藏

订单扣库存的接口明明走进了 beginTransaction(),库存却已经减掉,订单写入也没有撤回。PHP PDO 事务回滚失效,通常不是数据库突然不支持事务,而是异常被业务代码吞掉了,或者 commit() 的边界放错了。

把事务当成一个完整的业务边界:成功路径只提交一次,失败路径只回滚一次;不要在事务内部把异常改成普通返回值。

要点速览
  • PDO 默认异常模式不一定开启,先显式设置 PDO::ATTR_ERRMODE
  • 事务函数只负责边界,业务函数应通过异常把失败交给边界处理。
  • commit() 只能出现在所有写操作成功之后,不能夹在中间步骤。
  • 回滚后要查询数据库状态,不能只看接口返回的“操作失败”。

为什么 catch 之后,订单和库存仍然各改了一半

先看一个很常见的写法。订单创建和扣库存分别执行,但库存不足时只是返回 false

function createOrder(PDO $db, int $userId, int $skuId, int $count): bool
{
    $db->beginTransaction();

    try {
        $add = $db->prepare('INSERT INTO orders (user_id, sku_id, quantity, status) VALUES (?, ?, ?, ?)');
        $add->run([$userId, $skuId, $count, 'pending']);

        $take = $db->prepare('UPDATE stock SET quantity = quantity - ? WHERE sku_id = ? AND quantity >= ?');
        $take->run([$count, $skuId, $count]);

        if ($take->rowCount() !== 1) {
            return false;
        }

        $db->commit();
        return true;
    } catch (Throwable $e) {
        $db->rollBack();
        return false;
    }
}

问题在于 return false 会直接离开 try,不会自动跳到 catch。此时事务既没有提交,也没有回滚,连接后续被复用时可能带着未结束的事务;如果外层又做了提交,半套数据就会落库。

另一个坑是 PDO 仍处于默认错误模式。SQL 失败只返回 false 时,代码没有抛异常,commit() 仍可能继续运行。

PHP PDO 订单事务边界示意:订单写入和库存更新失败后都回到回滚分支

先把 PDO 的失败信号统一成异常

建立连接后马上设置错误模式,避免每条 SQL 都要手动检查返回值:

$db = new PDO($dsn, $username, $password, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

这样做以后,SQL 语法错误、约束冲突和连接层错误都会以 PDOException 进入异常路径。业务层仍然可以用 rowCount() 判断“库存条件是否满足”,因为库存为零不是数据库故障,而是业务拒绝。

这两个失败信号要分开处理:

现象含义处理方式
PDOExceptionSQL 或连接失败抛出,统一回滚
rowCount() 为 0库存条件不满足抛出业务异常,统一回滚
所有写入成功事务可提交只调用一次 commit()

把事务边界放在服务方法,而不是散落在 DAO 里

更稳妥的方式是:仓储方法只做单条 SQL 操作,服务方法负责事务的开启、提交和回滚。业务失败也用异常表达,这样调用方不会忘记处理未完成的事务。

final class StockNotEnough extends RuntimeException
{
}

function createOrder(PDO $db, int $userId, int $skuId, int $count): int
{
    if ($count beginTransaction();

    try {
        $add = $db->prepare(
            'INSERT INTO orders (user_id, sku_id, quantity, status) VALUES (?, ?, ?, ?)'
        );
        $add->run([$userId, $skuId, $count, 'pending']);
        $orderId = (int) $db->lastInsertId();

        $take = $db->prepare(
            'UPDATE stock SET quantity = quantity - ? WHERE sku_id = ? AND quantity >= ?'
        );
        $take->run([$count, $skuId, $count]);
        if ($take->rowCount() !== 1) {
            throw new StockNotEnough('库存不足');
        }

        $db->prepare('UPDATE orders SET status = ? WHERE id = ?')
            ->run(['paid_waiting', $orderId]);

        $db->commit();
        return $orderId;
    } catch (Throwable $e) {
        if ($db->inTransaction()) {
            $db->rollBack();
        }
        throw $e;
    }
}

这里有三个值得留意的细节。第一,commit() 紧贴成功路径末尾;第二,回滚前用 inTransaction() 判断,避免异常发生在事务开启之前又触发二次错误;第三,服务层重新抛出异常,让控制器决定返回 409、422 还是 500,而不是把所有失败都压成 200 状态码。

PHP PDO commit 与 rollBack 核对示意:成功订单提交,库存不足订单和库存都保持原状态

这几种写法看似安全,实际会留下边界漏洞

在 DAO 内部提前提交

如果订单 DAO 自己提交,库存 DAO 再失败,外层已经没有办法撤回订单数据。DAO 可以接收 PDO 实例并执行写操作,但不要替业务逻辑决定整个事务何时结束。

只判断语句运行的返回值

在异常模式下,失败会直接抛出;在静默模式下,返回值检查又很容易出现遗漏。新项目直接使用异常模式,旧项目迁移时至少要检查每一条写入语句,并禁止提交前存在失败结果。

catch 里无条件 rollBack()

连接错误可能发生在 beginTransaction() 之前,或者事务已经被其他层结束。无条件回滚会覆盖原始错误,所以先判断 inTransaction() 更稳妥。

把死锁当成普通库存不足

MySQL 的死锁通常需要记录错误信息并做有限次数重试;库存不足则是确定性的业务结果。两者都应回滚,但返回码、日志级别和重试策略不能混在一起。

用数据库状态核对提交和回滚结果

不要只用接口响应判断事务是否正确。准备一个初始库存为 10 的 SKU,分别测试成功下单、购买数量为0、库存不足和重复写入四条路径。

SELECT id, sku_id, quantity, status
FROM orders
WHERE user_id = 1007
ORDER BY id DESC
LIMIT 5;

SELECT sku_id, quantity
FROM stock
WHERE sku_id = 9001;

成功用例应同时生成一条新订单且库存对应扣减;库存不足用例应看不到新订单生成,库存仍保持为 10。如果订单数量增加但库存不变,说明写入顺序或异常边界仍有问题;如果库存减少但没有对应订单,要优先检查是否在服务方法外又调用了 commit()

测试时还应把同一个 PDO 连接放进连接池或长连接场景跑一次,验证失败后下一次请求不会继承上一次未结束的事务。

常见问题

PDO 的 beginTransaction() 调用后一定能回滚吗?

只有事务真正开启、底层表支持事务,并且后续写入仍在同一连接上时,回滚才有意义。MySQL 的非事务表不会按 InnoDB 事务规则撤回修改。

业务失败必须抛异常吗?

不一定,但事务边界必须收到一个明确的失败信号。用专门的业务异常最不容易漏掉回滚,也能让控制器区分库存不足和系统故障。

事务里能调用外部支付接口吗?

不建议长时间占用数据库事务等待外部网络响应。更合适的做法是先提交本地订单状态,再通过可靠消息或补偿任务推进支付状态。

怎么判断 rollBack() 真的执行了?

检查 publish 前后的订单和库存查询结果,并记录事务开始、提交、回滚和异常类型。只看日志里的“库存不足”提示不够,还要确认数据库没有残留异常订单。

PDO 事务最小可靠模型可以压缩成一句话:连接开启异常模式,服务方法包住完整业务,失败抛出并回滚,成功最后一次提交,随后用数据库状态复查。这样排查“回滚没生效”问题时,先看异常有没有穿过边界,再看提交是否提前发生,定位速度会快很多。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go REST API 如何统一错误响应:错误码、字段语义与兼容边界Go REST API 如何统一错误响应:错误码、字段语义与兼容边界
上一篇
Go REST API 如何统一错误响应:错误码、字段语义与兼容边界
Go json.Decoder 与 json.Unmarshal 怎么选:连续 JSON 请求的边界问题
下一篇
Go json.Decoder 与 json.Unmarshal 怎么选:连续 JSON 请求的边界问题
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    173次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    106次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    34次使用
  • LangGPT提示词框架:结构化Prompt设计方法与开源工具指南
    LangGPT
    LangGPT是一种受编程语言启发的结构化提示词设计工具,提供双层框架、模块化模板及变量功能,帮助用户高效编写高质量Prompt。该项目已在GitHub免费开源,适用于内容创作、编程辅助等多场景。
    42次使用
  • ClickPrompt:AI提示词生成与优化工具,支持Stable Diffusion、ChatGPT及代码辅助
    ClickPrompt
    ClickPrompt是一款专为AI提示词编写者设计的开源在线工具,支持Stable Diffusion绘图、ChatGPT对话及GitHub Copilot代码辅助。提供Prompt自动生成、一键运行、社区分享及可视化优化功能,帮助用户高效获取精准AI输出。
    79次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码