事务和第三方系统交互问题
问题分析
当事务访问的数据全部都是zeze管理的,这种情况下,zeze已经处理了所有问题(几乎吧), 实现逻辑时,可以按自己最舒服的方式进行编写逻辑实现。当事务需要操作非zeze管理的 数据(这里的数据包括不是zeze管理的内存变量)时,如上一章提到的注册Timer,向 ThreadPool提交任务等,这些操作非常可靠,几乎可以认为不会失败,此时可以简单的用 Transaction.WhileCommit处理。
当事务操作的是第三方系统的数据,由于第三方系统是有 可能失败的,此时不能简单的使用WhileCommit处理。如果第三方系统支持两段式提交等 事务方式,可以采用并融入本地事务,如果第三方系统不支持事务,需要具体问题具体分析。 下面先举个例子。
Transaction(InputParam)
{
Foreach (var item in InputParam.Items)
{
If (false == Bag.Remove(item))
Return false; // rollback
}
// Oss:某个云对象存储系统。
If (false == oss.putObject(InputParam))
Return false; // 希望正确rollback
Return true; // 事务提交
}
上面的流程看起来非常合理,但是已知存在如下问题:
- 事务可能重做,但最终成功,oss.putObject会写入多次,这不是个大问题,仅仅影响性 能。
- 由于事务可能重做,并且再次尝试时回滚了,但是oss.putObject的数据已经写到第三 方系统了,这会导致错误的写入Oss。
解决思路
- 还是WhileCommit
如果认为Oss系统足够可靠,那么简单的使用WhileCommit(() => oss.putObject())即可。这 种方案在安全性要求不高的情况下推荐采用,虽然不出错的第三方系统应该是不存在的。但只要需求足够 就行了。这个方案实际上不做特殊处理,仅仅由系统保证安全性。
- 队列啊队列
当第三方系统不支持事务方式操作时,可以引入一个“支持事务的队列”。zeze事务处理时, 先写入这个队列,落地持久化。然后这个队列有个订阅者搬运工,把提交的内容搬到真正的 第三方系统中。这个“支持事务的队列”的实现,可以千奇百怪了,下面举几个例子。
- 使用Zeze的Table 使用zeze的table存储将要保存到oss的数据。这就把原来事务内对oss的操作转换成受 zeze管理的数据的操作。然后有个搬运线程,不断的把最新提交的数据搬到oss。TODO 抽 象并包装成zeze-one-object-queue。
- 使用本地文件系统 使用本地文件系统存储将要保存到oss的数据。实际上这个就是自己实现一个队列了。注意, 保存到文件系统的数据有先后关系,别忘了这是一个队列。即,最新事务提交的数据应该后 处理,最终保存到oss的数据是最新的。这个方案的缺点是,不支持分布式。当存在多个server 时, server之间的队列之间没有通讯,会导致fifo失效。
- 使用RocksMQ 对于大系统,可以考虑采用现在已经很成熟的第三方队列。Zeze已经对RocksMQ进行了简 单包装。这个方案的缺点是系统复杂度大大提升,需要自己权衡得失。TODO 确认zeze对 RocksMQ的包装处理了所有问题。
- 事务与第三方交互建议规则
这里的事务与第三方交互问题不是分布式事务性质的交互,仅仅是事务与外部系统的调度问 题,上面的解决思路提供了一些方法,但不完善。另外这个问题没有一致的解决方法,下面 提供两个基本规则。
- 事务外调度 把本地事务看成一个procedure.call,第三方任意的看成rpc1.call(),rpc2.call(),然后把这些 call按比较合理的规则排号顺序。这就是事务外调度,一般来说推荐这种调度方式。
- 事务内调用 有些时候事务外调度可能不符合需求。比如某个rpc的参数是事务内计算得到的,而rpc的 结果又决定了事务是否继续执行。此时就要求事务内调用,那么这个需要自行处理事务重做 以及其他可能的问题。举个例子,比如有个第三方的消息过滤服务,这里假定其调用rpc的 参数要求事务内计算得到,如果检查不通过,事务需要回滚。此时就需要事务内调用。这个 第三方消息过滤服务看功能就是符合事务重做等情况下,都能随时调用,不会出现任何问题。
- 事务内调用改造成事务外调用 当rpc调用需要在一个事务流程内调用,但是它又不能安全的处理zeze事务的重做问题, 此时有个办法就是把这个事务一分为二,proc1,proc2,proc1_undo然后采用事务外规则,安 排好执行。比如:
if (proc1.call() && rpc.call()) return proc2.call(); else proc1_undo.call();