dapeng-soa分布式事务设计

1、基本概念

TI:Transaction Interceptor,事务拦截器,位于dapeng容器的filterChain链中。

由于TI的逻辑会比较复杂, 不太适合在IO线程中操作

TM:Transaction Manager, 事务管理器,作为一个独立的服务存在。

事务发起方: 服务调用链或者说请求会话中第一个加入全局事务的接口方法,称为事务发起方。
事务参与方: 服务调用链或者说请求会话中除事务发起方的其它加入了全局事务的接口方法,称为事务参与方。

例如,对于服务a,b,c, d:
client调用a.m1, a.m1调用b.m2以及c.m3, b.m2调用d.m4.
其中,a.m1以及b.m2,d.m4都声明为TCC事务, 那么在这次服务调用中, a.m1为事务发起方,b.m2,d.m4为事务参与方。

由事务参与方发起confirm或者cancel操作。

事务管理器负责confirm或者cancel失败后的重试。

在定义接口的时候, 需要加上以下注解,以表明该接口需要加入全局事务。
@TCC(confirm="",cancel="")
该注解有2个可选参数, 其中, confirm代表该接口的confirm方法名字,cancel代表该接口的cancel方法名字。

默认情况下,methodA的confirm方法名为methodA_confirm, cancel方法名为methodA_cancel

2、数据表结构

t_gtx

CREATE TABLE IF NOT EXISTS `gtx_db`.`t_gtx` (
  `id` INT(11) NOT NULL,
  `gtx_id` INT(11) NOT NULL COMMENT '全局事务id,一般使用服务的会话id(sesstionTid)',
  `status` SMALLINT(2) NOT NULL DEFAULT 1 COMMENT '全局事务状态, 1:新建(CREATED);2:成功(SUCCEED);3:失败(FAILED);4:完成(DONE)',
  `created_time` DATETIME(0) NOT NULL COMMENT '创建时间',
  `updated_time` TIMESTAMP(0) NOT NULL DEFAULT DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  `remark` VARCHAR(45) NULL COMMENT '备注, 每次状态变更都需要追加到remark字段。',
  PRIMARY KEY (`id`),
  INDEX `index_gtx_id` (`gtx_id` ASC))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4
COMMENT = '全局事务表'

t_gtx_step

CREATE TABLE IF NOT EXISTS `gtx_db`.`t_gtx_step` (
  `id` INT NOT NULL,
  `gtx_id` INT(11) NOT NULL COMMENT '全局事务id,一般使用服务的会话id(sesstionTid)',
  `step_seq` SMALLINT(2) NOT NULL COMMENT '子事务序号',
  `status` SMALLINT(2) NOT NULL DEFAULT 1 COMMENT '子事务状态, 1:新建(CREATED);2:成功(SUCCEED);3:失败(FAILED);4:完成(DONE)',
  `service_name` VARCHAR(128) NOT NULL COMMENT '服务名',
  `version` VARCHAR(32) NOT NULL DEFAULT '1.0.0' COMMENT '服务版本号',
  `method_name` VARCHAR(32) NOT NULL,
  `request` BLOB NULL,
  `confirm_method_name` VARCHAR(32) NULL,
  `cancel_method_name` VARCHAR(32) NULL,
  `redo_times` INT(11) NOT NULL DEFAULT 0,
  `created_time` DATETIME(0) NOT NULL COMMENT '创建时间',
  `updated_time` TIMESTAMP(0) NOT NULL DEFAULT DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  `remark` VARCHAR(45) NOT NULL DEFAULT '' COMMENT '备注, 每次状态变更都需要追加到remark字段。',
  PRIMARY KEY (`id`)),
  INDEX `index_gtx_id` (`gtx_id` ASC))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4
COMMENT = '全局事务流程表'

t_gtx_journal
对于参与分布式事务的服务接口,需要在本地有个事务流水表(例如orderDb):

