分布式事务:下一代无侵入的分布式事务解决方案

1、前言


1.1、背景介绍

在TCC模式下,发起方需要考虑如何存储事务日志:

  • 异库发起方需要实现回查服务,并且自身业务量会受XTS Server及其db的瓶颈限制;

  • 同库发起方需要自行建库建表维护数据库,如果分库分表的话还需考虑如何对数据分片;


对于参与者, 需要按TCC参与者设计规范,实现参与者服务;此外一个TCC参与者通常只适用于一个特定的业务场景,新的业务场景需要设计和实现新的TCC参与者,复用性比较差;


XTS的TCC模式,接入成本大,使用门槛高,对业务的侵入较大,代码复用性也较差;

为了解决TCC模式的易用性问题,XTS推出自动模式,自动模式解决分布式事务的易用性问题,自动模式最大的特点是易于使用、快速接入、对业务代码无侵入;


1.2、自动模式说明

在自动模式下,无需用户编写TCC参与者,用户只需关注一阶段操作,XTS框架会自动帮用户生成二阶段操作;

用户根据自己的业务逻辑编写增删改查SQL语句,这些SQL将作为一阶段操作;XTS会解析一阶段用户SQL语义,生成对应的二阶段(提交和回滚)操作;

自动模式最大的特点是接入、使用、维护成本低,用户可以非常方便快捷的接入分布式事务,自动模式对业务代码几乎无侵入;


1.3、分布式事务自动模式产品

XTS支持自动模式的产品是:

  • dtx-client:

XTS自动模式客户端,在蚂蚁金服内部(主站、国际站、网商等环境),以及金融云上均已发布;



目前,XTS自动模式有线上业务已接入,其他更多用户前来咨询试用;


2、自动模式原理简介


2.1、自动模式概述

自动模式

如上图所示,自动模式下,发起方启动分布式事务之后,在整个分布式事务生命周期内,任何一个DB上的任意一个DAO操作,都可以作为分布式事务的一个分支事务:

  • 1、发起方内部的DAO操作可以作为分布式事务的一个分支事务:

用户根据自己的业务逻辑编写DAO操作(业务SQL),DAO内可以是任意增删改查SQL语句,XTS对用户的业务SQL使用无约束条件,此DAO可作为分布式事务的一个分支事务;

  • 2、兼容TCC参与者:

XTS的自动模式除了将DAO操作为分布式事务的分支事务外,也兼容TCC参与者;发起方在分布式事务内调用了TCC参与者服务,这个TCC参与者将作为分布式事务的一个分支事务;

TCC参与者的3个方法由用户实现,XTS自动模式不会为TCC参与者自动生成任何操作;

如上图所示,发起方内部调用了TCC参与者服务,此TCC参与者服务由应用“APP Y”发布;

  • 3、分布式事务上下文跨应用透传:

除了发起方应用内的DAO操作外,发起方也可以通过Sofa-rpc将分布式事务上下文透传至其他应用,在其他应用Sofa-Service内部的DAO操作也将作为分布式事务的一个分支事务;

如上图所示,发起方在分布式事务内,通过Sofa-rpc调用“APP X”的远程服务,“APP X”的Sofa-Service内的DAO操作将作为分布式事务的一个分支事务;除了DAO外,“APP X”的SOFA-Service内也可以调用TCC参与者,或者再发起新的Sofa-rpc调用将分布式事务上下文会透传至其它应用;

  • 4、XTS服务端作为事务协调者:

自动模式下,XTS的服务端(DTX-Server)作为事务协调者,所有分支事务都会向DTX-Server汇报数据,由DTX-Server根据所有分支事务的状态决定整个事务是提交还是回滚,并协调所有分支事务执行提交或者回滚操作;


2.2、自动模式原理


2.2.1、自动模式的两阶段

如上图,自动模式下事务参与者的一阶段操作是用户DAO(用户SQL);二阶段操作由XTS框架托管,XTS框架会自动根据一阶段用户SQL语义,生成对应的二阶段(提交和回滚)操作;各阶段功能描述如下:

  • 一阶段:

用户的DAO操作将作为一阶段,DAO操作可以是增删改查任意SQL语句,也可以是一个数据库事务(事务内有多条SQL语句);一阶段成功则用户DAO操作便在业务库上提交,此时用户DB中数据已更新;

一阶段执行过程中,XTS框架会根据业务SQL语义,保存一些对用户不可见的中间数据,包括:行锁、业务数据快照等;这些中间数据将用于业务数据的并发访问控制,生成业务DAO对应的二阶段操作;

  • 二阶段提交(XTS生成):

二阶段提交的主要工作是清理前面保存的中间数据;

