ORM事务处理-事务传播

事务传播(Transaction Propagation)是GoFrame ORM框架提供的一种高级事务管理机制,用于控制嵌套事务的行为。在复杂的业务场景中,一个事务方法可能会调用其他事务方法,此时需要明确定义事务的传播行为,以确保数据的一致性和完整性。

基本介绍

事务传播定义了事务方法被另一个事务方法调用时应该如何表现。GoFrame ORM支持多种事务传播行为,开发者可以根据业务需求选择合适的传播类型。

GoFrame ORM支持以下事务传播类型:

  • PropagationNested (默认):如果当前存在事务,则创建嵌套事务(使用保存点);如果不存在事务,则创建新事务。
  • PropagationRequired:如果当前存在事务,则加入该事务;如果不存在事务,则创建新事务。
  • PropagationSupports:如果当前存在事务,则加入该事务;如果不存在事务,则以非事务方式执行。
  • PropagationRequiresNew:创建新事务,如果当前存在事务,则挂起当前事务。
  • PropagationNotSupported:以非事务方式执行,如果当前存在事务,则挂起当前事务。
  • PropagationMandatory:如果当前存在事务,则加入该事务;如果不存在事务,则抛出异常。
  • PropagationNever:以非事务方式执行,如果当前存在事务,则抛出异常。

ORM事务处理-事务传播 - 图1 信息

注意事项:为保证和旧版本事务处理的兼容性,GoFrame ORM默认的事务传播类型为PropagationNested,而不是PropagationRequired。

使用优势

  1. 灵活的事务管理 :根据业务需求选择不同的事务传播行为,实现更精细的事务控制。
  2. 提高代码复用性 :服务层方法可以独立定义事务行为,无需关心调用环境是否已有事务。
  3. 降低事务冲突 :通过适当的传播行为,减少长事务导致的锁定和冲突问题。
  4. 增强错误隔离 :使用独立事务(如PropagationRequiresNew)可以隔离错误影响,避免整体事务回滚。
  5. 简化复杂业务逻辑 :在复杂业务流程中,不同步骤可以使用不同的事务传播策略,使代码更清晰。

应用场景

  1. 服务组合 :当一个服务方法调用多个其他服务方法时,可以根据业务需求选择是共享同一事务还是使用独立事务。
  2. 日志记录 :业务操作需要记录日志,但日志写入失败不应影响主要业务逻辑,可以使用PropagationRequiresNew。
  3. 批量处理 :在批量处理中,可能希望每个项目使用独立事务,以便某个项目失败不影响其他项目。
  4. 事务补偿 :在分布式系统中,可以使用不同的传播行为实现事务补偿机制。
  5. 嵌套业务逻辑 :复杂业务逻辑中的某些步骤可能需要作为一个独立的事务单元,可以使用PropagationNested。

使用示例

在开始示例之前,我们先创建一个用于测试的数据表:

CREATE TABLE IF NOT EXISTS `user` (
    id INT PRIMARY KEY,
    username VARCHAR(50)
);

ORM事务处理-事务传播 - 图2 提示

如果您想逐步运行以下示例,您需要在每次运行新示例之前清空一下user表数据。

PropagationNested (嵌套事务,默认)

如果当前存在事务,则创建嵌套事务(使用保存点);如果不存在事务,则创建新事务。

package main

import (
    _ "github.com/gogf/gf/contrib/drivers/mysql/v2"

    "context"
    "fmt"

    "github.com/gogf/gf/v2/database/gdb"
    "github.com/gogf/gf/v2/frame/g"
)

