Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

我想咨询或探讨一个关于事务问题, 请指教 #2

Open
zbysir opened this issue Dec 28, 2020 · 7 comments
Open

我想咨询或探讨一个关于事务问题, 请指教 #2

zbysir opened this issue Dec 28, 2020 · 7 comments

Comments

@zbysir
Copy link

zbysir commented Dec 28, 2020

我拜读你写的文章: 清晰架构(Clean Architecture)的Go微服务: 事物管理, 思路很好, 不过我还有一个问题没思考明白:

不知道你有没有遇到一个事务跨几个DataService的场景?

假如 我定义了两个DataService: OrderDataInterface, ProductDataInterface, 分别处理订单数据与商品数据, 继续, 现在我定义了一个UserCase: OrderUseCase, 它依赖OrderDataInterfaceProductDataInterface来进行下单操作: 新建一个Order并将Product库存减一, 这两个操作我想做到一个事务里.

我看到此仓库的代码里只能对其中一个DataService开启事务:

import (
	"github.com/jfeng45/servicetmpl1/applicationservice/dataservice"
	"github.com/jfeng45/servicetmpl1/domain/model"
)

// RegistrationTxUseCase implements RegistrationTxUseCaseInterface.
// It has UserDataInterface, which can be used to access persistence layer
type RegistrationTxUseCase struct {
	UserDataInterface dataservice.UserDataInterface
}

// The use case of ModifyAndUnregister with transaction
func (rtuc *RegistrationTxUseCase) ModifyAndUnregisterWithTx(user *model.User) error {

	udi := rtuc.UserDataInterface
	return udi.EnableTx(func() error {
		// wrap the business function inside the TxEnd function
		return ModifyAndUnregister(udi, user)
	})
}

我想让一个事务跨几个DataService, 请问这个情况下我应该如何构建代码?

不知道你还用qq吗? 我的qq是 1019654929, 想与你实时交流更方便. 如有打扰请见谅.

@zbysir
Copy link
Author

zbysir commented Dec 28, 2020

还有一个问题:

为何你说"事务逻辑应该作用于用例层(业务逻辑) 不在持久层上", 这样设计有什么好处?

如果非要在Usecase中开启事务, 就不得不让DataService实现EnableTx接口, 我感觉这会导致DataService不干净, 以为我后期可能会使用其他DB(不支持事务的)或者微服务来实现DataService, 这时候就会多出一个EnableTx方法.

@jfeng45
Copy link
Owner

jfeng45 commented Dec 30, 2020

这个设计是可以支持多个DataService场景的。如果增加一个ProductDataInterface,大概的代码如下:

import (
	github.com/jfeng45/servicetmpl1/applicationservice/dataservice
	github.com/jfeng45/servicetmpl1/domain/model
)
// RegistrationTxUseCase implements RegistrationTxUseCaseInterface.
// It has UserDataInterface, which can be used to access persistence layer
type RegistrationTxUseCase struct {
	UserDataInterface dataservice.UserDataInterface
        ProductDataInterface dataservice.ProductDataInterface
}

// The use case of ModifyAndUnregister with transaction
func (rtuc *RegistrationTxUseCase) ModifyAndUnregisterWithTx(user *model.User, product *model.Product) error {

	udi := rtuc.UserDataInterface
        pdi := rtuc.ProductDataInterface
	return udi.EnableTx(func() error {
		// wrap the business function inside the TxEnd function
		ModifyAndUnregister(udi, user)
                return ReduceProduct(pdi, product)
	})
}

@jfeng45
Copy link
Owner

jfeng45 commented Dec 30, 2020

你提到的文章是旧版的,我有一个升级版的文章,比原来的简化了很多。下面是链接。“一个非侵入的Go事务管理库——如何使用

