关于后端:消息最终一致性最易用的新架构

3次阅读

共计 4564 个字符,预计需要花费 12 分钟才能阅读完成。

概述

跨服务更新数据是利用开发常见的工作,如果一些要害数据对一致性的要求较高,而业务上也不须要反对回滚的话,那么通常就会采纳本地音讯表的形式来保障最终统一。许多公司在解决跨服务更新数据一致性问题时,都会先引入本地音讯表,后续随着业务场景复杂化,再引入更多的事务模式

本文提出的二阶音讯,是一种新模式,新架构,优雅的解决了音讯最终一致性的问题,带来更加繁难快捷的开发新体验。

上面咱们以跨行转账作为例子,给大家详解这种新架构。业务场景介绍如下:

咱们须要跨行从 A 转给 B 30 元,咱们先进行可能失败的转出操作 TransOut,即进行 A 扣减 30 元。如果 A 因余额有余扣减失败,那么转账间接失败,返回谬误;如果扣减胜利,那么进行下一步转入操作,因为转入操作没有余额有余的问题,能够假设转入操作肯定会胜利。

采纳新架构开发

新架构基于分布式事务管理器 dtm

实现上述工作的外围代码如下所示:

        msg := dtmcli.NewMsg(DtmServer, gid).
            Add(busi.Busi+"/TransIn", &TransReq{Amount: 30})
        err := msg.PrepareAndSubmit(busi.Busi+"/QueryPreparedB", db, func(tx *sql.Tx) error {return busi.SagaAdjustBalance(tx, busi.TransOutUID, -req.Amount, "SUCCESS")
        })

::: gRPC
gRPC 的接入和 HTTP 根本一样,这里不再赘述,有须要的读者,能够参考 dtm-labs/dtm-examples 中的例子
:::

这部分代码中

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

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

胜利流程

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

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

提交后宕机流程

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

首先咱们要达到的最重要指标是业务胜利执行和 msg 事务是原子操作,因而首先看如果在业务实现提交后,发送 Submit 音讯前呈现了宕机故障会怎么样,新架构如何保障原子性?

咱们来看看这种状况下的时序图:

如果在本地事务提交之后,在发送 Submit 前,呈现了过程 Crash 或者机器宕机会怎么样?这个时候 DTM 会在肯定超时工夫之后,取出只 Prepare 但未 Submit 的 msg 事务,调用 msg 事务指定的回查服务。

您的回查服务逻辑,不须要手动编写,只须要依照如下代码进行调用即可:

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

这个回查函数,会到表外面查问,本地事务是否提交了:

  • 已提交: 返回胜利,dtm 进行下一步子事务调用
  • 已回滚: 返回失败,dtm 终止全局事务,不再进行子事务调用
  • 进行中: 这个回查会期待最终后果,而后依照后面的已提交 / 已回滚的状况解决

提交前宕机流程

咱们来看看本地事务被回滚的时序图:

如果在 dtm 收到 Prepare 调用后,AP 在事务提交前,遇见故障宕机,那么数据库会检测到 AP 的连贯断开,主动回滚本地事务。

后续 dtm 轮询取出曾经超时的,只 Prepare 但没有 Submit 的全局事务,进行回查。回查服务发现本地事务已回滚,返回后果给 dtm。dtm 收到已回滚的后果后,将全局事务标记为失败,并完结该全局事务。

易用性

采纳新架构解决一致性问题,仅须要:

  • 定义好本地业务逻辑,指定下一步解决的服务即可
  • 定义 QueryPrepared 解决服务,复制粘贴例子代码即可。

而后咱们看看其余计划状况

二阶音讯 vs 本地音讯表

上述的问题也能够采纳本地音讯表计划(计划详情参考分布式事务最经典的七种解决方案),来保证数据的最终一致性。如果采纳本地音讯表,须要的工作包含:

  • 在本地事务中执行本地业务逻辑,将音讯插入音讯表并最初提交
  • 编写轮询工作,将本地音讯表的音讯,发给音讯队列
  • 生产音讯,并将音讯发给相应的解决服务

两者比照,二阶音讯有以下长处:

  • 无需学习或保护任何音讯队列
  • 不须要解决轮询工作
  • 不须要生产音讯

二阶音讯 vs 事务音讯

上述的问题也能够采纳 RocketMQ 的事务音讯计划(计划详情参考分布式事务最经典的七种解决方案),来保证数据的最终一致性。如果采纳本地音讯表,须要的工作包含:

如果采纳事务音讯,须要的工作包含:

  • 开启本地事务,发送半音讯,提交事务,发送 commit 音讯
  • 生产超时的半音讯,对于收到的超时半音讯,查问本地数据库,而后进行 Commit/Rollback
  • 生产已提交的音讯,并将音讯发送给解决服务

两者比照,二阶音讯有以下长处:

  • 无需学习或保护任何音讯队列
  • 本地事务与发送音讯之间的简单操作须要手动解决,一不小心,可能呈现 bug。而二阶音讯则是全自动解决
  • 不须要生产音讯

二阶音讯在二阶段提交方面,与 RocketMQ 的事务音讯类似,是受到 RocketMQ 的事务音讯启发后提出的新架构。二阶音讯的命名,不再复用 RocketMQ 的事务音讯,次要是因为二阶音讯在架构上有很大的扭转,而另一方面,在分布式事务的上下文中,应用”事务音讯“这个名字,容易带来了解上的混同。