func main() {
    var (
        ctx = context.Background()
        db  = g.DB()
    )
    db.SetDebug(true)

    // 执行事务
    err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
        // 在外层事务中插入数据
        _, err := tx.Insert("user", g.Map{
            "id":       1,
            "username": "outer_user",
        })
        if err != nil {
            return err
        }

        // 嵌套事务 - 使用PropagationNested创建嵌套事务(使用保存点)
        err = tx.Transaction(ctx, func(ctx context.Context, tx2 gdb.TX) error {
            // 在嵌套事务中插入数据
            _, err = tx2.Insert("user", g.Map{
                "id":       2,
                "username": "nested_user",
            })
            if err != nil {
                return err
            }

            // 模拟错误,导致嵌套事务回滚到保存点
            return fmt.Errorf("嵌套事务故意失败")
        })

        // 嵌套事务失败,但外层事务可以继续
        fmt.Println("嵌套事务错误:", err)

        // 继续在外层事务中插入数据
        _, err = tx.Insert("user", g.Map{
            "id":       3,
            "username": "outer_after_nested",
        })
        // 外层事务正常提交
        return nil
    })

    if err != nil {
        fmt.Println("事务执行失败:", err)
        return
    }

    // 查询结果
    result, err := db.Model("user").All()
    if err != nil {
        fmt.Println("查询失败:", err)
        return
    }

    // 数据中只有id为1和3的数据,没有2
    fmt.Println("查询结果:", result)
}

上述代码执行的SQL语句类似于:

-- 开始外层事务
BEGIN;
-- 插入外层数据
INSERT INTO user(id,username) VALUES(1,'outer_user');

-- 创建保存点
SAVEPOINT sp1;
-- 插入嵌套事务数据
INSERT INTO user(id,username) VALUES(2,'nested_user');
-- 嵌套事务失败,回滚到保存点
ROLLBACK TO SAVEPOINT sp1;

-- 继续外层事务
INSERT INTO user(id,username) VALUES(3,'outer_after_nested');
-- 提交外层事务
COMMIT;

PropagationRequired (保证事务)

事务传播类型PropagationRequired与PropagationNested的主要区别在于,PropagationRequired在当前存在事务时加入该事务, 而在没有事务时创建新事务,并不会创建嵌套事务 。

package main

import (
    _ "github.com/gogf/gf/contrib/drivers/mysql/v2"

    "context"
    "fmt"

    "github.com/gogf/gf/v2/database/gdb"
    "github.com/gogf/gf/v2/frame/g"
)

func main() {
    var (
        ctx = context.Background()
        db  = g.DB()
    )
    db.SetDebug(true)

    // 执行事务
    err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
        // 在外层事务中插入数据
        // 在事务闭包中使用tx对象或者db对象来操作数据表都是等价的
        _, err := tx.Insert("user", g.Map{
            "id":       1,
            "username": "outer_user",
        })
        if err != nil {
            return err
        }

        // 嵌套事务 - 默认使用PropagationRequired
        err = tx.TransactionWithOptions(ctx, gdb.TxOptions{
            Propagation: gdb.PropagationRequired,
        }, func(ctx context.Context, tx2 gdb.TX) error {
            // 在嵌套事务中插入数据(使用相同的事务)
            // 在事务闭包中使用tx2对象或者db对象来操作数据表都是等价的
            _, err = tx2.Insert("user", g.Map{
                "id":       2,
                "username": "inner_user",
            })
            return err
        })

        return err
    })
    if err != nil {
        fmt.Println("事务执行失败:", err)
        return
    }

    // 查询结果
    result, err := db.Model("user").All()
    if err != nil {
        fmt.Println("查询失败:", err)
        return
    }

    fmt.Println("查询结果:", result)
}

上述代码执行的SQL语句类似于:

-- 开始外层事务
BEGIN;
-- 插入外层数据
INSERT INTO user(id,username) VALUES(1,'outer_user');
-- 插入内层数据(使用相同事务)
INSERT INTO user(id,username) VALUES(2,'inner_user');
-- 提交事务
COMMIT;

PropagationRequiresNew (创建新事务)

创建新事务,如果当前存在事务,则挂起当前事务。

package main

import (
    _ "github.com/gogf/gf/contrib/drivers/mysql/v2"

    "context"
    "fmt"

    "github.com/gogf/gf/v2/database/gdb"
    "github.com/gogf/gf/v2/frame/g"
)

