乐趣区

关于go:如何优雅地实现多数据库的发件箱模式

发件箱模式简介

一个微服务可能须要执行“存数据库”和“发送事件”两个步骤。例如公布一篇文章后,须要更新作者的发文统计信息。业务上要求两个操作同时失败,或者同时胜利,而不能呈现一个胜利一个失败。如果最终文章公布了,更新发文统计失败了,就会导致数据不统一。

发件箱模式是解决这个问题的最罕用模式,其原理为:

  1. 本地业务作为一个事务运行,在提交事务之前,将事件写入到音讯表;提交事务时,会同时提交业务,以及事件
  2. 通过轮询音讯表或者监听 binlog 形式,将事件发给音讯队列

    • 轮询形式:每隔 1s 或者 0.2s 取出音讯表中事件,发给音讯队列,而后删除事件
    • 监听 binlog 形式:通过 Debezium 等数据库工具,监听数据库的 binlog,获取事件,发送给音讯队列
  3. 编写消费者,处理事件

因为 1 中,业务和事件的提交是在同一个事务,保障了两者会同时提交。
在步骤 2,3 中,都是不会失败的操作,如果两头产生宕机事件等,都会重试,并最终胜利。

对于前述的发文后提交统计信息场景,上述计划保障了统计信息被最终更新,数据会达到最终统一

多数据库的问题

在当今风行的微服务架构下,通常一个微服务会采纳一个独自的数据库。当多个服务须要应用发件箱模式时,那么传统的发件箱架构就比拟难以保护。

  • 采纳轮询形式获取事件:须要在轮询工作中,编写多个数据库的轮询工作
  • 采纳监听 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 {return busi.SagaAdjustBalance(tx, busi.TransOutUID, -req.Amount, "SUCCESS")
})

这部分代码中

  • 首先生成一个 DTM 的 msg 全局事务,传递 dtm 的服务器地址和全局事务 id
  • 给 msg 增加一个分支业务逻辑,这里的业务逻辑为余额转入操作 TransIn,而后带上这个服务须要传递的数据,金额 30 元
  • 而后调用 msg 的 DoAndSubmitDB,这个函数保障业务胜利执行和 msg 全局事务提交,要么同时胜利,要么同时失败

    1. 第一个参数为回查 URL,具体含意稍后说
    2. 第二个参数为 sql.DB,是业务拜访的数据库对象
    3. 第三个参数是业务函数,咱们这个例子中的业务是给 A 扣减 30 元余额

胜利流程

DoAndSubmitDB 是如何保障业务胜利执行与 msg 提交的原子性的呢?请看如下的时序图:

个别状况下,时序图中的 5 个步骤会失常实现,整个业务依照预期进行,全局事务实现。这外面有个新的内容须要解释一下,就是 msg 的提交是依照两个阶段发动的,第一阶段调用 Prepare,第二阶段调用 Commit,DTM 收到 Prepare 调用后,不会调用分支事务,而是期待后续的 Submit。只有收到了 Submit,开始分支调用,最终实现全局事务。

异常情况

在分布式系统中,各类的宕机和网络异样都是须要思考的,上面咱们来看看可能产生的问题:

首先咱们要达到的最重要指标是业务胜利执行和 msg 事务是原子操作,那么如果后面时序图中,当 Prepare 音讯发送胜利之后,Submit音讯发送胜利之前,出现异常宕机会如何?这个时候 dtm 会检测到该事务超时,会进行回查。对于开发人员来说,该回查很简略,只须要粘贴如下代码即可:

    app.GET(BusiAPI+"/QueryPreparedB", dtmutil.WrapHandler2(func(c *gin.Context) interface{} {return MustBarrierFromGin(c).QueryPrepared(dbGet())
    }))

如果您应用的不是 go 框架 gin,那么您须要依据您的框架做一些小批改,然而该代码是通用的,适宜您的每个业务。

回查的次要原理次要是通过音讯表,然而 dtm 的回查通过认真的论证,可能解决以下状况:

  • 回查时,本地事务未开始
  • 回查时,本地事务还在进行中
  • 回查时,本地事务已回滚
  • 回查时,本地事务已提交

具体的回查原理有些简单,已申请了专利,这里不做具体介绍,详情能够参考 https://dtm.pub/practice/msg.html

多数据库反对

该计划下,如果您须要解决多数据库,运维层面,只须要给相应的库创立好消息表;代码层面,只须要在回查的中央,传入不同的数据库连贯即可。

比照于原有的轮询表,以及监听 binlog 计划,运维老本大大降低。该架构的负载仅仅与事件数量相干,跟数据库数量等其余因素无关,具备了更好的弹性。

更多存储引擎的反对

dtm 的二阶段音讯,不仅提供了数据库的反对DoAndSubmitDB,还提供了 NoSQL 的反对

Mongo 反对

上面这段代码,能够保障 Mongo 下的业务和音讯两者同时提交

err := msg.DoAndSubmit(busi.Busi+"/RedisQueryPrepared", func(bb *dtmcli.BranchBarrier) error {return bb.MongoCall(MongoGet(), func(sc mongo.SessionContext) error {return SagaMongoAdjustBalance(sc, sc.Client(), TransOutUID, -reqFrom(c).Amount, reqFrom(c).TransOutResult)
    })
})

Redis 反对

上面这段代码,能够保障 Redis 下的业务和音讯两者同时提交

err := msg.DoAndSubmit(busi.Busi+"/RedisQueryPrepared", func(bb *dtmcli.BranchBarrier) error {return bb.RedisCheckAdjustAmount(busi.RedisGet(), busi.GetRedisAccountKey(busi.TransOutUID), -30, 86400)
})

dtm 的回查计划能够很容易的扩大到其余各种各样的反对事务的存储引擎

计划特点

二阶段音讯下具备以下特点:

  • 优雅的反对了多数据库
  • 不仅反对 SQL 数据库,还反对了 Mongo,Redis 等 NoSQL
  • 代码简短,比通常的发件箱模式代码量大幅缩小
  • 整个架构和开发过程不波及音讯队列,只波及 api,更容易上手
  • 负载仅仅与音讯量无关,与波及的数据库数量无关

比照 RocketMQ 事务音讯

回查的这种模式最早是在 RocketMQ 的事务音讯中提出的,然而作者全网查找了回查的例子,以及各种案例,都未找到可能把各种异常情况都解决好的回查计划。已找到的计划中,都未可能正确处理”本地事务还在进行中“的这种状况,都会存在极其状况导致数据不统一,详情参考 https://dtm.pub/practice/msg.html。

另外 dtm 的二阶段音讯,不须要引入队列,或者也能够联合其余的音讯队列应用,因而应用范畴更广

小结

本文介绍的 dtm 二阶段音讯,更好的反对多数据库的状况。该架构计划,具备诸多长处,能够完满的代替发件箱模式,给开发者带来更简略易用的架构。

欢送拜访 https://github.com/dtm-labs/dtm 并 star 反对咱们

退出移动版