前言

大家好,这里是白泽,这篇文章在 go-kratos 官方的 layout 项目的整洁架构基础上,实现优雅的数据库事务操作。

视频讲解 :B站:白泽talk,公众号【白泽talk】

本期涉及的学习资料:

在开始学习之前,先补齐一下整洁架构 & 依赖注入的前置知识。

预备知识

整洁架构

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 需要准备:

  1. 本地数据库 dev:root:root@tcp(127.0.0.1:3306)/dev?parseTime=True&loc=Local
  2. 建立表:
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
  1. 启动服务:go run ./cmd/helloworld/,通过 config.yaml 配置了 HTTP 服务监听 localhost:8000,GRPC 则是 localhost:9000。

  2. 发起一个 get 请求

核心逻辑

helloworld 项目本质是一个打招呼服务,由于 kit/transcation/helloworld 已经是魔改后的版本,为了与默认项目做对比,你可以自行生成一个 helloworld 项目,在同级目录下,对照学习。

internal/biz/greeter.go 文件中,是我更改的内容,为了测试事务,我在 biz 层的 CreateGreeter 方法中,调用了 repo 层的 SaveUpdate 两个方法,且这两个方法都会成功,但是 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在整洁架构基础上实现事务的更多相关文章

  1. Pass Infrastructure基础架构(上)

    Pass Infrastructure基础架构(上) Operation Pass OperationPass : Op-Specific OperationPass : Op-Agnostic De ...

  2. Golang优秀开源项目汇总, 10大流行Go语言开源项目, golang 开源项目全集(golang/go/wiki/Projects), GitHub上优秀的Go开源项目

    Golang优秀开源项目汇总(持续更新...)我把这个汇总放在github上了, 后面更新也会在github上更新. https://github.com/hackstoic/golang-open- ...

  3. 微服务架构基础之Service Mesh

    ServiceMesh(服务网格) 概念在社区里头非常火,有人提出 2018 年是 ServiceMesh 年,还有人提出 ServiceMesh 是下一代的微服务架构基础. 那么到底什么是 Serv ...

  4. Spring基础系列-Spring事务不生效的问题与循环依赖问题

    原创作品,可以转载,但是请标注出处地址:https://www.cnblogs.com/V1haoge/p/9476550.html 一.提出问题 不知道你是否遇到过这样的情况,在ssm框架中开发we ...

  5. 【原创】002 | 搭上SpringBoot事务源码分析专车

    前言 如果这是你第二次看到师长,说明你在觊觎我的美色! 点赞+关注再看,养成习惯 没别的意思,就是需要你的窥屏^_^ 专车介绍** 该趟专车是开往Spring Boot事务源码分析的专车 专车问题 为 ...

  6. Java进阶专题(十七) 系统缓存架构设计 (上)

    前言 ​ 我们将先从Redis.Nginx+Lua等技术点出发,了解缓存应用的场景.通过使用缓存相关技术,解决高并发的业务场景案例,来深入理解一套成熟的企业级缓存架构如何设计的.本文Redis部分总结 ...

  7. 如何在国产龙芯架构平台上运行c/c++、java、nodejs等编程语言

    高能预警:本文内容过于硬核,涉及编译器原理.cpu指令集.机器码.编程语言原理.跨平台原理等计算机专业基础知识,建议具有c.c++.java.nodejs等多种编程语言开发能力,且实战经验丰富的资深开 ...

  8. Golang 汇编asm语言基础学习

    Golang 汇编asm语言基础学习 一.CPU 基础知识 cpu 内部结构 cpu 内部主要是由寄存器.控制器.运算器和时钟四个部分组成. 寄存器:用来暂时存放指令.数据等对象.它是一个更快的内存. ...

  9. 基于ASP.NET Core 6.0的整洁架构

    大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享学习心得,希望我的文章能成为你成长路上的垫脚石,让我们一起精进. 本节将介绍基于ASP.NET Core的整洁架构的设计理念,同时基于理论落地的代码 ...

  10. 【JavaEE】SSH+Spring Security基础上配置AOP+log4j

    Spring Oauth2大多数情况下还是用不到的,主要使用的还是Spring+SpringMVC+Hibernate,有时候加上SpringSecurity,因此,本文及以后的文章的example中 ...

随机推荐

  1. Apollo quick start SampleApp demo Java

    <!--配置中心--> <dependency> <groupId>com.ctrip.framework.apollo</groupId> <a ...

  2. 效率提升利器:一个在线的.NET源码查询网站

    前言 你是否有这样的苦恼,有时候需要查询.NET中的某个类型.方法.属性或程序集的源代码,但又不想从GitHub中下载源代码.今天大姚分享一个在线且实用的.NET源码查询网站. 在线查询地址 http ...

  3. 02-CentOS7基础

    基础知识介绍 shell shell俗称壳,它包裹在内核的外面,是用户命令的翻译官. 作用:接收用户的命令,翻译后(处理一下)交给Linux内核处理. 命令 -> shell -> 内核 ...

  4. 【动手学深度学习】第三章笔记:线性回归、SoftMax 回归、交叉熵损失

    这章感觉没什么需要特别记住的东西,感觉忘了回来翻一翻代码就好. 3.1 线性回归 3.1.1 线性回归的基本元素 1. 线性模型 \(\boldsymbol{x}^{(i)}\) 是一个列向量,表示第 ...

  5. C语言:不定长结构体的实现方式

    需求 有时候,我们会遇到一些情况:数据前部分相同,但是后部分长度不固定:数据格式相似,只是尾缀的长度不同,例如某些数据包,需要不定长度. 为了能够同时使用上不同长度的数据.可以用以下的方式实现. 方案 ...

  6. 使用Github Action来辅助项目管理

    Github action 是一个Github官方提供的非常流行且速度集成 持续集成和持续交付(CI/CD)的工具.它允许你在GitHub仓库中自动化.定制和执行你的软件开发工作流.你可以发现.创建和 ...

  7. SDL3 入门(4):选择图形引擎

    SDL2 创建渲染器时只能指定使用软件渲染还是硬件加速,无法选择使用哪种图形引擎实现硬件加速.SDL3 对此做了优化,可以在创建渲染器时指定 rendering driver 也就是图形引擎,比如在 ...

  8. Swin Transformer:最佳论文,准确率和性能双佳的视觉Transformer | ICCV 2021

    论文提出了经典的Vision Transormer模型Swin Transformer,能够构建层级特征提高任务准确率,而且其计算复杂度经过各种加速设计,能够与输入图片大小成线性关系.从实验结果来看, ...

  9. 自己写一个 NODE/ATTR 的结构

    ## python 3.8 以上 from typing import Dict, List, TypeVar, Tuple, Generic, get_args import json T = Ty ...

  10. Spark3学习【基于Java】1. Spark-Sql入门程序

    spark-sql是用来处理结构化数据的模块,是入门spark的首要模块. 技术的学习无非就是去了解它的API,但是Spark有点难,因为它的例子和网上能搜到的基本都是Scala写的.我们这里使用Ja ...