用户DAO操作在一阶段已经提交至DB,此时XTS保存的中间数据已是无用数据,二阶段提交操作要做的事情就是清理这些无用数据;

  • 二阶段回滚(XTS生成):

二阶段回滚的主要工作是回滚用户SQL更新的数据;

XTS框架根据中间数据生成用户SQL的逆向SQL,还原一阶段DAO更新的数据;最后清理掉所有中间数据,使数据库恢复至初始状态;


2.2.2、一阶段业务DAO执行

在一阶段XTS拦截用户DAO,提取用户SQL并解析SQL语义,找到业务SQL要更新的数据,生成并保存行锁和业务数据快照;行锁用于业务数据的并发访问控制,业务数据快照用于生成二阶段操作,具体描述如下:

  • 1、DAO拦截

XTS框架通过代理业务DB的数据源(java.sql.DataSource)来拦截DAO操作,有2种方式:

1)XTS自动模式已与zdal5集成,业务使用zdal5作为数据源即可;

2)业务也可以使用XTS提供的数据源;

具体细节详见下文“接入指南”;

  • 2、解析SQL语义

解析业务SQL的目的是:找出业务SQL要更新的具体数据;故需要知道SQL类型(select、insert、delete、update)、表名称、数据行(where条件)、更新操作(SET语句)等数据;

为了提取SQL中的这些信息,首先需要将无结构的SQL字符串转换成结构化的SQL语法树,XTS使用Druid将SQL语句转换成语法树;

以SQL"update table_a set name='zhangsan' where id = 1" 为例,转换成语法树之后的结构如下:

得到SQL语法树之后,就可以从语法树相应的叶子节点上提取SQL类型、表名称、where条件、set语句等信息;

  • 3、解析表元数据

得到表名称等信息之后,还需要获取表的字段名称、字段类型、及主键、唯一约束键等信息;

JDBC自带的API中,能根据表名称获取表的元数据信息,API如下:

//数据库连接
java.sql.Connection conn = ds.getConnection();

DatabaseMetaData dbMetaData = conn.getMetaData();
//获取所有列信息
ResultSet columnRet = dbMetaData.getColumns(null,"%", tableName,"%"); 
while(columnRet.next()) {
    String columnName =columnRet.getString("COLUMN_NAME"); //字段名称 
    int columnType = columnRet.getInt("DATA_TYPE");        //字段类型
    ... ...
}

//获取所有索引
ResultSet indexesRet = dbMetaData.getIndexInfo(null, null, tableName, false, true);
while (indexesRet.next()) {
//此处收集索引信息,包括主键和唯一性约束键
}

Copy

  • 4、原快照数据(undo log)

在用户SQL语句执行之前,需要先保存DB数据的快照,此快照数据将被保存为undo log;undo log用于二阶段回滚时还原DB数据;

查询undo log的方式是:

select [所有列名称] from [表名称] where [where条件] for update

Copy

其中,[所有列名称] 在“解析表元数据”时已经获取,此处将其拼接进去;

[表名称]、[where条件]在“解析SQL语义”时已经获得,在此处将其拼接进去;

最终得到undo log查询SQL后,便可查询并保存undo log;

  • 5、执行业务SQL

此阶段执行真正的原业务SQL,更新业务DB数据;

  • 6、新快照数据(redo log)

业务SQL执行之后,DB数据已更新;XTS会将更新后的DB数据快照再保存成redo log;redo log用于二阶段回滚时,校验DB数据是否有脏写;

查询redo log的方式是:

select [所有列名称] from [表名称] where [主键] in ([主键值1], [主键值2], ......, [主键值N])

Copy

其中,[所有列名称] 在“解析表元数据”时已经获取;[表名称] 在“解析SQL语义”时已获取;

[主键] 即该表的主键字段名称,在“解析表元数据”时已经获得;

"[主键值1], [主键值2], ......, [主键值N]" 在“查询undo log”后以获取;

将以上结果拼接入redo log查询SQL,查询并保存redo log;

  • 7、行锁

XTS针对事务内的每一条DB数据,都会生成行锁,行锁用于不同分布式事务对相同数据进行操作时的并发控制;

行锁在一阶段保存,在二阶段分布式事务提交或者回滚时删除;

在同一个DB内部,表名称+主键值 能唯一的定位一行数据;

行锁的生成规则是:

[表名称]+[主键值]

Copy

其中,[表名称]在SQL解析时已经获得,[主键值]在undo log中已获得,XTS拼接并保持行锁至DB;

行锁的校验机制:

CREATE TABLE `dtx_row_lock` (
  `row_key` varchar(512) NOT NULL COMMENT '行锁',
  ... ...

  PRIMARY KEY (`row_key`)

);

Copy

