Golang在整潔架構(gòu)基礎(chǔ)上實現(xiàn)事務(wù)操作
前言
大家好,這里是白澤,這篇文章在 go-kratos 官方的 layout 項目的整潔架構(gòu)基礎(chǔ)上,實現(xiàn)優(yōu)雅的數(shù)據(jù)庫事務(wù)操作。
本期涉及的學(xué)習(xí)資料:
- 我的開源Golang學(xué)習(xí)倉庫:https://github.com/BaiZe1998/go-learning,這期的所有內(nèi)容匯聚成一個可運行的 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ū)動設(shè)計思想:本文不多涉及,具備相關(guān)背景知識食用本文更佳。
在開始學(xué)習(xí)之前,先補齊一下整潔架構(gòu) & 依賴注入的前置知識。
預(yù)備知識
整潔架構(gòu)
kratos 是 Go 語言的一個微服務(wù)框架,github ?? 23k,https://github.com/go-kratos/kratos
該項目提供了 CLI 工具,允許用戶通過 kratos new xxxx
,新建一個 xxxx 項目,這個項目將使用 kratos-layout 倉庫的代碼結(jié)構(gòu)。
倉庫地址: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
依賴注入
?? 通過依賴注入,實現(xiàn)了資源的使用和隔離,同時避免了重復(fù)創(chuàng)建資源對象,是實現(xiàn)整潔架構(gòu)的重要一環(huán)。
kratos 的官方文檔中提到,十分建議用戶嘗試使用 wire 進行依賴注入,整個 layout 項目,也是基于 wire,完成了整潔架構(gòu)的搭建。
service 層,實現(xiàn) rpc 接口定義的方法,實現(xiàn)對外交互,注入了 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ù)訪問的實現(xiàn)層,實現(xiàn)了上游接口,注入了數(shù)據(jù)庫實例資源。
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 優(yōu)雅事務(wù)
準備
?? 項目獲?。簭娏医ㄗh克隆倉庫后實機操作。
git clone git@github.com:BaiZe1998/go-learning.git cd kit/transcation/helloworld
這個目錄基于 go-kratos CLI 工具使用 kratos new helloworld
生成,并在此基礎(chǔ)上修改,實現(xiàn)了事務(wù)支持。
運行 demo 需要準備:
- 本地數(shù)據(jù)庫 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 進行數(shù)據(jù)庫變更管理的能力(goose 也是一個開源的高 ?? 項目,推薦學(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
啟動服務(wù):
go run ./cmd/helloworld/
,通過config.yaml
配置了 HTTP 服務(wù)監(jiān)聽 localhost:8000,GRPC 則是 localhost:9000。發(fā)起一個 get 請求
核心邏輯
helloworld
項目本質(zhì)是一個打招呼服務(wù),由于 kit/transcation/helloworld
已經(jīng)是魔改后的版本,為了與默認項目做對比,你可以自行生成一個 helloworld
項目,在同級目錄下,對照學(xué)習(xí)。
在 internal/biz/greeter.go
文件中,是我更改的內(nèi)容,為了測試事務(wù),我在 biz 層的 CreateGreeter
方法中,調(diào)用了 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 層開啟事務(wù)
如果忽略上文注釋中的內(nèi)容,因為兩個 repo 的數(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 }
即使最后拋出 Update 的異常,但是 save 和 update 都已經(jīng)成功了,且彼此不強關(guān)聯(lián),數(shù)據(jù)庫中會多增加一條數(shù)據(jù)。
biz 層開啟事務(wù)
因此為了 repo 層的兩個方法能夠共用一個事務(wù),應(yīng)該在 biz 層就使用 db 開啟事務(wù),且將這個事務(wù)的會話傳遞給 repo 層的方法。
?? 如何傳遞:使用 context 便成了順理成章的方案。
接下來將 internal/biz/greeter.go
文件中注釋的部分釋放,且注釋掉分開使用事務(wù)的兩行,此時重新運行項目請求接口,則由于 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 }
核心實現(xiàn)
由于 biz 層的 Usecase 實例持有 *DBClient
,repo 層也持有 *DBClient
,且二者在依賴注入的時候,代表同一個數(shù)據(jù)庫連接池實例。
在 pkg/db/db.go
中,為 *DBClient
提供了如下兩個方法: ExecTx()
& DB()
。
在 biz 層,通過優(yōu)先執(zhí)行 ExecTx()
方法,創(chuàng)建事務(wù),以及將待執(zhí)行的兩個 repo 方法封裝在 fn 參數(shù)中,傳遞給 gorm 實例的 Transaction()
方法待執(zhí)行。
同時在 Transcation 內(nèi)部,觸發(fā) fn() 函數(shù),也就是聚合的兩個 repo 操作,需要注意的是,此時將攜帶 contextTxKey 事務(wù) tx 的 ctx 作為參數(shù)傳遞給了 fn 函數(shù),因此下游的兩個 repo 可以獲取到 biz 層的事務(wù)會話。
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ù)庫操作的時候,嘗試通過 DB()
方法,從 ctx 中獲取到上游傳遞下來的事務(wù)會話,如果有則使用,如果沒有,則使用 repo 層自己持有的 *DBClient
,進行數(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 }
參考文獻
到此這篇關(guān)于Golang在整潔架構(gòu)基礎(chǔ)上實現(xiàn)事務(wù)的文章就介紹到這了,更多相關(guān)Golang實現(xiàn)事務(wù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go多線程中數(shù)據(jù)不一致問題的解決方案(sync鎖機制)
在Go語言的并發(fā)編程中,如何確保多個goroutine安全地訪問共享資源是一個關(guān)鍵問題,Go語言提供了sync包,其中包含了多種同步原語,用于解決并發(fā)編程中的同步問題,本文將詳細介紹sync包中的鎖機制,需要的朋友可以參考下2024-10-10golang 中string和int類型相互轉(zhuǎn)換
這篇文章主要介紹了golang 中string和int類型相互轉(zhuǎn)換,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02