TXC-分布式事务中间件



1.1 分布式事务产生的背景


1.1.1 数据库单库容量不足,需要分库

当前的业务数据量膨胀,数据规模达到数亿甚至更多,单库存储受容量和性能限制,已无法满足业务需求。把数据存储到多个DB,并行操作是趋势。写操作一旦跨多个数据库,单机事务就无法保证数据一致性,需要通过分布式事务中间件进行保证。


1.1.2 业务服务化

业务量级扩大之后,平台业务服务化即刻成为强需求,实现系统间解耦、隔离不同的业务系统,能够降低系统之间相互影响的风险,能够使不同的业务线专注于业务本身,能够更好的应对上层业务变更,也为服务鉴权、流控等提供实现的可能。
相应的,一个完整的业务往往需要调用多个服务,如何保证多个服务间的数据一致性成为一个难题。比如一个业务调用了N个服务,服务1~N-1都成功,服务N失败了,则这次业务调用失败。我们期望的是前N-1个服务所修改的业务数据恢复到调用前的状态,这需要复杂的业务逻辑才能实现。为了简化业务逻辑,让业务与事务分离,我们需要一个分布式事务中间件支持。


1.2 解决分布式事务所面临的问题

• 原子性
整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
• 隔离性
对于并发提交的事务,数据库需要提供并发控制,保证数据库从一个事务的开始状态转换到该事务的结束状态的过程中,中间状态不被其他事务可见,就像事务串行执行一样。
• 持久性
在事务完成以后,即使发生掉电、系统宕机等错误,该事务对数据库所作的更改仍需持久的保存在数据库中。
• 一致性
从事务开始到事务结束,数据库的完整性约束(包括外建约束、级连关系、触发器等)不应该因为任何原因被破坏。


1.3 典型分布式事务场景


1.3.1 转账

对于银行账户间转账的问题。账户A向账户B转账,从实现上来看,一般至少会被拆分为“账户A减钱”、“账户B加钱”两个操作步骤,两个账户大多数情况下会被切分到不同的数据库上,更多的是,两个操作会是两次服务调用。这两个操作要求做到要么同时成功、要么同时失败。因此引入了分布式事务问题。


1.3.2 交易

从实现上来看,一般包括写买家库、写卖家库两个操作或两个服务。很难采用统一的切分条件把操作路由到一个数据库实例上,因此出现分布式事务问题。


1.3.3 下单减库存

在消费者点击购买按钮后,交易后台会进行库存检查、下单、减库存、更新订单状态等一连串的服务调用,每一个操作对应一个独立的服务,服务一般会有独立的数据库,因此产生分布式事务问题。


1.3.4 数据库扩容产生的分布式事务

业务初始阶段往往规模比较小,大多情况下,单库就可以满足需求,经过一段时间后,业务规模也会随之变得大而复杂,会出现分库的情况,这时原有的单机事务往往会变成分布式事务。


1.3.5 跨资源操作

在某些业务场景中,需要进行DB操作的同时,还会调用消息系统,DB操作成功、消息发送失败或者反过来都会造成业务的不完整。


1.4 实例分析


1.4.1 跨多个数据源的分布式事务

1
我们模拟一个账务系统来说明跨多个数据库导致的分布式事务问题。客户端提交SQL到中间代理层,中间代理层解析SQL,根据账户ID进行Hash,路由到DB1、DB2两个物理数据库。数据库表包含两个字段{账户ID、账户余额}。假设账户{K1、K2…K6}的初始余额都是100元。数据库自身有容灾,所有发生的数据变更都会产生binlog并同步到备份数据库上。本文暂不讨论数据库容灾的问题。
从ACID的角度来分析发生跨库事务的话,业务方需要考虑的问题。
A:把“K2向K1转账20元”分解为两个操作:(1)DB1上K1减去20元,剩余100-20=80元,(2)DB2上K2加上20元,剩余100+20=120元。两个操作要么同时成功、要么同时失败。如果(1)成功,而(2)失败,必然会引起用户投诉;而如果(2)成功,而(1)失败,那么会带来银行或者金融机构发生资损;
C:K1、K2两个账户组成的账务系统,K1+K2的余额总和在事务发生前、发生后都必须是一致的,都应该是200元,否则会引入系统性风险;
I:在转账操作结束之前,其他并发的事务不应该读到事务变更后的账户信息,不能对两个账户的余额字段进行修改操作;
D:事务一旦提交,对账户K1、K2的修改应该是持久的,这个属性一般的数据库都是可以保证的。


1.4.2 跨多个服务的分布式事务

假设某业务场景中需要调用A、B两个服务,要求两个服务在一个分布式事务内;或者某业务场景需要调用服务A,而服务A又调用到服务B,同样要求两个服务在一个分布式事务内。
同跨资源操作一样,业务方仍然需要考虑分布式事务中的多个服务要么同时成功、要么同时失败;分布式事务执行过程中,需要考虑已完成部分的隔离性问题,需要考虑多个服务的一致性问题。