CREATE TABLE IF NOT EXISTS `order_db`.`t_gtx_journal` (
  `id` INT(11) NOT NULL,
  `gtx_id` INT(11) NOT NULL COMMENT '全局事务id',
  `step_id` INT(11) NOT NULL COMMENT '子事务id',
  `biz_tag` VARCHAR(45) NOT NULL COMMENT '本次全局事务操作的本地业务表名字',
  `biz_id` INT(11) NOT NULL COMMENT '本次全局事务操作的本地业务记录id',
  `created_time` DATETIME(0) NOT NULL COMMENT '创建时间',
  `updated_time` TIMESTAMP(0) NOT NULL DEFAULT DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  `remark` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '备注, 每次状态变更都需要追加到remark字段。',
  PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8mb4
COMMENT = '子事务的本地流' /* comment truncated */ /*水表。 当本地事务成功时, 由本地业务*/

3、案例描述

这里以订单创建为例。
用户创建订单,同时扣除库存。
其中订单、库存分别为两个不同的服务。同时, TM也是一个单独的服务。

本流程有2个业务服务参与,分别是订单服务的创建订单接口以及库存服务的库存扣减接口。

业务主流程如下:
1、客户端调用orderService.createOrder, 发起订单创建流程
2、orderService调用stockService.decreaseStock, 扣减库存
3、orderService创建订单,并返回客户端。

对应的订单创建序列图如下:


创建订单

3.1. 客户端发起订单创建的操作

对应时序图的No.1调用

参数

3.2、全局事务的Try阶段

订单服务的全局事务拦截器(TI)收到请求后, 识别到目标方法带有TCC标识,即进入Trying阶段。

3.2.1、订单服务开启全局事务

TI向事务管理服务请求开启全局事务,对应时序图的No.2。
tm.beginGTX(gtxId, params)

txId可用sessionTid(long的形式),params可直接用bytes

3.2.2、事务管理器处理订单服务请求

对应时序图的No.3/4/5

事务管理器根据txId去决定调用方是事务发起者还是事务参与者。
这里,orderService是事务发起方, 那么:
1、TM首先通过createTGX(txId)方法创建一个全局事务(插入一条全局事务记录到t_gtx表中,状态为新建)
2、通过createStep(txId, params)方法创建一个子事务日志(插入一条子事务记录到t_gtx_step表中, 状态为新建)

全局事务开启, 操作成功后返回stepId继续下一步,否则失败后直接返回调用方,由调用方决定是继续还是回滚(在这个案例中, 这里的调用方是client)。

3.2.3、订单服务的TI转发请求到具体的业务服务方法

对应时序图中的No.6/7
全局事务开启成功后, TI转发请求到业务服务。这里为orderService.createOrder

在这个方法中, 首先调用库存服务的扣减库存接口:stockService.decreaseStock

如果全局事务开启失败,那么TI会直接报错返回给调用方(Err-Gtx-001: begin gtx error)

3.2.4、库存服务开启全局事务

对应时序图的No.8

同3.2.1,库存服务的TI收到扣减库存请求后,开启全局事务: `tm.beginGTX'

3.2.5、事务管理器处理库存服务请求

对应时序图的No.9/10

事务管理器通过gtxId发现全局事务已经开启,那么该请求来自事务参与方而不是发起方。
这时候,直接通过createStep插入一条子事务日志到t_gtx_step表中即可,并返回stepId。

3.2.6、库存服务本地逻辑处理

对应时序图的No.11/12/13

TI开始全局事务成功后, 转发扣减库存请求给具体的业务方法。
库存服务执行本地事务(库存余额扣减,冻结库存增加)后返回到TI

3.2.7、库存服务的TI更新全局事务

对应时序图的No.14/15/16

TI根据3.2.6的结果,调用tm.updateGTX更新全局事务。

TM根据gtxId以及stepId判断该请求来自事务参与方,那么仅更新子事务日志表updateStep, 状态为成功/失败。

这一步有可能失败,导致本地子事务提交后,结果没反映到TM的子事务表的状态中。

还有一个可能就是本地子事务成功,TI更新全局事务也成功了, 但是由于网络中断或者其他原因,导致服务调用方(这里是orderService)的对扣减库存调用失败。

不管如何,服务调用方调用失败后,由服务调用方自行决定是继续前行还是回滚全局事务。

3.2.8、订单服务本地业务逻辑处理

对应时序图的No.18/19

订单服务根据库存扣减的结果,决定是继续往前走还是失败回退。

如果继续往前走的话,就完成本地事务后返回结果给订单服务的TI;
如果失败回退的话,就把失败信息返回给订单服务的TI。

3.2.9、订单服务的TI更新全局事务

对应序列图的No.20/21/22/23

如果订单服务本地事务成功,那么TI通过tm.updateGTX把结果反馈给TM。

TM根据gtxId判断该请求来自事务发起方,那么根据status把全局事务状态更新为成功/失败;
同时, 更新子事务状态为成功/失败

全局事务的最终状态跟事务发起方对应的子事务的最终状态一致。

至此,Trying阶段完成。

根据本阶段的结果, TI将会进入TCC的confirm(成功)或者cancel阶段(失败)

3.3、confirm阶段

对应序列图的No.24~33
理论上, Trying阶段成功的话,confirm阶段一定能成功(最终一致).

Confirm操作由TI发起,而具体的逻辑由TM控制。

3.3.1 事务管理器的confirm操作

首先事务管理器根据gtxId得到全局事务记录以及子事务记录集合(gtx_steps)。

按照子事务的seq从小到大的顺序,依次调用子事务的confirm方法。(这个过程可以使用异步的方式并发去confirm?)

最后根据结果更新全局事务以及子事务的状态。

只有全部子事务的状态为完成,全局事务状态才能更新为完成。

TI发起confirm操作后,不管本次confirm操作是否成功, 都返回成功给client。

3.4、cancel阶段

对应序列图的No.24~43
本阶段跟confirm阶段逻辑类似,但是子事务的执行顺序相反

TI发起cancel操作后,不管本次cancel操作是否成功, 都返回失败给client。

3.5、confirm/cancel阶段的异常处理

TM通过定时器,定时扫描全局事务日志表中状态为非完成的记录(1分钟前),再次执行confirm/cancel操作。

4. 业务场景

TCC场景:

4.1. 客户端调用单独的TCC服务

image.png

4.1.1 正常流程

try成功,confirm成功

  1. try阶段:
    1.1 t_gtx, t_gtx_step插入事务日志成功, 状态皆为新建
    1.2 tccServiceA本地事务成功
    1.3 t_gtx, t_gtx_step更新事务日志成功,状态皆为成功
  2. confirm阶段
    2.1 TM调用tccServiceA成功,更新t_gtx, t_gtx_step成功,状态为完成。

try失败,cancel成功

  1. try阶段:
    1.1 t_gtx, t_gtx_step插入事务日志成功, 状态皆为新建
    1.2 tccServiceA本地事务失败
    1.3 t_gtx, t_gtx_step更新事务日志成功,状态皆为失败
  2. cancel阶段
    2.1 TM调用tccServiceA成功,更新t_gtx, t_gtx_step成功,状态为完成。

4.1.2 异常流程

try成功,confirm阶段或者cancel阶段失败
那么后续由TM定时任务继续重试。

4.1.3 异常流程

try阶段TI插入事务日志失败(Err-Gtx-001: begin gtx error)
如果是事务发起方(本案例), 那么TI直接返回Err-Gtx-001,本次服务调用失败。
如果是事务参与方, 那么TI直接返回Err-Gtx-001,并最终回到事务发起方,本次全局事务失败,并对已经有记录的子事务做cancel操作。

因为这里缺失了分布式事务的某个子事务日志记录,TM无法进行confirm或者cancel操作。

try阶段本地事务成功,但是TI更新事务日志失败(Err-Gtx-002: update gtx error),子事务的状态停留在新建的状态
这时候如果是事务发起方(本案例),那么TI会继续走confirm或者cancel的流程。
如果是事务参与方,把Err-Gtx-002返回, 事务发起方会忽略该错误,其对应的TI会继续走confirm或者cancel的流程。

在confirm或者cancel的逻辑里,TM会把gtxId以及该子事务id、状态通过cookie传过来。
如果子事务状态为成功或者失败,那么直接执行confirm或者cancel逻辑;

如果子事务状态为新建,那么目前尚不清楚到底try阶段的本地事务执行了没。

如果执行了, 那么必然可以通过gtxId,stepId找到在try阶段的本地事务操作过的本地事务流水记录,从而确认try阶段的本地事务提交情况,再进而决定本次confirm或者cancel该做的操作。

举个例子, 库存服务的扣减库存接口。
在try阶段,本地事务成功,然后TI在更新子事务状态的时候失败了,那么该子事务状态为新建。
然后事务发起方依然决定做confirm操作,同时库存服务扣减库存接口的confirm方法,通过gtxId以及stepId,找到了本地事务流水记录,从而可以执行confirm操作。

如果在try阶段,本地事务失败,然后TI在更新子事务状态的时候也失败了,那么该子事务状态为新建。
然后事务发起方依然决定做confirm操作,同时库存服务扣减库存接口的confirm方法,通过gtxId以及stepId,这时候是找不到本地事务流水记录的,说明try阶段本地事务失败。 那么业务可以调用一下把try以及confirm的逻辑合并起来,完成本次confirm操作。

4.2. 客户端先后调用2个TCC服务

image.png

这时候, 这两次服务调用分别构成一个全局事务, 是两个互不相关的全局事务

4.3. 客户端调用TCC服务a,服务a再调用TCC服务b

image.png

4.4. 客户端调用TCC服务a,服务a再分别调用TCC服务b以及TCC服务c

image.png

4.5. 客户端调用TCC服务a,服务a调用TCC服务b,服务b再调用TCC服务c

image.png

5. 异常流程处理

在4.3的业务场景中, tccServiceA调用tccServiceB失败,

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,699评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,124评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,127评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,342评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,356评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,057评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,654评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,572评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,205评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,343评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,015评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,704评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,196评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,690评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,348评论 2 358

推荐阅读更多精彩内容

  • 一个TCC事务框架需要解决的当然是分布式事务的管理。关于TCC事务机制的介绍,可以参考TCC事务机制简介。 TCC...
    Java高级进阶阅读 1,499评论 0 0
  • 一. 事务 1.1 什么是事务 数据库事务(简称:事务,Transaction)是指数据库执行过程中的一个逻辑单位...
    Java小铺阅读 578评论 0 3
  • 为什么无眠?想要把夜拉长,再拉长一些! 夜是写满悲欢的月色,伴随着不愿迷失的灵魂,怕自己睡下贪恋梦的旖旎! 夜是挂...
    馨欣然阅读 409评论 0 3
  • 时间习惯书写故事 在每个地方珍藏一段记忆 然后凝化于心 标签: 原创
    赵艺闳Z阅读 155评论 0 0