Golang在整洁架构中优雅使用事务
学习资料
- kratos CLI 工具:使用命令
go install github.com/go-kratos/kratos/cmd/kratos/v2@latest
安装。 - kratos 微服务框架
- wire 依赖注入库
- 领域驱动设计思想:本文不多涉及,具备相关背景知识更佳。
在开始学习之前,先补充整洁架构与依赖注入的前置知识。
预备知识
整洁架构
Kratos 是 Go 语言的微服务框架,GitHub 星标 23k,地址:kratos。该项目提供 CLI 工具,允许用户通过 kratos new xxxx
新建项目,使用 kratos-layout 仓库的代码结构。
kratos-layout 项目为用户提供了一个典型的 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 进行依赖注入。
Service层
在 service 层,实现 RPC 接口的方法,注入 biz:
type GreeterService struct {
v1.UnimplementedGreeterServer
uc *biz.GreeterUsecase
}
func NewGreeterService(uc *biz.GreeterUsecase) *GreeterService {
return &GreeterService{uc: uc}
}
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层
在 biz 层,定义 repo 接口,注入 data 层:
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)
}
type GreeterUsecase struct {
repo GreeterRepo
log *log.Helper
}
func NewGreeterUsecase(repo GreeterRepo, logger log.Logger) *GreeterUsecase {
return &GreeterUsecase{repo: repo, log: log.NewHelper(logger)}
}
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
}
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
}
数据库连接
注入 data 作为被操作的对象:
type Data struct {
// TODO: wrapped database client
}
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/transaction/helloworld
该目录基于 go-kratos CLI 工具生成,并在此基础上修改,实现了事务支持。
运行 demo 需要准备:
- 本地数据库
dev
:root:root@tcp(127.0.0.1:3306)/dev?parseTime=True&loc=Local
- 建立表:
CREATE TABLE IF NOT EXISTS greeter (
hello VARCHAR(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
启动服务
运行服务:
go run ./cmd/helloworld/
通过 config.yaml
配置 HTTP 服务监听 localhost:8000
,GRPC 则是 localhost:9000
。
核心逻辑
helloworld 项目本质是一个打招呼服务。在 internal/biz/greeter.go
文件中,为了测试事务,在 biz 层的 CreateGreeter
方法中,调用了 repo 层的 Save
和 Update
方法,且 Update
方法人为抛出一个异常。
func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {
uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello)
var (
greeter *Greeter
err error
)
err = uc.db.ExecTx(ctx, func(ctx context.Context) error {
greeter, err = uc.repo.Save(ctx, g)
_, err = uc.repo.Update(ctx, g)
return err
})
if err != nil {
return nil, err
}
return greeter, nil
}
Repo层开启事务
为了在 repo 层共用一个事务,在 biz 层使用 db 开启事务,并将事务会话传递给 repo 层的方法。
核心实现
在 biz 层,通过优先执行 ExecTx()
方法,创建事务,并将待执行的两个 repo 方法封装在 fn 参数中,传递给 GORM 实例的 Transaction()
方法。
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 层持有的数据库实例。