1.5 常用解决方案(没有TXC的情况下常见的方案)


1.5.1 XA两阶段提交(2pc)

2
优点:
• XA接口标准化、使用简单,使用成本也很低;
缺点:
• 性能不理想,很难满足互联网大并发需求。开销大,分布式事务RT相较于单机事务有数量级的差距;分布式事务对相关资源的加锁机制增加了并发冲突,影响到系统吞吐和可伸缩性;
• XA要求资源管理必须实现XA接口,对资源管理器提出了要求,限制了业务方的产品选型;
• Mysql的XA实现上,没有记录prepare阶段日志,主备切换能够导致主备库数据不一致。


1.5.2 状态机方案

假设业务调用的执行序列为服务A、服务B。对于任何一个服务调用,都可能得到三种执行状态:成功、失败、超时。以服务B为例,“成功”的状态是业务所期望的,不需多做分析;“失败”的状态意味着该服务执行失败,业务无法继续流转,所有的前序操作(服务A)需要回滚,需要另实现一个用于对服务A进行回滚的服务A′;“超时”状态更加复杂,除了需要将前序操作回滚,还需要判断当前服务是否成功,因此,可以在服务B的业务数据库中新增一张表用户记录服务的执行状态,通过单机事务保证服务B和执行状态的原子性,后续可以通过查询执行状态来检查服务B是否完成。因此,“超时”状态可不额外处理。
在业务数据库中记录下服务的执行状态,可以保证服务只会被执行一次,看上去比较完美了,但是这样真的足够了吗?一旦业务进程crash,如何继续未完成的分布式事务呢? 恐怕要引入一个分布式事务协调器,嫌麻烦?那就人肉好了。
再来回顾一下实现此方案有哪些约束?1、进程crash的情况cover不住;2、要求每一个服务实现一个可逆的回滚服务;3、要求服务提供回查接口,用户查询服务的执行状态。对于1,由于发生概率低,可以考虑人肉解决;对于2和3,完全依赖服务本身的实现,如果服务是自己的还好,如果是别人提供的,那么该方案几乎行不通了。


1.5.3 消息驱动最终一致模型

在某些特定业务场景下,可以通过引入消息系统实现业务最终一致。假设业务调用的执行序列为服务A、服务B。在调用服务A的同时,向消息系统发送一条消息,通过异步轮询消费该消息驱动服务B,以完成业务最终完成。

1.5.3.1 业务与消息耦合
在执行服务A时,同时记录消息数据,这个消息数据和服务A的业务数据保存到同一个数据库实例中。
进程1:
Begin local trx:
服务A DML;
push messageQ “call 服务B”;
Commit/Rollback;
进程2:
for eash message in queue
begin local trx:
服务B DML;
commit/rollback;
if trx.success:
pop message;
end if;
end for;
进程1中,由于消息队列和服务A共用数据库资源,可以把消息和服务在单机事务中执行;进程2中,如果服务B执行失败,重新消费消息重试即可;进程2中,如果服务B执行成功、删除消息失败,如果服务B能够实现幂等然后可以重试(可以改造服务B,执行B的事务中,同步插入一行记录描述B是否执行,重试时检查下即可消除对幂等的要求)。
优点:满足业务最终一致要求的同时,有高性能;
缺点:消息系统和资源管理器需要共用数据库资源,不利于业务分层;系统延迟高,没有隔离性;一旦消息产生,后续业务不允许失败,一旦中间某个阶段出现无法继续的情况,即需要人工介入;系统开发成本高,开发人员需要关注分布式事务对业务的影响以及各种状态转换,测试困难,一般来说,开发成本会是不考虑事务的许多倍。

1.5.3.2 业务与消息解耦
上述方案中,消息资源和业务共用数据库资源,无法解决调用多于2个服务的业务场景,无法应对业务执行顺序发生变更的需求。因此,考虑将业务资源和消息资源解耦。
进程1:
push half message;
Begin local trx:
服务A DML;
Commit/Rollback;
If trx.success
Commit half message;
End if;
进程2:
for eash message in queue
begin local trx:
服务B DML;
commit/rollback;
if trx.success:
pop message;
end if;
end for;
进程1中,首先发送半消息,该消息还不能被消费,然后执行服务A,服务A执行成功后,使半消息生效,否则删除半消息。进程2的执行逻辑与上面相同。
如果服务A执行成功后、使半消息生效前,进程1crash,消息系统会看到一条半消息,但是不知道服务A是否执行成功,所以要求业务实现回调接口,用于检查服务A是否执行成功。
优点:满足业务最终一致的同时,实现高性能;消息数据独立存储,消除业务系统与消息系统间的耦合;
缺点:一次消息发送需要两次请求;业务处理服务需要实现消息状态回查接口;系统延迟高,没有隔离性;一旦消息产生,后续业务不允许失败,一旦中间某个阶段出现无法继续的情况,即需要人工接入;系统开发成本高,开发人员需要关注分布式事务对业务的影响以及各种状态转换,测试困难,一般来说,开发成本会是不考虑事务的许多倍。


