您的位置:首頁 > 軟件教程 > 教程 > Golang在整潔架構(gòu)基礎(chǔ)上實(shí)現(xiàn)事務(wù)

Golang在整潔架構(gòu)基礎(chǔ)上實(shí)現(xiàn)事務(wù)

來源:好特整理 | 時(shí)間:2024-08-08 10:14:33 | 閱讀:184 |  標(biāo)簽: a 基礎(chǔ) Golang GO   | 分享到:

這篇文章在 go-kratos 官方的 layout 項(xiàng)目的整潔架構(gòu)基礎(chǔ)上,在微服務(wù)架構(gòu)下,實(shí)現(xiàn)優(yōu)雅的數(shù)據(jù)庫事務(wù)操作。

前言

大家好,這里是白澤,這篇文章在 go-kratos 官方的 layout 項(xiàng)目的 整潔架構(gòu) 基礎(chǔ)上,實(shí)現(xiàn)優(yōu)雅的數(shù)據(jù)庫事務(wù)操作。

視頻講解 ?:B站: 白澤talk ,公眾號(hào)【白澤talk】

Golang在整潔架構(gòu)基礎(chǔ)上實(shí)現(xiàn)事務(wù)

本期涉及的學(xué)習(xí)資料:

  • 我的開源Golang學(xué)習(xí)倉庫: https://github.com/BaiZe1998/go-learning,這期的所有內(nèi)容匯聚成一個(gè)可運(yùn)行的 demo, kit/transaction 路徑下。
  • kratos CLI 工具: go install github.com/go-kratos/kratos/cmd/kratos/v2@latest 。
  • kratos 微服務(wù)框架: https://github.com/go-kratos/kratos
  • wire 依賴注入庫: https://github.com/google/wire
  • 領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)思想:本文不多涉及,具備相關(guān)背景知識(shí)食用本文更佳。

在開始學(xué)習(xí)之前,先補(bǔ)齊一下整潔架構(gòu) & 依賴注入的前置知識(shí)。

預(yù)備知識(shí)

整潔架構(gòu)

kratos 是 Go 語言的一個(gè)微服務(wù)框架,github ? 23k, https://github.com/go-kratos/kratos

該項(xiàng)目提供了 CLI 工具,允許用戶通過 kratos new xxxx ,新建一個(gè) xxxx 項(xiàng)目,這個(gè)項(xiàng)目將使用 kratos-layout 倉庫的代碼結(jié)構(gòu)。

倉庫地址: https://github.com/go-kratos/kratos-layout

Golang在整潔架構(gòu)基礎(chǔ)上實(shí)現(xiàn)事務(wù)

kratos-layout 項(xiàng)目為用戶提供的,配合 CLI 工具生成的一個(gè)典型的 Go 項(xiàng)目布局看起來像這樣:

application
|____api
| |____helloworld
| | |____v1
| | |____errors
|____cmd
| |____helloworld
|____configs
|____internal
| |____conf
| |____data
| |____biz
| |____service
| |____server
|____test
|____pkg
|____go.mod
|____go.sum
|____LICENSE
|____README.md

依賴注入

? 通過依賴注入,實(shí)現(xiàn)了資源的使用和隔離,同時(shí)避免了重復(fù)創(chuàng)建資源對(duì)象,是實(shí)現(xiàn) 整潔架構(gòu) 的重要一環(huán)。

kratos 的官方文檔中提到,十分建議用戶嘗試使用 wire 進(jìn)行依賴注入,整個(gè) layout 項(xiàng)目,也是基于 wire,完成了整潔架構(gòu)的搭建。

service 層,實(shí)現(xiàn) rpc 接口定義的方法,實(shí)現(xiàn)對(duì)外交互,注入了 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 作為數(shù)據(jù)訪問的實(shí)現(xiàn)層,實(shí)現(xiàn)了上游接口,注入了數(shù)據(jù)庫實(shí)例資源。

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,作為被操作的對(duì)象。

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 優(yōu)雅事務(wù)

準(zhǔn)備

? 項(xiàng)目獲。簭(qiáng)烈建議克隆倉庫后實(shí)機(jī)操作。

git clone [email protected]:BaiZe1998/go-learning.git
cd kit/transcation/helloworld

這個(gè)目錄基于 go-kratos CLI 工具使用 kratos new helloworld 生成,并在此基礎(chǔ)上修改,實(shí)現(xiàn)了事務(wù)支持。

運(yùn)行 demo 需要準(zhǔn)備:

  1. 本地?cái)?shù)據(jù)庫 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 進(jìn)行數(shù)據(jù)庫變更管理的能力(goose 也是一個(gè)開源的高 ? 項(xiàng)目,推薦學(xué)習(xí))

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. 啟動(dòng)服務(wù): go run ./cmd/helloworld/ ,通過 config.yaml 配置了 HTTP 服務(wù)監(jiān)聽 localhost:8000,GRPC 則是 localhost:9000。

  2. 發(fā)起一個(gè) get 請(qǐng)求

