Golang在整洁架构基础上实现事务操作
作者:白泽talk
前言
大家好,这里是白泽,这篇文章在 go-kratos 官方的 layout 项目的整洁架构基础上,实现优雅的数据库事务操作。
本期涉及的学习资料:
- 我的开源Golang学习仓库:https://github.com/BaiZe1998/go-learning,这期的所有内容汇聚成一个可运行的 demo,
kit/transaction
路径下。 - kratos CLI 工具:
go install github.com/go-kratos/kratos/cmd/kratos/v2@latest
。 - kratos 微服务框架:https://github.com/go-kratos/kratos
- wire 依赖注入库:https://github.com/google/wire
- 领域驱动设计思想:本文不多涉及,具备相关背景知识食用本文更佳。
在开始学习之前,先补齐一下整洁架构 & 依赖注入的前置知识。
预备知识
整洁架构
kratos 是 Go 语言的一个微服务框架,github 🌟 23k,https://github.com/go-kratos/kratos
该项目提供了 CLI 工具,允许用户通过 kratos new xxxx
,新建一个 xxxx 项目,这个项目将使用 kratos-layout 仓库的代码结构。
仓库地址:https://github.com/go-kratos/kratos-layout
kratos-layout 项目为用户提供的,配合 CLI 工具生成的一个典型的 Go 项目布局看起来像这样:
application |____api | |____helloworld | | |____v1 | | |____errors |____cmd | |____helloworld |____configs |____internal | |____conf | |____data | |____biz | |____service | |____server |____test |____pkg |____go.mod |____go.sum |____LICENSE |____README.md
依赖注入
🌟 通过依赖注入,实现了资源的使用和隔离,同时避免了重复创建资源对象,是实现整洁架构的重要一环。
kratos 的官方文档中提到,十分建议用户尝试使用 wire 进行依赖注入,整个 layout 项目,也是基于 wire,完成了整洁架构的搭建。
service 层,实现 rpc 接口定义的方法,实现对外交互,注入了 biz。
// GreeterService is a greeter service. type GreeterService struct { v1.UnimplementedGreeterServer uc *biz.GreeterUsecase } // NewGreeterService new a greeter service. func NewGreeterService(uc *biz.GreeterUsecase) *GreeterService { return &GreeterService{uc: uc} } // SayHello implements helloworld.GreeterServer. func (s *GreeterService) SayHello(ctx context.Context, in *v1.HelloRequest) (*v1.HelloReply, error) { g, err := s.uc.CreateGreeter(ctx, &biz.Greeter{Hello: in.Name}) if err != nil { return nil, err } return &v1.HelloReply{Message: "Hello " + g.Hello}, nil }
biz 层:定义 repo 接口,注入 data 层。
// GreeterRepo is a Greater repo. type GreeterRepo interface { Save(context.Context, *Greeter) (*Greeter, error) Update(context.Context, *Greeter) (*Greeter, error) FindByID(context.Context, int64) (*Greeter, error) ListByHello(context.Context, string) ([]*Greeter, error) ListAll(context.Context) ([]*Greeter, error) } // GreeterUsecase is a Greeter usecase. type GreeterUsecase struct { repo GreeterRepo log *log.Helper } // NewGreeterUsecase new a Greeter usecase. func NewGreeterUsecase(repo GreeterRepo, logger log.Logger) *GreeterUsecase { return &GreeterUsecase{repo: repo, log: log.NewHelper(logger)} } // CreateGreeter creates a Greeter, and returns the new Greeter. func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) { uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello) return uc.repo.Save(ctx, g) }
data 作为数据访问的实现层,实现了上游接口,注入了数据库实例资源。
type greeterRepo struct { data *Data log *log.Helper } // NewGreeterRepo . func NewGreeterRepo(data *Data, logger log.Logger) biz.GreeterRepo { return &greeterRepo{ data: data, log: log.NewHelper(logger), } } func (r *greeterRepo) Save(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) { return g, nil } func (r *greeterRepo) Update(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) { return g, nil } func (r *greeterRepo) FindByID(context.Context, int64) (*biz.Greeter, error) { return nil, nil } func (r *greeterRepo) ListByHello(context.Context, string) ([]*biz.Greeter, error) { return nil, nil } func (r *greeterRepo) ListAll(context.Context) ([]*biz.Greeter, error) { return nil, nil }
db:注入 data,作为被操作的对象。
type Data struct { // TODO wrapped database client } // NewData . func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) { cleanup := func() { log.NewHelper(logger).Info("closing the data resources") } return &Data{}, cleanup, nil }
Golang 优雅事务
准备
🌟 项目获取:强烈建议克隆仓库后实机操作。
git clone git@github.com:BaiZe1998/go-learning.git cd kit/transcation/helloworld
这个目录基于 go-kratos CLI 工具使用 kratos new helloworld
生成,并在此基础上修改,实现了事务支持。
运行 demo 需要准备:
- 本地数据库 dev:
root:root@tcp(127.0.0.1:3306)/dev?parseTime=True&loc=Local
- 建立表:
CREATE TABLE IF NOT EXISTS greater ( hello VARCHAR(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
ps:Makefile 中提供了使用 goose 进行数据库变更管理的能力(goose 也是一个开源的高 🌟 项目,推荐学习)
up: goose mysql "root:root@tcp(localhost:3306)/dev?parseTime=true" up down: goose mysql "root:root@tcp(localhost:3306)/dev?parseTime=true" down create: goose mysql "root:root@tcp(localhost:3306)/dev?parseTime=true" create ${name} sql
启动服务:
go run ./cmd/helloworld/
,通过config.yaml
配置了 HTTP 服务监听 localhost:8000,GRPC 则是 localhost:9000。发起一个 get 请求
核心逻辑
helloworld
项目本质是一个打招呼服务,由于 kit/transcation/helloworld
已经是魔改后的版本,为了与默认项目做对比,你可以自行生成一个 helloworld
项目,在同级目录下,对照学习。
在 internal/biz/greeter.go
文件中,是我更改的内容,为了测试事务,我在 biz 层的 CreateGreeter
方法中,调用了 repo 层的 Save
和 Update
两个方法,且这两个方法都会成功,但是 Update
方法人为抛出一个异常。
// CreateGreeter creates a Greeter, and returns the new Greeter. func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) { uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello) var ( greater *Greeter err error ) //err = uc.db.ExecTx(ctx, func(ctx context.Context) error { // // 更新所有 hello 为 hello + "updated",且插入新的 hello // greater, err = uc.repo.Save(ctx, g) // _, err = uc.repo.Update(ctx, g) // return err //}) greater, err = uc.repo.Save(ctx, g) _, err = uc.repo.Update(ctx, g) if err != nil { return nil, err } return greater, nil } // Update 人为抛出异常 func (r *greeterRepo) Update(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) { result := r.data.db.DB(ctx).Model(&biz.Greeter{}).Where("hello = ?", g.Hello).Update("hello", g.Hello+"updated") if result.RowsAffected == 0 { return nil, fmt.Errorf("greeter %s not found", g.Hello) } return nil, fmt.Errorf("custom error") //return g, nil }
repo 层开启事务
如果忽略上文注释中的内容,因为两个 repo 的数据库操作都是独立的。
func (r *greeterRepo) Save(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) { result := r.data.db.DB(ctx).Create(g) return g, result.Error } func (r *greeterRepo) Update(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) { result := r.data.db.DB(ctx).Model(&biz.Greeter{}).Where("hello = ?", g.Hello).Update("hello", g.Hello+"updated") if result.RowsAffected == 0 { return nil, fmt.Errorf("greeter %s not found", g.Hello) } return nil, fmt.Errorf("custom error") //return g, nil }
即使最后抛出 Update 的异常,但是 save 和 update 都已经成功了,且彼此不强关联,数据库中会多增加一条数据。
biz 层开启事务
因此为了 repo 层的两个方法能够共用一个事务,应该在 biz 层就使用 db 开启事务,且将这个事务的会话传递给 repo 层的方法。
🌟 如何传递:使用 context 便成了顺理成章的方案。
接下来将 internal/biz/greeter.go
文件中注释的部分释放,且注释掉分开使用事务的两行,此时重新运行项目请求接口,则由于 Update 方法抛出 err,导致事务回滚,未出现新增的 xiaomingupdated
记录。
// CreateGreeter creates a Greeter, and returns the new Greeter. func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) { uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello) var ( greater *Greeter err error ) err = uc.db.ExecTx(ctx, func(ctx context.Context) error { // 更新所有 hello 为 hello + "updated",且插入新的 hello greater, err = uc.repo.Save(ctx, g) _, err = uc.repo.Update(ctx, g) return err }) //greater, err = uc.repo.Save(ctx, g) //_, err = uc.repo.Update(ctx, g) if err != nil { return nil, err } return greater, nil }
核心实现
由于 biz 层的 Usecase 实例持有 *DBClient
,repo 层也持有 *DBClient
,且二者在依赖注入的时候,代表同一个数据库连接池实例。
在 pkg/db/db.go
中,为 *DBClient
提供了如下两个方法: ExecTx()
& DB()
。
在 biz 层,通过优先执行 ExecTx()
方法,创建事务,以及将待执行的两个 repo 方法封装在 fn 参数中,传递给 gorm 实例的 Transaction()
方法待执行。
同时在 Transcation 内部,触发 fn() 函数,也就是聚合的两个 repo 操作,需要注意的是,此时将携带 contextTxKey 事务 tx 的 ctx 作为参数传递给了 fn 函数,因此下游的两个 repo 可以获取到 biz 层的事务会话。
type contextTxKey struct{} // ExecTx gorm Transaction func (c *DBClient) ExecTx(ctx context.Context, fn func(ctx context.Context) error) error { return c.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { ctx = context.WithValue(ctx, contextTxKey{}, tx) return fn(ctx) }) } func (c *DBClient) DB(ctx context.Context) *gorm.DB { tx, ok := ctx.Value(contextTxKey{}).(*gorm.DB) if ok { return tx } return c.db }
在 repo 层执行数据库操作的时候,尝试通过 DB()
方法,从 ctx 中获取到上游传递下来的事务会话,如果有则使用,如果没有,则使用 repo 层自己持有的 *DBClient
,进行数据访问操作。
func (r *greeterRepo) Save(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) { result := r.data.db.DB(ctx).Create(g) return g, result.Error } func (r *greeterRepo) Update(ctx context.Context, g *biz.Greeter) (*biz.Greeter, error) { result := r.data.db.DB(ctx).Model(&biz.Greeter{}).Where("hello = ?", g.Hello).Update("hello", g.Hello+"updated") if result.RowsAffected == 0 { return nil, fmt.Errorf("greeter %s not found", g.Hello) } return nil, fmt.Errorf("custom error") //return g, nil }
参考文献
到此这篇关于Golang在整洁架构基础上实现事务的文章就介绍到这了,更多相关Golang实现事务内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!