func main() {
    var (
        ctx = context.Background()
        db  = g.DB()
    )
    db.SetDebug(true)

    // 执行事务
    err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
        // 在外层事务中插入数据
        _, err := tx.Insert("user", g.Map{
            "id":       1,
            "username": "outer_user",
        })
        if err != nil {
            return err
        }

        // 嵌套事务 - 使用PropagationRequiresNew创建新事务
        err = tx.TransactionWithOptions(ctx, gdb.TxOptions{
            Propagation: gdb.PropagationRequiresNew,
        }, func(ctx context.Context, tx2 gdb.TX) error {
            // 在新事务中插入数据
            _, err = tx2.Insert("user", g.Map{
                "id":       2,
                "username": "new_tx_user",
            })
            // 模拟错误,导致内层事务回滚
            return fmt.Errorf("内层事务故意失败")
        })

        // 内层事务失败不影响外层事务
        fmt.Println("内层事务错误:", err)

        // 继续在外层事务中插入数据
        _, err = tx.Insert("user", g.Map{
            "id":       3,
            "username": "outer_after_error",
        })
        // 外层事务正常提交
        return nil
    })

    if err != nil {
        fmt.Println("事务执行失败:", err)
        return
    }

    // 查询结果
    result, err := db.Model("user").All()
    if err != nil {
        fmt.Println("查询失败:", err)
        return
    }

    fmt.Println("查询结果:", result)
}

上述代码执行的SQL语句类似于:

-- 开始外层事务
BEGIN;
-- 插入外层数据
INSERT INTO user(id,username) VALUES(1,'outer_user');

-- 开始新的独立事务
BEGIN;
-- 插入内层数据(使用新事务)
INSERT INTO user(id,username) VALUES(2,'new_tx_user');
-- 内层事务回滚
ROLLBACK;

-- 继续外层事务
INSERT INTO user(id,username) VALUES(3,'outer_after_error');
-- 提交外层事务
COMMIT;

PropagationSupports (支持当前事务)

如果当前存在事务,则加入该事务;如果不存在事务,则以非事务方式执行。

package main

import (
    _ "github.com/gogf/gf/contrib/drivers/mysql/v2"

    "context"
    "fmt"

    "github.com/gogf/gf/v2/database/gdb"
    "github.com/gogf/gf/v2/frame/g"
)

func main() {
    var (
        ctx = context.Background()
        db  = g.DB()
    )
    db.SetDebug(true)

    // 场景1: 有外部事务时,加入外部事务
    err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
        // 在外层事务中插入数据
        _, err := tx.Insert("user", g.Map{
            "id":       1,
            "username": "outer_user",
        })
        if err != nil {
            return err
        }

        // 嵌套事务 - 使用PropagationSupports
        err = tx.TransactionWithOptions(ctx, gdb.TxOptions{
            Propagation: gdb.PropagationSupports,
        }, func(ctx context.Context, tx2 gdb.TX) error {
            // 在支持当前事务的方式下插入数据(使用外层事务)
            _, err = tx2.Insert("user", g.Map{
                "id":       2,
                "username": "supports_user",
            })
            return err
        })

        return err
    })
    if err != nil {
        fmt.Println("场景1执行失败:", err)
        return
    }

    // 查询结果
    result, err := db.Model("user").All()
    if err != nil {
        fmt.Println("查询失败:", err)
        return
    }

    fmt.Println("场景1查询结果:", result)

    // 清空数据表
    _, err = db.Exec(ctx, `TRUNCATE TABLE user`)
    if err != nil {
        fmt.Println("执行失败:", err)
        return
    }

    // 场景2: 没有外部事务时,非事务方式执行
    err = db.TransactionWithOptions(ctx, gdb.TxOptions{
        Propagation: gdb.PropagationSupports,
    }, func(ctx context.Context, tx gdb.TX) error {
        // 以非事务方式插入数据
        _, err = tx.Insert("user", g.Map{
            "id":       3,
            "username": "non_tx_user",
        })
        return err
    })
    if err != nil {
        fmt.Println("场景2执行失败:", err)
        return
    }

    // 查询结果
    result, err = db.Model("user").All()
    if err != nil {
        fmt.Println("查询失败:", err)
        return
    }

    fmt.Println("场景2查询结果:", result)
}

上述代码执行的SQL语句类似于:

-- 场景1: 有外部事务
-- 开始外层事务
BEGIN;
-- 插入外层数据
INSERT INTO user(id,username) VALUES(1,'outer_user');
-- 插入内层数据(使用外层事务)
INSERT INTO user(id,username) VALUES(2,'supports_user');
-- 提交事务
COMMIT;

-- 场景2: 没有外部事务
-- 非事务方式直接插入数据
INSERT INTO user(id,username) VALUES(3,'non_tx_user');

