本文目录导读:

在 PHP 中,数据库事务的传播行为(Propagation Behavior)是一个概念性问题,因为 PHP 本身并没有像 Java 的 Spring 框架那样,内置一套完整的声明式事务管理机制(@Transactional)。
但我们可以模拟或理解这种传播行为,特别是在使用 Laravel、ThinkPHP 等框架,或自己封装数据库类时,事务传播行为决定了嵌套事务(事务里套事务)如何处理。
核心概念:什么是传播行为?
当程序执行到一个需要事务的方法时,它发现当前已经存在一个事务(外部事务),此时它应该怎么做? 传播行为定义的就是这种嵌套场景下的处理策略。
在 PHP 开发中,最常见的场景是服务层(Service)互相调用。
class OrderService {
public function createOrder() {
DB::beginTransaction();
try {
$this->userService->deductBalance(); // 这里开启了新事务?
$this->inventoryService->reduceStock(); // 这里又开启了新事务?
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
}
}
}
最常见的三种传播行为(PHP 开发者必须掌握)
A. 必需(REQUIRED) —— 最常用
规则:如果当前有事务,就加入(沿用);如果没有,就新建一个。 结果:内层方法抛异常,整个外层事务一起回滚。
class UserService {
public function deductBalance() {
// 假设这是 REQUIRED 行为
// 因为外层已有事务,这里不会开新事务,直接复用
DB::table('users')->decrement('balance', 100);
// 如果这里抛出异常,外层 catch 会捕获并回滚所有操作
}
}
B. 新建(REQUIRES_NEW) —— 独立事务
规则:无论当前有没有事务,都挂起当前事务,开启一个全新的独立事务。 结果:内层事务提交/回滚不影响外层事务,常用于记录日志、发送消息(不希望主流程失败影响日志记录)。
class LogService {
public function writeLog($msg) {
// 挂起外层事务,开启新事务
DB::beginTransaction();
try {
DB::table('logs')->insert(['msg' => $msg]);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
}
// 提交完成后,恢复外层事务继续执行
}
}
注意:在 MySQL 中,不支持真正的多事务并发挂起。REQUIRES_NEW 在 PHP 中通常意味着先提交外层当前操作(如果你手动调用 commit),或者使用独立的数据库连接(Connection)来处理。
C. 支持(SUPPORTS) —— 跟随环境
规则:当前有事务就加入,没有就算了(不主动创建)。 结果:通常用于只读查询操作(Select),不强制要求事务。
PHP 原生 PDO 如何模拟这些行为?
PDO 默认没有传播概念,如果你在嵌套调用中写 beginTransaction(),PDO 会抛出异常:
PDOException: There is already an active transaction
你可以通过事务计数器(嵌套深度)来模拟REQUIRED行为:
class TransactionManager {
private static $depth = 0;
private static $pdo;
public static function begin() {
if (self::$depth === 0) {
self::$pdo->beginTransaction();
}
self::$depth++;
}
public static function commit() {
self::$depth--;
if (self::$depth === 0) {
self::$pdo->commit();
}
}
public static function rollBack() {
self::$depth = 0; // 直接回滚所有
self::$pdo->rollBack();
}
}
注意:这种模拟有一个致命弱点——如果内层代码 catch 了异常并调用了 commit(),外层事务会被误提交,所以必须约定:只有最外层(depth=0)才能提交。
框架中的实际情况
Laravel(Eloquent / DB Facade)
Laravel 不支持 REQUIRES_NEW 的自动实现,它通过 savepoints(保存点)来模拟嵌套事务。
// Laravel 中的嵌套事务
DB::beginTransaction(); // 外层开启
try {
DB::beginTransaction(); // 内层:实际创建 savepoint
DB::table('users')->update(['name' => 'x']);
DB::commit(); // 内层:释放 savepoint
DB::beginTransaction(); // 再嵌套
// ...
DB::rollBack(); // 内层回滚:回到 savepoint,不影响外层
} catch (\Exception $e) {
DB::rollBack(); // 回滚整个外层
}
关键特性:内层 rollBack() 不会回滚外层已经 commit 的数据,它只是回滚到 savepoint 位置。
ThinkPHP 6
ThinkPHP 6 提供了更接近 Java 概念的 Transaction 门面,支持 trans() 闭包:
Db::transaction(function () {
// 操作1
Db::transaction(function () {
// 操作2(嵌套)
});
});
ThinkPHP 底层也是通过 savepoints 处理嵌套,如果想实现 REQUIRES_NEW,需要手动使用独立的数据库连接。
REQUIRES_NEW 在 PHP 中的正确实现
由于 MySQL 的 InnoDB 引擎不支持在一个连接上并行开启两个事务,PHP 实现 REQUIRES_NEW 只有两种办法:
- 使用第二个数据库连接(Connection Pool):
$conn1 = DB::connection(); // 主逻辑事务 $conn2 = DB::connection('mysql_secondary'); // 日志独立事务
$conn1->beginTransaction(); try { // 主业务 $conn2->beginTransaction(); $conn2->table('logs')->insert(...); $conn2->commit(); // 主业务继续 $conn1->commit(); } catch (\Exception $e) { $conn1->rollBack(); }
2. **提前提交(不推荐)**:在开启新事务前,先 commit 当前事务,这会导致**语义改变**,破坏原子性。
---
### 6. 最佳实践建议(针对 PHP 开发者)
1. **尽量避免使用 `REQUIRES_NEW`**:除非是日志或异步任务,在 MySQL 中,保存点(savepoint)足以应对绝大多数嵌套业务。
2. **统一事务边界**:事务只应该开在**最外层的 Service** 方法上,内层方法不要使用 `beginTransaction`,直接写 SQL 即可(因为方法被外层调用时,PDO 自动共享同一个连接)。
3. **异常机制**:使用 `try...catch...throw` 抛出异常,避免在内层 catch 后吞掉异常导致外层不知道失败。
4. **使用框架的 Transaction 闭包**:Laravel 的 `DB::transaction(function(){})` 会自动处理提交/回滚,并且支持嵌套(保存点)。
---
### 总结对比表
| 传播行为 | 含义 | PHP框架中的表现 | 是否推荐 |
| :--- | :--- | :--- | :--- |
| **REQUIRED** | 加入当前事务或新建 | Laravel / ThinkPHP 默认实现(保存点) | ✅ 最常用 |
| **REQUIRES_NEW** | 挂起当前,开启独立新事务 | 原生 PDO 不支持,需多连接 | ⚠️ 谨慎使用 |
| **SUPPORTS** | 有就加入,没有就算了 | 相当于普通查询 | ✅ 只读操作 |
| **NOT_SUPPORTED** | 挂起当前事务,执行非事务代码 | 需要手动 commit 当前 | ❌ 很少用 |
**核心本质**:在 PHP 中,事务传播行为 = **如何协调多个数据库操作作用于同一连接(或不同连接)上的问题**,理解这一点,你就能灵活处理各种嵌套事务场景。