Golang在整潔架構(gòu)基礎(chǔ)上實(shí)現(xiàn)事務(wù)

核心邏輯

helloworld 項(xiàng)目本質(zhì)是一個(gè)打招呼服務(wù),由于 kit/transcation/helloworld 已經(jīng)是魔改后的版本,為了與默認(rèn)項(xiàng)目做對(duì)比,你可以自行生成一個(gè) helloworld 項(xiàng)目,在同級(jí)目錄下,對(duì)照學(xué)習(xí)。

internal/biz/greeter.go 文件中,是我更改的內(nèi)容,為了測(cè)試事務(wù),我在 biz 層的 CreateGreeter 方法中,調(diào)用了 repo 層的 Save Update 兩個(gè)方法,且這兩個(gè)方法都會(huì)成功,但是 Update 方法人為拋出一個(gè)異常。

// 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 層開啟事務(wù)

如果忽略上文注釋中的內(nèi)容,因?yàn)閮蓚(gè) repo 的數(shù)據(jù)庫操作都是獨(dú)立的。

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 都已經(jīng)成功了,且彼此不強(qiáng)關(guān)聯(lián),數(shù)據(jù)庫中會(huì)多增加一條數(shù)據(jù)。

Golang在整潔架構(gòu)基礎(chǔ)上實(shí)現(xiàn)事務(wù)

biz 層開啟事務(wù)

因此為了 repo 層的兩個(gè)方法能夠共用一個(gè)事務(wù),應(yīng)該在 biz 層就使用 db 開啟事務(wù),且將這個(gè)事務(wù)的會(huì)話傳遞給 repo 層的方法。

? 如何傳遞:使用 context 便成了順理成章的方案。

接下來將 internal/biz/greeter.go 文件中注釋的部分釋放,且注釋掉分開使用事務(wù)的兩行,此時(shí)重新運(yùn)行項(xiàng)目請(qǐng)求接口,則由于 Update 方法拋出 err,導(dǎo)致事務(wù)回滾,未出現(xiàn)新增的 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
}

核心實(shí)現(xiàn)

由于 biz 層的 Usecase 實(shí)例持有 *DBClient ,repo 層也持有 *DBClient ,且二者在依賴注入的時(shí)候,代表同一個(gè)數(shù)據(jù)庫連接池實(shí)例。

pkg/db/db.go 中,為 *DBClient 提供了如下兩個(gè)方法: ExecTx() & DB() 。

在 biz 層,通過優(yōu)先執(zhí)行 ExecTx() 方法,創(chuàng)建事務(wù),以及將待執(zhí)行的兩個(gè) repo 方法封裝在 fn 參數(shù)中,傳遞給 gorm 實(shí)例的 Transaction() 方法待執(zhí)行。

同時(shí)在 Transcation 內(nèi)部,觸發(fā) fn() 函數(shù),也就是聚合的兩個(gè) repo 操作,需要注意的是,此時(shí)將攜帶 contextTxKey 事務(wù) tx 的 ctx 作為參數(shù)傳遞給了 fn 函數(shù),因此下游的兩個(gè) repo 可以獲取到 biz 層的事務(wù)會(huì)話。

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 層執(zhí)行數(shù)據(jù)庫操作的時(shí)候,嘗試通過 DB() 方法,從 ctx 中獲取到上游傳遞下來的事務(wù)會(huì)話,如果有則使用,如果沒有,則使用 repo 層自己持有的 *DBClient ,進(jìn)行數(shù)據(jù)訪問操作。

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
}

參考文獻(xiàn)

小編推薦閱讀

好特網(wǎng)發(fā)布此文僅為傳遞信息,不代表好特網(wǎng)認(rèn)同期限觀點(diǎn)或證實(shí)其描述。

a 1.0
a 1.0
類型:休閑益智  運(yùn)營狀態(tài):正式運(yùn)營  語言:中文   

游戲攻略

游戲禮包

游戲視頻

游戲下載

游戲活動(dòng)

《alittletotheleft》官網(wǎng)正版是一款備受歡迎的休閑益智整理游戲。玩家的任務(wù)是對(duì)日常生活中的各種雜亂物
Go v1.62
Go v1.62
類型:動(dòng)作冒險(xiǎn)  運(yùn)營狀態(tài):正式運(yùn)營  語言:中文   

游戲攻略

游戲禮包

游戲視頻

游戲下載

游戲活動(dòng)

GoEscape是一款迷宮逃脫休閑闖關(guān)游戲。在這款游戲中,玩家可以挑戰(zhàn)大量關(guān)卡,通過旋轉(zhuǎn)屏幕的方式幫助球球

相關(guān)視頻攻略

更多

掃二維碼進(jìn)入好特網(wǎng)手機(jī)版本!

掃二維碼進(jìn)入好特網(wǎng)微信公眾號(hào)!

本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請(qǐng)發(fā)郵件[email protected]

湘ICP備2022002427號(hào)-10 湘公網(wǎng)安備:43070202000427號(hào)© 2013~2025 haote.com 好特網(wǎng)