发件箱模式简介一个微服务可能需要执行两个步骤:“存储数据库”和“发送事件”。例如,一篇文章发表后,作者的发表统计需要更新。业务要求两个操作同时失败,或者同时成功,而不是一个成功一个失败。如果最后发表文章,发帖统计更新失败,会导致数据不一致。发件箱模式是解决这个问题最常用的模式。它的原理是:本地业务作为一个事务运行,事务提交前将事件写入消息表;查询消息表或监听binlog方法,将事件发送到消息队列。轮询方式:每隔1s或0.2s取出消息表中的事件,发送到消息队列中,然后删除事件。监听binlog的方法:使用Debezium等数据库工具监听数据库binlog,获取事件,发送到消息队列写入消费者,处理事件。因为在1中,业务和事件的提交是在同一个事务中,保证两者同时提交。在第2步和第3步中,所有操作都不会失败。如果中间出现宕机事件,他们会重试,最后成功。对于前面提到的post后提交统计信息的场景,上述方案保证了统计信息的最终更新,数据会达到最终的一致性。多数据库问题在当今流行的微服务架构中,一个微服务通常使用一个单独的数据库。当多个服务需要使用发件箱模式时,传统的发件箱架构更难维护。轮询获取事件:在轮询任务中,编写多个数据库的轮询任务。通过监听binlog获取事件:需要监控多个数据库的binlog。数据库,可维护性差。而且,架构的灵活性也不好。如果数据库很多,及时产生的事件很少,架构的负载就会很高,造成资源浪费。理想的架构负载只与发送的事件数量有关,与其他因素无关。解决方案开源分布式事务框架https://github.com/dtm-labs/dtm中的二阶段消息可以很好的处理这个问题。下面是跨行转账服务的例子:msg:=dtmcli.NewMsg(DtmServer,gid).Add(busi.Busi+"/TransIn",&TransReq{Amount:30})err:=msg.DoAndSubmitDB(busi.Busi+"/QueryPreparedB",db,func(tx*sql.Tx)error{returnbusi.SagaAdjustBalance(tx,busi.TransOutUID,-req.Amount,"SUCCESS")})这部分代码首先生成一个DTMmsgglobaltransaction,通过dtm的服务器地址和全局交易id给msg增加一个分支业务逻辑,这里的业务逻辑是余额转账操作TransIn,然后带上这个服务需要转账的数据,金额为30元,然后调用msg的DoAndSubmitDB,这个函数要保证业务的成功执行和msg全局事务的提交,要么同时成功,要么同时失败。第一个参数是checkbackURL,具体含义后面会讲到。第二个参数是sql.DB,是业务访问的数据库对象。第三个参数是业务函数。我们例子中的业务是从A的余额中扣除30元,DoAndSubmitDB如何保证业务执行成功和msg提交的原子性呢?请看下面的时序图:一般情况下,时序图中的五个步骤会正常完成,整个业务会按预期进行,完成全局事务。这里有一个新的内容需要说明一下,就是分两个阶段发起msg的提交。第一阶段调用Prepare,第二阶段调用Commit。DTM收到Prepare调用后,不会调用分支事务,而是等待后续的Submit。只有收到Submit,才开始分支调用,最终完成全局事务。异常情况在分布式系统中,需要考虑各种宕机和网络异常。我们来看看可能出现的问题:首先,我们最需要达到的目标是业务执行成功,而msg事务是一个原子操作,那么如果在前面的时序图中,Prepare消息发送成功后而在Submit消息发送成功之前,如果出现异常宕机怎么办?这时候dtm会检测到事务超时,会回查。对于开发者来说,checkback非常简单,只需要粘贴下面的代码:())}))如果你没有使用go框架的gin,那么你需要根据你的框架做一些小的修改,但是代码是通用的,适合你的每一个业务。backcheck的主要原理主要是通过消息表,但是dtm的backcheck经过仔细论证可以处理以下几种情况:backcheck时,本地事务还没有开始backcheck,本地事务还在进行中。回滚时,本地事务已提交的详细checkback原理有些复杂,已经申请了专利,这里就不详细介绍了。具体可以参考https://dtm.pub/practice/msg.html多数据库支持这种方案,如果需要处理多数据库,在运维层面,只需要创建一个消息表对于相应的库;在代码层面,只需要在review的地方传入不同的数据库连接即可。相比原有的轮询表和binlog监控方案,大大降低了运维成本。该架构的负载只与事件数量有关,与数据库数量等其他因素无关,因此具有较好的弹性。更多存储引擎支持dtm的二阶段消息,不仅提供数据库支持DoAndSubmitDB,还提供NoSQL支持Mongo支持如下代码,可以保证Mongo下的业务和消息都提交err:=msg。DoAndSubmit(busi.Busi+"/RedisQueryPrepared",func(bb*dtmcli.BranchBarrier)错误{returnbb.MongoCall(MongoGet(),func(scmongo.SessionContext)错误{returnSagaMongoAdjustBalance(sc,sc.Client(),TransOutUID,-reqFrom(c).Amount,reqFrom(c).TransOutResult)})})Redis支持如下代码,可以保证Redis下业务和消息同时提交err:=msg.DoAndSubmit(busi.Busi+"/RedisQueryPrepared",func(bb*dtmcli.BranchBarrier)error{returnbb.RedisCheckAdjustAmount(busi.RedisGet(),busi.GetRedisAccountKey(busi.TransOutUID),-30,86400)})dtmcheckback方案可以轻松扩展到支持事务的各种其他存储引擎解决方案。特点二阶段消息具有以下特点:优雅地支持多数据库不仅支持SQL数据库,还支持Mongo、Redis等NoSQL代码比通常的发送更短box模式的代码量大大减少。整个架构和开发过程不涉及消息队列,只涉及API,更容易上手交易消息中提出,但是笔者全网搜索回溯实例和各种案例,没找到一个可以处理各种异常情况的回溯解决方案。在已经找到的解决方案中,没有一个能够正确处理“本地事务仍在进行中”的情况,会出现导致数据不一致的极端情况。详情请参考https://dtm.pub/practice/msg.html。另外,dtm的二阶段消息不需要引入队列,也可以和其他消息队列结合使用,使用范围更广。小结本文介绍的dtm二阶段消息较好的支持了多数据库的情况。该架构方案具有诸多优点,可以完美替代发件箱模式,为开发者带来更简单易用的架构。欢迎访问https://github.com/dtm-labs/dtm和star支持我们