PropagationMandatory (强制使用事务)

如果当前存在事务,则加入该事务;如果不存在事务,则抛出异常。

package main

import (
    _ "github.com/gogf/gf/contrib/drivers/mysql/v2"

    "context"
    "fmt"

    "github.com/gogf/gf/v2/database/gdb"
    "github.com/gogf/gf/v2/frame/g"
)

func main() {
    var (
        ctx = context.Background()
        db  = g.DB()
    )
    db.SetDebug(true)

    // 场景1: 有外部事务时,加入外部事务
    err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
        // 在外层事务中插入数据
        _, err := tx.Insert("user", g.Map{
            "id":       1,
            "username": "outer_user",
        })
        if err != nil {
            return err
        }

        // 嵌套事务 - 使用PropagationMandatory
        err = tx.TransactionWithOptions(ctx, gdb.TxOptions{
            Propagation: gdb.PropagationMandatory,
        }, func(ctx context.Context, tx2 gdb.TX) error {
            // 在强制使用当前事务的方式下插入数据(使用外层事务)
            _, err = tx2.Insert("user", g.Map{
                "id":       2,
                "username": "mandatory_user",
            })
            return err
        })

        return err
    })

    if err != nil {
        fmt.Println("场景1执行失败:", err)
        return
    }

    // 查询结果
    result, err := db.Model("user").All()
    if err != nil {
        fmt.Println("查询失败:", err)
        return
    }

    fmt.Println("场景1查询结果:", result)

    // 清空数据表
    _, err = db.Exec(ctx, `TRUNCATE TABLE user`)
    if err != nil {
        fmt.Println("执行失败:", err)
        return
    }

    // 场景2: 没有外部事务时,将抛出异常
    fmt.Println("场景2: 没有外部事务时使用PropagationMandatory")
    err = db.TransactionWithOptions(ctx, gdb.TxOptions{
        Propagation: gdb.PropagationMandatory,
    }, func(ctx context.Context, tx gdb.TX) error {
        // 这里的代码不会执行,因为没有外部事务时会抛出异常
        _, err = tx.Insert("user", g.Map{
            "id":       3,
            "username": "will_not_insert",
        })
        return err
    })

    // 应该有错误,因为没有外部事务
    fmt.Println("场景2错误:", err)
}

上述代码执行的SQL语句类似于:

-- 场景1: 有外部事务
-- 开始外层事务
BEGIN;
-- 插入外层数据
INSERT INTO user(id,username) VALUES(1,'outer_user');
-- 插入内层数据(使用外层事务)
INSERT INTO user(id,username) VALUES(2,'mandatory_user');
-- 提交事务
COMMIT;

-- 场景2: 没有外部事务
-- 抛出异常: "mandatory transaction is required, but none exists"

PropagationNever (不允许在事务中执行)

以非事务方式执行,如果当前存在事务,则抛出异常。

package main

import (
    _ "github.com/gogf/gf/contrib/drivers/mysql/v2"

    "context"
    "fmt"

    "github.com/gogf/gf/v2/database/gdb"
    "github.com/gogf/gf/v2/frame/g"
)

