V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
HarrisIce
V2EX  ›  Go 编程语言

大家做 go 后端开发时,都是怎么处理接口操作的原子性的?

  •  
  •   HarrisIce · 247 天前 · 3748 次点击
    这是一个创建于 247 天前的主题,其中的信息可能已经有所发展或是发生改变。

    背景

    我个人习惯,开发带数据库的后端时,gorm 的代码直接写在业务逻辑层,供 gin 的 handler 调用,然后每个业务逻辑层的代码都会带一个tx *gorm.DB(事务),请求到 gin 的 handler 解析完参数后立马开一个 tx ,后边调用的所有业务逻辑代码都带上 tx ,以便一步失败后能够直接 rollback 掉整个请求所有已经执行的业务逻辑。

    但是,实际工作中也见过很多大佬写的代码,包括一些开源项目,实际上看到的大家的 dao 封装时根本都不传 tx ,也没怎么见到过在接口做原子性的,一般都是在 dao 封装的时候保证这个函数中涉及的查询和操作整体原子。

    疑问

    1. 像我那样的“接口原子性”有实际意义吗? 99%的场景其实没用么?
    2. go 的 database 框架中的 tx ,reddit 确实有见到过 best practice 把 tx 在整个 gin 的 handler 传做接口原子的,这个思路是对的吗?
    3. 顺带问个 gorm 的问题,你们用 gorm 的话,还会把它再封装一层 dao 么,还是直接放到业务逻辑部分的代码中?
    25 条回复    2024-03-09 11:25:36 +08:00
    BeautifulSoap
        1
    BeautifulSoap  
       247 天前   ❤️ 2
    事务塞 context 里,然后从 context 取事务。所有方法不管你有没有用到,总之规定好第一个参数就默认是 ctx context.Context ,算是 go 写业务的标准做法了

    至于在哪里开启事务,我喜欢在相关复杂业务逻辑起始的地方,比如 doamin service 里,然后同时 rollback 也是在 domain service (当然 tx 这东西肯定要包装抽象一下的不能直接用)。至于别人为什么不开事务,要么就是数据库操作太简单一行 sql 结束,要么就是根本没考虑倒需要在业务层用事务(以我经验,大部分人属于后者,就是纯粹的没有项目经验想不到那一层)

    > 顺带问个 gorm 的问题,你们用 gorm 的话,还会把它再封装一层 dao 么,还是直接放到业务逻辑部分的代码中?

    repository 了解下,想好好写业务的话直接的数据库操作之类的不应该放到 doamin 层
    BeautifulSoap
        2
    BeautifulSoap  
       247 天前
    @BeautifulSoap "要么就是数据库操作太简单一行 sql 结束" 这里说错了,不是数据操作太简单,而是业务太简单,涉及不到多数据的互动保存,或者干脆就是把很多本应放入业务逻辑层的逻辑给塞进 repository 这一层里了。
    mooyo
        3
    mooyo  
       247 天前
    看完你写的我还是没搞清楚原子性和 Go 有什么关系。。。
    Nazz
        4
    Nazz  
       247 天前 via Android
    atomic/mutex 和事务/分布式锁
    heww
        5
    heww  
       247 天前
    ORM? 那个 interface (可以时 db 也可以是 tx ) 放 ctx ,操作数据库时从 ctx 获取 DB 来处理,这样处理数据库的部分不用关心用的是 tx 还是 db ,除非那部分操作需要 tx 。http server 那边通过一个 middleware 把 ORM interface 注入到 ctx 并开启一个 tx ,http handler 失败了 rollback ,成功了 commit 。这种情况下 http handler 内部的操作还需要 tx 的话 orm 里应该运行的是 nested tx 。

    可以参考几年前我给 harbor 添加的这部分实现(主要为了解决之前的版本一个 API 成功一部分导致的脏数据问题),把 middleware 和 orm 换成自己的就可以了。

    middlwares

    https://github.com/goharbor/harbor/blob/main/src/server/middleware/orm/orm.go
    https://github.com/goharbor/harbor/blob/main/src/server/middleware/transaction/transaction.go


    db operation

    https://github.com/goharbor/harbor/blob/main/src/pkg/blob/dao/dao.go#L111

    db operation requires tx

    https://github.com/goharbor/harbor/blob/main/src/pkg/project/dao/dao.go#L91
    seth19960929
        6
    seth19960929  
       247 天前
    1. 没看明白你说的原子性, 你说的超出了 golang 的认知原子性(你要表达的是同一次请求原子性?)
    2. 不需要开启事务的查询, 直接从连接池中获取 client 来查询, 需要开启事务的代码, 从连接池中获取 client, 然后所有的查询用这个 client 去操作
    3. 封

    ctx 肯定要传递, 不然你查询 redis, mysql. 客户端取消了请求, 你传 ctx 就可以直接中止查询, 不然请求 goroutine 还在跑
    thevita
        7
    thevita  
       247 天前   ❤️ 1
    同 1 楼,放 ctx 里,

    开启事务:

    ...
    transactions.RunInTx(ctx, fun(ctx context.Context) error {
    // biz logic
    })
    ...

    repository 实现

    func (r *Repository) FindXXX(ctx context.Context, id string) (xxx, error) {
    return transactions.Db(ctx).Find(ctx, ...)
    }


    事务对业务的侵入性也小,有一行代码的事
    gitrebase
        8
    gitrebase  
       247 天前
    @BeautifulSoap 想问问佬,关于 Redis 的操作也放在 Repository 层做吗
    lxdlam
        9
    lxdlam  
       247 天前   ❤️ 1
    将一个接口 wrap 进一个 db transaction 本身只保证了 db 操作的“原子性”,这还建立在本身 db 的 txn 处于正确的 isolation level 下。

    所谓的接口原子性要考虑的问题远比这个复杂,如果使用了其他的后端服务,诸如 Redis 写入、第三方系统 API 调用,当非原子操作产生时,这些服务是否均支持回滚?是否保证回滚时的一致性?这样就需要从业务逻辑去考虑,然后落实到技术层面去解决,比如 Redis 是否需要 transaction 去配合?对于不支持原子的操作技术上如何取消?无法取消的事务如何在业务跟技术层面去做补偿?
    BeautifulSoap
        10
    BeautifulSoap  
       247 天前   ❤️ 1
    @gitrebase emmm 虽然是个比较复杂的问题,但就结果来说 redis 相关的操作最终放入 repository 层的情况会更多。因为即便是用 Redis ,很多时候和用 mysql 的目的也是一样的——都是为了读写 Entity 。涉及到 Entity 的读取恢复的话,那就是 repository 的职责了。
    8355
        11
    8355  
       247 天前
    你说的接口原子性到底是啥?
    同一时间同一操作的接口原子性?
    还是说同一时间业务系统接口是原子性?

    如果是前者不就是接口并发锁吗?
    如果是后者不就是单机单线程系统吗,有处理中的请求其他请求直接阻塞等待前置处理完毕?
    qinze113
        12
    qinze113  
       247 天前
    最好拆分业务逻辑
    qloog
        13
    qloog  
       247 天前   ❤️ 2
    我的分层是这样的:
    handler -> service -> dao/repository -> model

    事务的开启是在 service , 操作数据的是在 dao 或 repo 层,model 仅仅定义数据字段和表名(无任何 db 操作)。

    PS:也像楼主一样考虑过,将这些事务放在一起,且放置于 dao 里,也就不用传递 tx 了,但会带来一个问题: 一个 dao 要操作多个 model (或者说表), 我目前是倾向于一个 dao 操作一个 model ,这样 dao 的职责就很清晰, 也方便 cli 工具自动生成 dao 。

    @gitrebase 提到的 redis 操作,我把他们都放在了 cache 目录里(和 dao 、service 在同一级), 然后供 dao/repo 调用,也就是 dao/repo 扮演了数据操作的角色,不关是 接口、db 、redis 、MongoDB 、ES 等都在这里操作,供上层的 service 调用,一个 service 可以调用多个 dao, 只依赖 dao 定义的接口,方便使用 wire 做依赖注入。

    代码可参考: https://github.com/go-microservice/moment-service/blob/main/internal/service/post_svc.go#L88
    keakon
        14
    keakon  
       247 天前
    分布式的事务(比如涉及多个数据库和服务)建议用最终一致性,关系型数据库先提交,成功后再提交其他部分,失败走任务队列重试,直到成功。
    EchoGroot
        15
    EchoGroot  
       247 天前
    关于 gorm 的使用,我是这么干的
    + https://github.com/EchoGroot/kratos-examples

    涉及相关功能
    + 通过 grom 操作 postgres 数据库
    + 封装 gorm 的辅助工具类,提供了基础的 CRUD 方法,通过泛型实现。 命名参照 mybatisplus 的 mapper
    + 使用 BeforeCreate 钩子函数,自动生成 id
    + 封装分页查询操作
    + 使用可选函数封装数据库连接初始化
    nobject
        16
    nobject  
       247 天前
    1. 接口原子性没太明白,如果是数据库操作的原子性的话,就是开启事务。如果是接口的,那加个分布式锁?
    2. tx 一般在 service 层有个接口实现在 ctx 中注入 orm ,接口的实现在 repo 层,一般就如下使用方式:
    ```
    type Transaction interface {
    TX(ctx context.Context, fn func(ctx context.Context) error) error
    }

    func (d *Repo) TX(ctx context.Context, fn func(ctx context.Context) error) error {
    return d.db.Transaction(func(tx *gorm.DB) (err error) {
    defer func() {
    if e := recover(); e != nil {
    err = fmt.Errorf("%#v", e)
    // log
    }
    }()
    ctx = context.WithValue(ctx, contextTxKey{}, tx)
    err = fn(ctx)
    return err
    })
    }

    ```
    service 层调用各 repo:
    ```

    s.tm.TX(ctx, func(ctx context.Context) error {
    if err := s.Repo1.Create(ctx, ...); err != nil{
    return err
    }
    return nil
    })
    ```
    3. 个人会封装一层,用于常规的 curd 实现,然后各个 dao 不通用的部分单独写
    qloog
        17
    qloog  
       247 天前
    补充:如果想简单操作,也可以在 dao 里(一个 dao 操作多个 model 也还好)统一操作, 利用 gorm 的 Transaction 函数:

    ```go
    // dao/example.go
    // 开始事务
    tx := db.Begin()
    if tx.Error != nil {
    log.Fatalf("failed to begin transaction: %v", tx.Error)
    }

    // 事务中执行业务逻辑
    err = tx.Transaction(func(tx *gorm.DB) error {
    // 插入一条数据
    err := tx.Create(&Model{Name: "test"}).Error
    if err != nil {
    return err
    }

    // 查询数据
    var model Model
    err = tx.First(&model, "name = ?", "test").Error
    if err != nil {
    return err
    }

    fmt.Printf("Found: %v\n", model)

    // 更新数据
    err = tx.Model(&model).Update("name", "updated").Error
    if err != nil {
    return err
    }

    fmt.Printf("Updated: %v\n", model)

    return nil
    })

    // 根据事务结果提交或回滚
    if err != nil {
    tx.Rollback()
    log.Fatalf("transaction failed: %v", err)
    } else {
    tx.Commit()
    fmt.Println("transaction committed successfully")
    }
    ```

    类似于 @thevita 提到的
    assiadamo
        18
    assiadamo  
       247 天前
    我想如果按 userId 或业务 Id ,通过取模等方法将操作函数放入同一个线程/goroutin/channel ,类似 actor 模型,是不是也能解决你说的“原子性”的问题
    ninjashixuan
        19
    ninjashixuan  
       247 天前
    确实见过直接放在 handler 的,直接 middleware 控制 commit rollback 的做法,buffalo 框架好像就是这样做,但这样感觉事务颗粒度就太细了影响性能吧。 一般我只在 service 开始开启,但传递确实可以塞在 ctx 里。
    HarrisIce
        20
    HarrisIce  
    OP
       247 天前
    没想到晚上来看竟然这么多大佬回复,多谢各位。

    评论中蛮多说没搞明白原子性什么意思,我没表达清,其实就是想说单纯说 mysql 的事务 2333 ,还没有用到 redis ,单纯希望在围绕 mysql 的业务流中,业务流和你使用的 atomic 包一样都是原子的不可再切分的。

    另外多谢 @BeautifulSoap @heww @thevita @qloog @lxdlam @EchoGroot @nobject 的详细解释,看起来确实还是放在 ctx 里主流一些,容我研究研究。
    nodesolar
        21
    nodesolar  
       247 天前
    1. 事务
    2. 最终一致性
    EchoGroot
        23
    EchoGroot  
       246 天前
    @distleilei 没看出啥,麻烦详细说下
    skyqqcc581
        24
    skyqqcc581  
       245 天前
    题主想表达的应该是,一个 http 请求 可能涉及多个事务,假设
    A B 事务成功
    C 事务失败时 是否应该把 A B 一起回滚?

    因为假设封装了 dao 实现这个并不容易

    所以直接在整个 http 请求的生命周期里用同一个事务,假设生命周期内存在任意错误,则全部回滚吧?
    Makabaka01
        25
    Makabaka01  
       245 天前
    gorm 正常用还是要封装一下的,但是你没必要自己写啊,人家提供了 gen 工具,帮你生成 repo https://gorm.io/gen/index.html
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2588 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 02:34 · PVG 10:34 · LAX 18:34 · JFK 21:34
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.