更多的长处

比照于后面讲述的队列计划,二阶音讯还有很多额定的长处:

  • 二阶音讯整个裸露的接口,齐全与队列无关,只跟理论的业务和服务调用相干,对开发人员更加敌对
  • 二阶音讯不必思考音讯队列音讯沉积及其他故障等问题,因为二阶音讯只依赖 dtm,开发人员能够认为 dtm 与零碎中其余一个一般无状态服务一样,只依赖背地的存储 Mysql/Redis。
  • 音讯队列是异步的,而二阶音讯同时反对异步和同步,默认异步,只须要关上 msg.WaitResult=true,那么能够同步期待上游服务实现
  • 二阶音讯还反对同时指定多个上游服务

二阶音讯将来瞻望

二阶音讯可能大幅升高音讯最终一致性解决方案的难度,取得宽泛的利用。将来 dtm 会思考增加后盾,容许动静指定上游服务,提供更高的灵活性。如果您原先采纳音讯队列来做服务解耦,那么这个 dtm 的后盾,容许你间接指定某个音讯的多个接管函数,无需编写音讯消费者,带来更加简略、直观、易用的开发体验。

回查原理分析

后面的时序图中,以及接口中都呈现了回查服务,在二阶音讯中,是复制粘贴代码主动解决的,而 RocketMQ 的事务音讯,则是手动解决的。那么主动解决的原理是什么?

要进行回查,首先要在业务数据库实例中,建设一张独立的表,外面保留全局事务 id。在解决业务事务时,会把 gid 写入到这张表。

当咱们用 gid 回查时,如果可能在表中查到 gid,那么阐明本地事务已提交,这样就能够返回 dtm,告知本地事务已提交。

当咱们用 gid 回查时,没有在表中查到 gid,那么阐明本地事务未提交,此时可能的后果是两个,一是事务还在进行中,二是事务已回滚。我查了许多对于 RocketMQ 的材料,未找到无效的解决方案。搜到所有解决方案是,如果未查到后果,那么什么都不做,期待下一次回查,如果 2 分钟或者更久的回查,始终都是查不到的,那么认为本地事务已回滚。

上述这种计划有很大的问题:

  • 两分钟还查不到 gid,并不能认为本地事务已回滚,极其状况下,可能产生数据库故障(例如过程或磁盘卡住了),持续时间超过 2 分钟,最初数据又提交了,那么这个时候,数据就不是最终统一了,就须要人工染指解决了
  • 如果一个本地事务,曾经回滚了,然而回查操作,还会在两分钟之内,依照 10s 左右的工夫距离,一直的进行轮询,会给服务器造成不必要的压力

而 dtm 的二阶音讯计划,则彻底解决了这部分的问题。dtm 的二阶音讯工作过程如下:

  1. 在解决本地事务时,会将 gid 插入到 dtm_barrier.barrier 表中,同时带上插入起因为 committed。该表有一个惟一索引,次要字段为 gid。
  2. 当进行回查时,二阶音讯的操作不是间接查 gid 是否存在,而是再 insert ignore 一条带有雷同 gid 的数据,同时带上插入起因为 rollbacked。此时如果表中如果已有 gid 的记录,那么新的插入操作就会被 ignore,否则数据会被插入。
  3. 而后再用 gid 查问表中的记录,如果查到记录的 reason 为 committed,那么阐明本地事务已提交;如果查到记录的 reason 为 rollbacked,那么阐明本地事务已回滚。

那么比照 RocketMQ 回查时的常见计划,二阶音讯是如何辨别出进行中和已回滚呢?其中的技巧在于回查时插入的数据,如果回查时,数据库的事务还在进行中,那么插入操作就会被进行中的事务阻塞,因为插入操作会期待事务中持有的锁。如果插入操作失常返回,那么数据库中的本地事务,必然已完结,必然是已提交或已回滚。

上面给大家留一个问题:二阶音讯的操作 3 是否省略,是否只依据步骤 2 的插入是否胜利,来判断是否已回滚?欢送大家留言探讨

一般音讯

二阶音讯不仅能够替换本地音讯表计划,也可能替换一般音讯计划。如果间接调用 Submit,那么就与一般音讯计划近似,然而提供了更灵便简略的接口。

假如一个这样的利用场景,界面上有一个加入流动的按钮,如果加入流动,会赠与两本电子书的永恒权限。这种状况下,能够再这个按钮的服务端中,相似这样解决:

msg := dtmcli.NewMsg(DtmServer, gid).
    Add(busi.Busi+"/AuthBook", &Req{UID: 1, BookID: 5}).
    Add(busi.Busi+"/AuthBook", &Req{UID: 1, BookID: 6})
err := msg.Submit()

这种形式也提供了异步接口,而不必依赖音讯音讯队列。在微服务的许多场景中,能够替换原有的异步音讯架构。

小结

本文提出的二阶音讯,接口简洁优雅,带来了比本地音讯表和 Rocket 事务音讯更简略的架构,能够帮忙大家更好的解决无需回滚的数据一致性问题。

我的项目地址

对于分布式事务更多的理论知识与实际,能够拜访以下我的项目和公众号:

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

关注【分布式事务】公众号,获取更多分布式事务相干常识,同时能够退出咱们的社群

正文完
 0