如上,行锁字段row_key有唯一性约束,insert行锁成功说明获取锁成功;

如果insert行锁时触发主键冲突,说明行锁正在被其他分布式事务占用,此时当前DAO会回滚,并触发整个分布式事务回滚;

注意:行锁由表名称、主键值组成,故不允许业务SQL更新主键值;业务表结构变更时,如果涉及到主键的修改需先确认该业务表上无未完成事务;


快照数据(undo log、redo log)和行锁数据都保存业务的数据库中;“保存undo log”、“执行业务SQL"、“保存redo log”及“保存行锁” 在同一个DB事务执行,以保证这4个操作的原子性和一致性;如果用户DAO未开启DB事务,XTS会自动开启DB事务;


2.2.3、二阶段提交(生成)

二阶段提交操作由XTS框架生成,用户无感知;

一阶段用户DAO执行成功后,业务SQL已提交至DB,二阶段提交对业务的数据无任何操作;

二阶段提交操作要做的事情是清理XTS的中间数据,删除一阶段保存的undo log、redo log和行锁;


2.2.4、二阶段回滚(生成)

二阶段回滚操作由XTS框架生成,回滚阶段需要做的事情是还原业务SQL在一阶段更新的数据;

回滚操作的执行流程如下:

  • 1、查询XTS数据

根据XTS的主事务号和分支事务号,查询一阶段保存的快照数据;查询SQL使用‘select for update’句式,防止其他线程并发修改该数据;

XTS数据中包括undo log和redo log,以及表名称,业务SQL等信息;

  • 2、查询业务数据

根据上述XTS数据,使用‘select for update’句式,查询业务DB中该业务数据的当前值(actual data);

  • 3、校验脏写

’redo log‘是业务SQL执行之后,最新的业务数据的快照;

’actual data‘是业务数据库中,业务数据的当前值;

如果’redo log‘和’actual data‘值一致,则说明在一阶段业务SQL提交之后,该业务数据再也没被修改过;无脏写出现,可以正常回滚数据;

如果这2份数据的值不一致,说明在一阶段业务SQL提交之后,业务数据被修改过,出现了脏写,此时如果用undo log覆盖业务数据,会导致数据丢失;出现脏写XTS便不会再自动回滚业务数据,需要人工回滚;

  • 4、业务数据还原

确认无脏写出现后,便可使用'undo log'去覆盖'actual data', 具体做法是根据'undo log',生成业务SQL的逆向SQL(undo sql),还原业务SQL更新的数据;

业务SQL对应的‘undo sql’如下:

  • 5、删除快照、行锁

在还原业务数据之后,便可删除快照数据(undo log、redo log)和行锁等数据,分支事务即完成回滚;


上述 1 ~ 5 阶段均在同一个DB事务内,以保证所有操作的原子性;


2.3、自动模式的隔离级别

分布式事务自动模式有2种隔离级别:

  • 读未提交(read uncommit,默认)

读未提交是自动模式的默认隔离级别;

这个隔离级别下,分布式事务内的读SQL将不再校验行锁,分布式事务内能读到其它分布式事务尚未提交的数据,可能会出现脏读、幻读;

不过,不需要校验读SQL的行锁,也就不需要解析读SQL的语义,XTS也就不会去拦截读SQL,故这种隔离级别下,用户编写读SQL的自由度也比较大,可以使用join,嵌套等复杂查询句式;

  • 读已提交(read commit)

这个隔离级别下,读SQL会被XTS拦截解析语义,并严格校验行锁,以避免出现幻读、脏读;

通常读SQL的语法(join、嵌套等)能比写SQL复杂很多,过于复杂的SQL语法XTS可能无法解析SQL语义,这个隔离级别下,要求读SQL的语法尽量简单;


‘读未提交’和‘读已提交’只会限制的读请求的隔离级别,对于写SQL(insert、update、delete,以及select for update)不受限制;无论上述哪种隔离级别,写SQL都会全部解析SQL语义并严格校验行锁;


3、自动模式适用场景

自动模式的优点是易用,用户能方便快捷的接入分布式事务;

但是缺点是性能较差,并发较高的场景下,会出现大量行锁竞争导致分布式事务失败;

故自动模式只适合并发量较低的业务,一些非主流程的场景业务量较少,可以考虑使用分布式事务的自动模式,比如:用户服务申请和开通类、积分获取和兑换类、体现资金赎回等;


4、结束语

分布式事务自动模式能极大简化XTS的接入成本,发起方无需自己提供DB,参与者无需编写TCC服务;

目前有线上业务已经开始使用,期待有更多的用户前来咨询体验;

最后附上XTS自动模式一张架构图,完整的展示一下自动模式的设计:

_


评论区
Rick ©2018