关于"事务逻辑应该作用于用例层(业务逻辑)不在持久层上",这样做正是为了应对你所描述的情况。因为一个事务经常会跨多个不同的DataService,如果把事务逻辑放在持久层上会很麻烦。而且有的用例需要持久层支持事务,有的不要。这样如果把事务逻辑放在持久层上,持久层就会变得很复杂。现在把事务逻辑放在用例层,持久层就几乎不用考虑事务,变得几乎透明。
至于你说的不想让DataService实现EnableTx接口,归根到底是你想通过什么途径让一个函数支持事务,这个可以有不同的方法来实现(例如可以传入一个事务标识参数(flag))。EnableTx接口的主要作用是设定事务的边界(开始和结束),这个是无法避免的。别的方法也有各种各样的问题,综合来看EnableTx接口是相对最好的方法。

至于“可能会使用其他DB(不支持事务的)或者微服务来实现DataService,这时候就会多出一个EnableTx方法.”,这个不是问题。你可以按照你的意愿来实现EnableTx接口。如果是“其他DB(不支持事务的)“,你可以什么也不做或返回错误。微服务也类似。我觉得这正是EnableTx接口的优点,可以处理各种情况。我的QQ是3120156013,不过平时用的不多。

@zbysir
Copy link
Author

zbysir commented Dec 30, 2020

对于第一个问题:

我有点不能理解你举例的代码为什么能正确运行. 我试着理解了这个仓库中buildGdbc的代码, 我发现如果如果某Usecase配置了开启事务, 则在这个代码中始终会使用sql.DB.Begin()开启一个新的连接, 这意味着你上面举例的代码中rtuc.UserDataInterfacertuc.ProductDataInterface实际上内部使用的两个不同的连接, 在我的认知中, 只有所有的SQL都运行在一个连接并且在 BEGIN 和 COMMIT 之间, 才能保证原子性.

如果我的理解有误还请更正.

对于第二个问题:

是的, 我也没找到完美的方案, 只能选择相对优雅的方案.

@zbysir
Copy link
Author

zbysir commented Dec 30, 2020

补充一下, 我忽略了 这个仓库与你发的文章“一个非侵入的Go事务管理库——如何使用”的代码有一点不同:

在仓库中有以下代码:

// Only non-transaction connection is cached
if !dsc.Tx {
if value, found := c.Get(key); found {
logger.Log.Debug("found db in container for key:", key)
return value, nil
}
}

而文章中没有, 我应该以哪一个为准?

@jfeng45
Copy link
Owner

jfeng45 commented Dec 30, 2020

我有两个库”/jfeng45/servicetmpl/” 和“/jfeng45/servicetmpl1/”分别对应不同的文章。”/jfeng45/servicetmpl/”对应旧版文章,“/jfeng45/servicetmpl1/”对应新版文章。旧版文章是没有问题的,它不是在容器中获得“*sql.Tx”。新版主要增加了两个功能,“可自我进化”和升级事务。“可自我进化”用的例子是”/jfeng45/order/”和”/jfeng45/payment/”,这两个由于没有使用依赖注入也是没有问题的。但“/jfeng45/servicetmpl1/”确实是有问题的,正如你说,现在的代码如不改动用的不是一个“*sql.Tx”。我这里讲一下大概的修改思路,争取这两天上传代码。

总的来说需要把一个用例的“*sql.Tx”存储在容器中,这样当一个用例有多个DataService时,就能从容器中获得相同的“*sql.Tx”。每个用例会在程序中生成一个唯一的UUID,用它作为主键来存储“*sql.Tx”。 需要在配置文件的”sqlConfigTx“中增加一个新的字段”txNum“来存UUID。当DataService要获取“*sql.Tx”时先检查容器中是否有这个用例的UUID,如果有,就用它。如果没有就创建,并存入容器。当完成用例的实例创建后,从容器中删除UUID。

当代码库的代码和文章中不一致时,以代码库为准。

@zbysir
Copy link
Author

zbysir commented Dec 30, 2020

这是一个好的思路! 我好奇对于"容器"的概念 作者是从哪里得到的灵感?

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants