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

262 天前
 HarrisIce

背景

我个人习惯,开发带数据库的后端时,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 么,还是直接放到业务逻辑部分的代码中?
3784 次点击
所在节点    Go 编程语言
25 条回复
BeautifulSoap
262 天前
事务塞 context 里,然后从 context 取事务。所有方法不管你有没有用到,总之规定好第一个参数就默认是 ctx context.Context ,算是 go 写业务的标准做法了

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

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

repository 了解下,想好好写业务的话直接的数据库操作之类的不应该放到 doamin 层
BeautifulSoap
262 天前
@BeautifulSoap "要么就是数据库操作太简单一行 sql 结束" 这里说错了,不是数据操作太简单,而是业务太简单,涉及不到多数据的互动保存,或者干脆就是把很多本应放入业务逻辑层的逻辑给塞进 repository 这一层里了。
mooyo
262 天前
看完你写的我还是没搞清楚原子性和 Go 有什么关系。。。
Nazz
262 天前
atomic/mutex 和事务/分布式锁
heww
262 天前
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
262 天前
1. 没看明白你说的原子性, 你说的超出了 golang 的认知原子性(你要表达的是同一次请求原子性?)
2. 不需要开启事务的查询, 直接从连接池中获取 client 来查询, 需要开启事务的代码, 从连接池中获取 client, 然后所有的查询用这个 client 去操作
3. 封

ctx 肯定要传递, 不然你查询 redis, mysql. 客户端取消了请求, 你传 ctx 就可以直接中止查询, 不然请求 goroutine 还在跑
thevita
262 天前
同 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
262 天前
@BeautifulSoap 想问问佬,关于 Redis 的操作也放在 Repository 层做吗
lxdlam
262 天前
将一个接口 wrap 进一个 db transaction 本身只保证了 db 操作的“原子性”,这还建立在本身 db 的 txn 处于正确的 isolation level 下。

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

如果是前者不就是接口并发锁吗?
如果是后者不就是单机单线程系统吗,有处理中的请求其他请求直接阻塞等待前置处理完毕?
qinze113
262 天前
最好拆分业务逻辑
qloog
262 天前
我的分层是这样的:
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
262 天前
分布式的事务(比如涉及多个数据库和服务)建议用最终一致性,关系型数据库先提交,成功后再提交其他部分,失败走任务队列重试,直到成功。
EchoGroot
262 天前
关于 gorm 的使用,我是这么干的
+ https://github.com/EchoGroot/kratos-examples

涉及相关功能
+ 通过 grom 操作 postgres 数据库
+ 封装 gorm 的辅助工具类,提供了基础的 CRUD 方法,通过泛型实现。 命名参照 mybatisplus 的 mapper
+ 使用 BeforeCreate 钩子函数,自动生成 id
+ 封装分页查询操作
+ 使用可选函数封装数据库连接初始化
nobject
262 天前
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
262 天前
补充:如果想简单操作,也可以在 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
262 天前
我想如果按 userId 或业务 Id ,通过取模等方法将操作函数放入同一个线程/goroutin/channel ,类似 actor 模型,是不是也能解决你说的“原子性”的问题
ninjashixuan
262 天前
确实见过直接放在 handler 的,直接 middleware 控制 commit rollback 的做法,buffalo 框架好像就是这样做,但这样感觉事务颗粒度就太细了影响性能吧。 一般我只在 service 开始开启,但传递确实可以塞在 ctx 里。
HarrisIce
262 天前
没想到晚上来看竟然这么多大佬回复,多谢各位。

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

另外多谢 @BeautifulSoap @heww @thevita @qloog @lxdlam @EchoGroot @nobject 的详细解释,看起来确实还是放在 ctx 里主流一些,容我研究研究。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1021397

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX