我个人习惯,开发带数据库的后端时,gorm 的代码直接写在业务逻辑层,供 gin 的 handler 调用,然后每个业务逻辑层的代码都会带一个tx *gorm.DB
(事务),请求到 gin 的 handler 解析完参数后立马开一个 tx ,后边调用的所有业务逻辑代码都带上 tx ,以便一步失败后能够直接 rollback 掉整个请求所有已经执行的业务逻辑。
但是,实际工作中也见过很多大佬写的代码,包括一些开源项目,实际上看到的大家的 dao 封装时根本都不传 tx ,也没怎么见到过在接口做原子性的,一般都是在 dao 封装的时候保证这个函数中涉及的查询和操作整体原子。
1
BeautifulSoap 247 天前 2
事务塞 context 里,然后从 context 取事务。所有方法不管你有没有用到,总之规定好第一个参数就默认是 ctx context.Context ,算是 go 写业务的标准做法了
至于在哪里开启事务,我喜欢在相关复杂业务逻辑起始的地方,比如 doamin service 里,然后同时 rollback 也是在 domain service (当然 tx 这东西肯定要包装抽象一下的不能直接用)。至于别人为什么不开事务,要么就是数据库操作太简单一行 sql 结束,要么就是根本没考虑倒需要在业务层用事务(以我经验,大部分人属于后者,就是纯粹的没有项目经验想不到那一层) > 顺带问个 gorm 的问题,你们用 gorm 的话,还会把它再封装一层 dao 么,还是直接放到业务逻辑部分的代码中? repository 了解下,想好好写业务的话直接的数据库操作之类的不应该放到 doamin 层 |
2
BeautifulSoap 247 天前
@BeautifulSoap "要么就是数据库操作太简单一行 sql 结束" 这里说错了,不是数据操作太简单,而是业务太简单,涉及不到多数据的互动保存,或者干脆就是把很多本应放入业务逻辑层的逻辑给塞进 repository 这一层里了。
|
3
mooyo 247 天前
看完你写的我还是没搞清楚原子性和 Go 有什么关系。。。
|
4
Nazz 247 天前 via Android
atomic/mutex 和事务/分布式锁
|
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 |
6
seth19960929 247 天前
1. 没看明白你说的原子性, 你说的超出了 golang 的认知原子性(你要表达的是同一次请求原子性?)
2. 不需要开启事务的查询, 直接从连接池中获取 client 来查询, 需要开启事务的代码, 从连接池中获取 client, 然后所有的查询用这个 client 去操作 3. 封 ctx 肯定要传递, 不然你查询 redis, mysql. 客户端取消了请求, 你传 ctx 就可以直接中止查询, 不然请求 goroutine 还在跑 |
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, ...) } 事务对业务的侵入性也小,有一行代码的事 |
8
gitrebase 247 天前
@BeautifulSoap 想问问佬,关于 Redis 的操作也放在 Repository 层做吗
|
9
lxdlam 247 天前 1
将一个接口 wrap 进一个 db transaction 本身只保证了 db 操作的“原子性”,这还建立在本身 db 的 txn 处于正确的 isolation level 下。
所谓的接口原子性要考虑的问题远比这个复杂,如果使用了其他的后端服务,诸如 Redis 写入、第三方系统 API 调用,当非原子操作产生时,这些服务是否均支持回滚?是否保证回滚时的一致性?这样就需要从业务逻辑去考虑,然后落实到技术层面去解决,比如 Redis 是否需要 transaction 去配合?对于不支持原子的操作技术上如何取消?无法取消的事务如何在业务跟技术层面去做补偿? |
10
BeautifulSoap 247 天前 1
@gitrebase emmm 虽然是个比较复杂的问题,但就结果来说 redis 相关的操作最终放入 repository 层的情况会更多。因为即便是用 Redis ,很多时候和用 mysql 的目的也是一样的——都是为了读写 Entity 。涉及到 Entity 的读取恢复的话,那就是 repository 的职责了。
|
11
8355 247 天前
你说的接口原子性到底是啥?
同一时间同一操作的接口原子性? 还是说同一时间业务系统接口是原子性? 如果是前者不就是接口并发锁吗? 如果是后者不就是单机单线程系统吗,有处理中的请求其他请求直接阻塞等待前置处理完毕? |
12
qinze113 247 天前
最好拆分业务逻辑
|
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 |
14
keakon 247 天前
分布式的事务(比如涉及多个数据库和服务)建议用最终一致性,关系型数据库先提交,成功后再提交其他部分,失败走任务队列重试,直到成功。
|
15
EchoGroot 247 天前
关于 gorm 的使用,我是这么干的
+ https://github.com/EchoGroot/kratos-examples 涉及相关功能 + 通过 grom 操作 postgres 数据库 + 封装 gorm 的辅助工具类,提供了基础的 CRUD 方法,通过泛型实现。 命名参照 mybatisplus 的 mapper + 使用 BeforeCreate 钩子函数,自动生成 id + 封装分页查询操作 + 使用可选函数封装数据库连接初始化 |
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 不通用的部分单独写 |
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 提到的 |
18
assiadamo 247 天前
我想如果按 userId 或业务 Id ,通过取模等方法将操作函数放入同一个线程/goroutin/channel ,类似 actor 模型,是不是也能解决你说的“原子性”的问题
|
19
ninjashixuan 247 天前
确实见过直接放在 handler 的,直接 middleware 控制 commit rollback 的做法,buffalo 框架好像就是这样做,但这样感觉事务颗粒度就太细了影响性能吧。 一般我只在 service 开始开启,但传递确实可以塞在 ctx 里。
|
20
HarrisIce OP |
21
nodesolar 247 天前
1. 事务
2. 最终一致性 |
22
distleilei 247 天前
|
23
EchoGroot 246 天前
@distleilei 没看出啥,麻烦详细说下
|
24
skyqqcc581 245 天前
题主想表达的应该是,一个 http 请求 可能涉及多个事务,假设
A B 事务成功 C 事务失败时 是否应该把 A B 一起回滚? 因为假设封装了 dao 实现这个并不容易 所以直接在整个 http 请求的生命周期里用同一个事务,假设生命周期内存在任意错误,则全部回滚吧? |
25
Makabaka01 245 天前
gorm 正常用还是要封装一下的,但是你没必要自己写啊,人家提供了 gen 工具,帮你生成 repo https://gorm.io/gen/index.html
|