1.6 TXC分布式事务解决方案


1.6.1 整体架构

TXC包含三种角色:客户端、资源管理器、事务协调器。其中,客户端通过事务协调器开启/提交分布式事务,通过资源管理器执行业务;资源管理器(包括数据库系统、消息系统等)负责具体的资源操作,在操作过程中,记录必要的事务日志并将执行状态汇报给事务协调器;事务协调器负责分布式事务的推进,为客户端发起的分布式事务请求分配全局唯一的事务ID,并记录资源管理器提交的事务分支的状态,最终负责全局事务的提交或回滚。
TXC通过两阶段提交方式进行分布式事务推进。客户端向事务协调器注册全局事务作为一阶段的开启的标记;分布式事务内的每一次资源(DB或消息)操作,均通过资源管理器进行,资源管理器向事务协调器注册一个事务分支;客户端通知事务协调器进行全局提交/全局回滚作为一阶段完成的标记。
3
TXC分布式事务的二阶段由事务协调器驱动,驱动所有事务分支执行提交或回滚操作,一旦确定某个分布式事务提交或回滚,则不断重试所有事务分支,直到完成整个分布式事务提交或回滚。
1


1.6.2 软件结构

TXC使用diamond实现软负载均衡,支持自动化的扩容和缩容,在业务规模发生变化时不对业务产生影响,并消除了事务协调器的单点故障风险。
业务方可通过TXC客户端API接入TXC事务,当调用已经接入TXC事务的HSF服务时,HSF服务会自动加入当前的全局事务,最大程度降低业务的开发成本。
TXC分布式事务中间件提供多种资源器支持,目前,已经完成TDDL、DRDS、Notify、MetaQ接入。
5


1.6.3 事务恢复

客户端崩溃或宕机
-- 事务超时而回滚
Remote服务崩溃或宕机
-- 分支提交或回滚由其他服务完成
服务端崩溃或宕机
-- Server重启后恢复未完成事务
“committing”状态事务完成提交
“begin”/“rollbacking”状态事务完成回滚


1.6.4 接入方法


1.6.4.1 跨多个数据源的分布式事务
    @TxcTransaction(timeout = 1000 * 60) 
public void 业务方法(DataSource db1, DataSource db2) {
    Connection conn1 = db1.getConnection();
    Connection conn2 = db2.getConnection();
    conn1.execute("sql1"); // 在数据库1上执行SQL
    conn2.execute("sql2"); // 在数据库2上执行SQL
}
Copy

1.6.4.2 跨多个服务的分布式事务
   @TxcTransaction(timeout = 1000 * 60) 
public void 业务方法() {
    call hsf1();           // 调用远程服务1
    call hsf2();           // 调用远程服务2
}
Copy

1.6.4.3 混合型分布式事务
  // 事务数据源会拦截HSF服务中的SQL,进行分布式事务控制
  // 对于暂时不支持的资源(Hbase、Redis…),由业务实现两阶段接口,事务框架在事务回滚时自动调用二阶段方法
   @TxcTransaction(timeout = 1000 * 60) 
public void 业务方法(DataSource db1) {
    db1.execute("sql1");     // 在数据库1上执行SQL
    call hsf1();                     // 调用远程服务1
    call userDefined()// 业务中调用一阶段, 事务框架自动调用二阶段
}
Copy


1.6.5 开发目标

(1)节省开发成本。提供简单的开发接口,以最小的接入成本解决分布式事务问题;
(2)节省运维成本。解决跨库、跨资源、跨服务的分布事务问题,消除了业务运行当中产生的不完整业务,大大节约了人工运维成本;
(3)高性能、节省计算机资源。解决分布式事务问题的同时,仍能够保持极高的系统性能;系统吞吐高,比常见分布式事务方案能够节约大量计算机资源;
(4)任何异常情况下的数据严格一致。分布式事务异常点繁多,给业务开发带来困难,并且很难保证极端情况下的数据严格一致,分布式事务中间件能够把业务从繁琐中解放出来接口,以最小的接入成本解决分布式事务问题。


1.7 TXC 的其他用法

(1)降低事务消息接入成本。对业务方来说,有时check方法很难实现,可通过调用TXC消息API降低开发成本;
(2)TXC可以脱离数据源提供两阶段协调器的功能。用户可采用TXC框架,轻松实现自定义的补偿性分布式事务或预留资源型分布式事务;
(3)实现重试型分布式事务,在业务正常执行的时候不做干预,仅补偿业务中的SQL异常,并驱动该SQL不断重试,直到成功;


评论区
Rick ©2018