func main() {
    var (
        ctx = context.Background()
        db  = g.DB()
    )
    db.SetDebug(true)

    // 场景1: 有外部事务时,将抛出异常
    fmt.Println("场景1: 有外部事务时使用PropagationNever")
    err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
        // 在外层事务中插入数据
        _, err := tx.Insert("user", g.Map{
            "id":       1,
            "username": "outer_user",
        })
        if err != nil {
            return err
        }

        // 嵌套事务 - 使用PropagationNever
        err = tx.TransactionWithOptions(ctx, gdb.TxOptions{
            Propagation: gdb.PropagationNever,
        }, func(ctx context.Context, tx2 gdb.TX) error {
            // 这里的代码不会执行,因为在外部事务中使用PropagationNever会抛出异常
            _, err := tx2.Insert("user", g.Map{
                "id":       2,
                "username": "will_not_insert",
            })
            return err
        })

        // 应该有错误
        fmt.Println("嵌套事务错误:", err)

        // 继续外层事务
        return nil
    })
    if err != nil {
        fmt.Println("场景1执行失败:", err)
        return
    }

    // 查询结果
    result, err := db.Model("user").All()
    if err != nil {
        fmt.Println("查询失败:", err)
        return
    }
    // 应该只有id=1的记录
    fmt.Println("场景1查询结果:", result)

    // 清空数据表
    _, err = db.Exec(ctx, `TRUNCATE TABLE user`)
    if err != nil {
        fmt.Println("执行失败:", err)
        return
    }

    // 场景2: 没有外部事务时,非事务方式执行
    err = db.TransactionWithOptions(ctx, gdb.TxOptions{
        Propagation: gdb.PropagationNever,
    }, func(ctx context.Context, tx gdb.TX) error {
        // 以非事务方式插入数据
        _, err = tx.Insert("user", g.Map{
            "id":       3,
            "username": "non_tx_user",
        })
        return err
    })
    if err != nil {
        fmt.Println("场景2执行失败:", err)
        return
    }

    // 查询结果
    result, err = db.Model("user").All()
    if err != nil {
        fmt.Println("查询失败:", err)
        return
    }
    // 应该有id=3的记录
    fmt.Println("场景2查询结果:", result)
}

上述代码执行的SQL语句类似于:

-- 场景1: 有外部事务
-- 开始外层事务
BEGIN;
-- 插入外层数据
INSERT INTO user(id,username) VALUES(1,'outer_user');
-- 抛出异常: "transaction is existing, but never transaction is required"
-- 提交外层事务
COMMIT;

-- 场景2: 没有外部事务
-- 非事务方式直接插入数据
INSERT INTO user(id,username) VALUES(3,'non_tx_user');

PropagationNotSupported (非事务方式执行)

以非事务方式执行,如果当前存在事务,则挂起当前事务。

package main

import (
    _ "github.com/gogf/gf/contrib/drivers/mysql/v2"

    "context"
    "fmt"

    "github.com/gogf/gf/v2/database/gdb"
    "github.com/gogf/gf/v2/frame/g"
)

func main() {
    var (
        ctx = context.Background()
        db  = g.DB()
    )
    db.SetDebug(true)

    // 执行事务
    err := db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
        // 在事务中插入数据
        _, err := tx.Insert("user", g.Map{
            "id":       1,
            "username": "tx_user",
        })
        if err != nil {
            return err
        }

        // 使用PropagationNotSupported挂起当前事务
        err = tx.TransactionWithOptions(ctx, gdb.TxOptions{
            Propagation: gdb.PropagationNotSupported,
        }, func(ctx context.Context, tx2 gdb.TX) error {
            // 非事务方式写入数据
            _, err = tx2.Insert("user", g.Map{
                "id":       2,
                "username": "non_tx_user",
            })
            return err
        })
        if err != nil {
            return err
        }

        // 模拟错误,导致外层事务回滚
        return fmt.Errorf("外层事务故意失败")
    })

    fmt.Println("事务执行结果:", err)

    // 查询结果
    result, err := db.Model("user").All()
    if err != nil {
        fmt.Println("查询失败:", err)
        return
    }

    // 应该只看到id=2的记录,因为id=1的记录在事务回滚时被撤销
    fmt.Println("查询结果:", result)
}

上述代码执行的SQL语句类似于:

-- 开始事务
BEGIN;
-- 插入事务内数据
INSERT INTO user(id,username) VALUES(1,'tx_user');

-- 非事务方式执行(直接提交)
INSERT INTO user(id,username) VALUES(2,'non_tx_user');

-- 外层事务回滚
ROLLBACK;

总结

GoFrame ORM的事务传播功能为开发者提供了灵活的事务管理机制,能够满足复杂业务场景下的事务处理需求。通过合理选择事务传播行为,可以实现更精细的事务控制,提高系统的可靠性和性能。

在实际应用中,建议根据业务特点选择合适的传播行为:

  1. 对于大多数场景,默认的PropagationRequired已经能够满足需求。
  2. 当需要独立事务以隔离错误影响时,可以使用PropagationRequiresNew。
  3. 对于需要保存点功能的场景,可以使用PropagationNested。
  4. 当某些操作不需要事务保护时,可以使用PropagationNotSupported。

通过合理使用事务传播机制,可以构建更加健壮和可维护的应用程序。