PHP 数据库事务传播行为

wen PHP项目 4

本文目录导读:

PHP 数据库事务传播行为

  1. 核心概念:什么是传播行为?
  2. 最常见的三种传播行为(PHP 开发者必须掌握)
  3. PHP 原生 PDO 如何模拟这些行为?
  4. 框架中的实际情况
  5. 关于 REQUIRES_NEW 在 PHP 中的正确实现

在 PHP 中,数据库事务的传播行为(Propagation Behavior)是一个概念性问题,因为 PHP 本身并没有像 Java 的 Spring 框架那样,内置一套完整的声明式事务管理机制(@Transactional)。

但我们可以模拟理解这种传播行为,特别是在使用 LaravelThinkPHP 等框架,或自己封装数据库类时,事务传播行为决定了嵌套事务(事务里套事务)如何处理。


核心概念:什么是传播行为?

当程序执行到一个需要事务的方法时,它发现当前已经存在一个事务(外部事务),此时它应该怎么做? 传播行为定义的就是这种嵌套场景下的处理策略。

在 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 只有两种办法:

  1. 使用第二个数据库连接(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 中,事务传播行为 = **如何协调多个数据库操作作用于同一连接(或不同连接)上的问题**,理解这一点,你就能灵活处理各种嵌套事务场景。

抱歉,评论功能暂时关闭!