pdo不支持真正的嵌套事务,只能通过savepoint模拟:每层设唯一命名保存点,出错时rollback to对应点,commit/rollback终结全部;误用begintransaction或跨层提交会导致事务混乱。

PHP里PDO不支持真正的嵌套事务
PDO本身没有嵌套事务机制,beginTransaction() 连续调用只会开启一个事务,后续调用被忽略;commit() 一次就提交全部,rollback() 一次就回滚全部。所谓“嵌套”,其实是靠应用层模拟——用保存点(savepoint)来实现多层回滚控制。
用savepoint模拟多层事务的正确写法
核心是手动管理保存点名,每层逻辑开始前设一个,出错时只回滚到本层起点,不影响外层已执行的操作。
- 保存点名必须唯一,建议用递增序号或带上下文前缀,比如
sp_level_1、sp_payment - 调用
exec("SAVEPOINT $name")设保存点,失败要检查$pdo->errorCode() - 回滚到某层:用
exec("ROLLBACK TO $name"),不是rollback() - 提交时不需要显式释放保存点,但事务结束(
commit()或rollback())后所有保存点自动失效
try {
$pdo->beginTransaction();
$pdo->exec("SAVEPOINT sp_outer");
// 外层逻辑
$pdo->prepare("INSERT INTO orders ...")->execute();
try {
$pdo->exec("SAVEPOINT sp_inner");
// 内层逻辑
$pdo->prepare("INSERT INTO payments ...")->execute();
// 若这里异常,只回滚到 sp_inner,orders 仍保留
} catch (Exception $e) {
$pdo->exec("ROLLBACK TO sp_inner");
throw $e;
}
$pdo->commit();
} catch (Exception $e) {
$pdo->rollback();
throw $e;
}
常见错误:把beginTransaction当嵌套入口
看到“嵌套”就本能地在子函数里再调 beginTransaction(),结果事务状态混乱,甚至触发 SQLSTATE[HY000]: General error: 2014 Cannot execute queries while other unbuffered queries are active 这类非直接相关错误。
-
beginTransaction()在已有事务时静默失败,但不会报错,容易误判为“开启了新事务” - 子函数里调
commit()或rollback()会直接终结整个事务,导致外层无法控制 - 跨函数传递保存点名容易遗漏或重名,建议用栈结构管理,比如
$savepoints = [],入层array_push($savepoints, $name),出层array_pop($savepoints)
MySQL和PostgreSQL对savepoint的兼容性差异
基本语法一致,但行为细节有坑:
- MySQL 5.7+ 支持
RELEASE SAVEPOINT,但 PDO 不常用;PostgreSQL 要求保存点名符合标识符规则(不能含中划线,建议用下划线) - MySQL 在存储过程中使用 savepoint 可能与 AUTOCOMMIT 模式冲突;PDO 外部调用时,确保连接未启用
PDO::ATTR_AUTOCOMMIT => true - SQLite 支持 savepoint,但不支持命名 savepoint 的嵌套层级超过 10 层(实际很少遇到,但测试时要注意)
真正麻烦的是事务隔离级别叠加效应——比如外层 REPEATABLE READ + 内层 savepoint 回滚后,再次读取可能看到“幻读”,这不是 savepoint 的问题,而是隔离级别本身的限制。










