Golang在整洁架构基础上实现事务

news2024/9/21 14:41:19

前言

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

视频讲解 📺:B站:白泽talk

image-20240726234405804

本期涉及的学习资料:

  • 我的开源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

image-20240806235306095

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 请求

image-20240807005017171

核心逻辑

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 都已经成功了,且彼此不强关联,数据库中会多增加一条数据。

image-20240807005400189

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
}

参考文献

  • https://lailin.xyz/post/clean-arch-transaction.html

  • https://github.com/pressly/goose

  • https://github.com/go-kratos/kratos

  • https://go-kratos.dev/docs/getting-started/usage

  • https://gorm.io/zh_CN/docs/update.html

  • https://www.cnblogs.com/zhanchenjin/p/17855944.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1990570.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【随笔】详解Java POI及其使用方法

引言 随着企业和开发者对数据处理需求的不断增加,操作Excel文件已经成为日常编程工作的重要部分。在Java中,Apache POI(Poor Obfuscation Implementation)库虽然首页其貌不扬,但它绝对是处理Excel文件的强大工具。本文…

JavaWeb之servlet关于Ajax实现前后端分离

一、什么是Ajax: AJAX Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)。 AJAX 不是新的编程语言,而是一种使用现有标准的新方法。 AJAX 最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部…

proteus仿真c51单片机(四)双机串口通信(电路设计及代码)

实验要求 1.通过甲机的按键给乙机发送控制字符,同时也可以实现乙机给甲机发送控制字符 2.用PROTEUS软件根据所给电路画出电路图,用KEIL软件调试程序和编译,最后在PROTEUS软件中实现仿真。 3.甲乙两个单片机通过串口进行通信&am…

Please refer to dump files (if any exist) [date].dump, [date]-jvmRun[N]……解决

一、问题 Please refer to dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream.二、解决方案 1、当打包构建的时候出现这个问题,如果你只是打包部署,那么就是将maven的test禁止可以成功打包 2、当你是本地服务器启动…

前端模块化-理解Tapable与Webpack中的Hooks

前言 Webpack 中的核心架构是基于 Tapable 实现的,Tapable 是一个类似于 Node.js 的 EventEmitter 的库,专门用于实现发布-订阅模式。Webpack 中的核心组件 Compiler、Compilation、Module、Chunk、ChunkGroup、Dependency、Template 都是通过 Tapable …

Fiddler安装与使用

下载Fiddler 访问Fiddler官方网站,下载适用于您操作系统的最新版本Fiddler。目前,Fiddler支持Windows、macOS和Linux平台。 Web Debugging Proxy and Troubleshooting Tools|Fiddler (telerik.com) 安装Fiddler,以Windows为例 Windows用户…

gitlab给用户添加项目权限

1.进入管理员界面 2.进入群组 3.添加用户

【RISC-V设计-04】- RISC-V处理器设计K0A之架构

【RISC-V设计-04】- RISC-V处理器设计K0A之架构 文章目录 【RISC-V设计-04】- RISC-V处理器设计K0A之架构1. 简介2. 主要特点3. 结构框图4. 指令列表5. CSR指令集6. 中断返回指令7. 总结 1. 简介 在前几篇文章中,介绍了RISC-V处理器的结构和指令集,从本…

Animate软件基本概念:视频及音频

视频和音频是ANimate软件中比较重要的素材类型。 FlashASer:AdobeAnimate2021软件零基础入门教程https://zhuanlan.zhihu.com/p/633230084 FlashASer:实用的各种Adobe Animate软件教程https://zhuanlan.zhihu.com/p/675680471 FlashASer:A…

DSP如何进行竞价

下面根据DSP的系统构成还拆解讲解里面的各个模块,这一节将竞价系统,也就是竞价流程 0、负载均衡 增加吞吐量、加强网络数据处理能力、提高网络的灵活性和可用性。 1、ADX发起竞价请求 上面会携带User ID等用户信息和广告信息一大堆信息。 2、解析竞价…

fastadmin 表单添加默认搜索条件

项目场景:员工列表,查看员工邀约客户明细,在 dialog 窗口中的 table怎么获取当前员工的数据呢?看似简单的需求,实际操作起来还是有点考究的,记录一下实现步骤。 页面1:员工列表 页面2&#xff…

sql_day14(获取各门店的面积)

描述:获取各门店的面积 获取各门店的面积 门店面积信息可以从分店面积明细表中获取。 先取实际经营面积(8), 如果取不到(实际经营面积为空)再取经营面积(7)。 如果取不到(经营面积为空)再取合同面积(1)。…

AI大模型赋能开发者|海云安创始人谢朝海受邀在ISC.AI 2024大会就“大模型在软件开发安全领域的应用”主题发表演讲

近日,ISC.AI 2024 第十二届互联网安全大会在北京国家会议中心盛大开幕。作为全球规格最高、规模最大、影响力最深远的安全峰会之一,本次大会以“打造安全大模型 引领安全行业革命”为主题,聚焦安全与AI两大领域,吸引了众多行业领袖…

您知道Jmeter中Redirect Automatically 和 Follow Redirects的使用场景吗?

相信很多使用过jmeter的同学都没有关注过请求中的Redirect Automatically 和 Follow Redirects选项,如下图: 在 JMeter 中,Redirect Automatically 和 Follow Redirects 是与 HTTP 请求重定向相关的两个选项,它们之间是有很大区别…

Ubuntu小键盘消失,并且安装好搜狗输入法后无法打出中文的问题

Ubuntu右上角的键盘图标不见了_ubuntu虚拟机键盘选项消失了-CSDN博客解决Ubuntu18.04安装好搜狗输入法后无法打出中文的问题_ubuntu18.04 搜狗输入法无法输入中文-CSDN博客 sudo apt install libqt5qml5 libqt5quick5 libqt5quickwidgets5 qml-module-qtquick2sudo apt instal…

小智常见报表-自由报表

概述 自由报表:具有自由设计、修改、完善的能力的报表。 应用场景 如下图所示,简单展示数据 示例说明 数据准备 在数据面板中添加数据集,可选择Json数据集和API服务数据集。Json数据集输入如下图所示: [{"姓名"…

Keytool:Uniapp 云打包需要生成证书的操作笔记

文章目录 背景操作步骤概述安装 Java 并检测版本生成证书 xxx.keystore问题:报错,没有权限使用证书 背景 我用 uniapp 想要用云打包,但是需要本机生成一个证书 操作步骤概述 安装 Java在终端输入 /usr/libexec/java_home -V 之后&#xff…

2024华为数通HCIP-datacom最新题库(H12-831变题更新⑨)

请注意,华为HCIP-Datacom考试831已变题 请注意,华为HCIP-Datacom考试831已变题 请注意,华为HCIP-Datacom考试831已变题 近期打算考HCIP的朋友注意了,如果你准备去考试,还是用的之前的题库,切记暂缓。 如…

【Python学习笔记】序列化

【Python学习笔记】序列化 文章目录 【Python学习笔记】序列化1.python使用pickle序列化数据1.1. 环境准备1.2. 序列化datetime对象1.3. 序列化DataFrame对象1.3.1 json1.3.2 pickle 1.4 序列化list列表 2. flaskweb接口传输序列化数据2.1. bytes形式传输2.1.1. datetime对象2.…

k8s使用kustomize来部署应用

k8s使用kustomize来部署应用 本文主要是讲述kustomzie的基本用法。首先,我们说一下部署文件的目录结构。 ./ ├── base │ ├── deployment.yaml │ ├── kustomization.yaml │ └── service.yaml └── overlays└── dev├── kustomization.…