java 分布式事务 mq_java开发与分布式事务

分布式事务

2PC

说到 2PC 就不得不聊数据库分布式事务中的 XA Transactions。

MySQL从5.0.3开始,InnoDB存储引擎支持XA事务(XA Transactions)。MySQL XA是基于X/Open CAE文档中的Distributed Transaction Processing:The XA Specification(DTP XA规范)实现的。

在 XA 协议中分为两阶段:

事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交。

事务协调器要求每个数据库提交数据,或者回滚数据。

优点:

尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于 MySQL 是从 5.5 开始支持。

缺点:

单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在***阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。

同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。

数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能。

比如在第二阶段中,假设协调者发出了事务 Commit 的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了 Commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

总的来说,XA 协议比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其***的弱点。

TCC事务

TCC事务其实主要包含两个阶段:Try阶段、Confirm/Cancel阶段。

从TCC的逻辑模型上我们可以看到,TCC的核心思想是,try阶段检查并预留资源,确保在confirm阶段有资源可用,这样可以最大程度的确保confirm阶段能够执行成功。

1)    try-尝试执行业务

完成所有业务检查(一致性)

预留必须业务资源(准隔离性)

2)    confirm-确认执行业务

真正执行业务

不作任何业务检查

只使用Try阶段预留的业务资源

Confirm操作必须保证幂等性

3)   cancel-取消执行业务

释放Try阶段预留的业务资源

Cancel操作必须保证幂等性

TCC处理逻辑

使用TCC事务时,伪代码如下所示:

@Compensable(confirmMethod = "transferConfirm", cancelMethod = "transferCancel")

@Transactional

public void transferTry(long fromAccountId, long toAccountId, int amount) {

//检查Tom账户

//锁定Tom账户

//锁定Tracy账户

}

@Transactional

public void transferConfirm(long fromAccountId, long toAccountId, int amount) {

//tom账户-10元

//tracy账户+10元

}

@Transactional

public void transferCancel(long fromAccountId, long toAccountId, int amount) {

//解除Tom账户锁定

//接触Tracy账户锁定

}

对于 TCC 来说适合一些:

强隔离性,严格一致性要求的活动业务。

执行时间较短的业务

本地消息表

本地消息表这个方案最初是 eBay 提出的,eBay 的完整方案 https://queue.acm.org/detail.cfm?id=1394128。

此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。

人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。

对于本地消息队列来说核心是把大事务转变为小事务。还是举上面用 100 元去买一瓶水的例子。

1. 当你扣钱的时候,你需要在你扣钱的服务器上新增加一个本地消息表,你需要把你扣钱和减去水的库存写入到本地消息表,放入同一个事务(依靠数据库本地事务保证一致性)。

2. 这个时候有个定时任务去轮询这个本地事务表,把没有发送的消息,扔给商品库存服务器,叫它减去水的库存,到达商品服务器之后,这时得先写入这个服务器的事务表,然后进行扣减,扣减成功后,更新事务表中的状态。

3. 商品服务器通过定时任务扫描消息表或者直接通知扣钱服务器,扣钱服务器在本地消息表进行状态更新。

4. 针对一些异常情况,定时扫描未成功处理的消息,进行重新发送,在商品服务器接到消息之后,首先判断是否是重复的。

如果已经接收,再判断是否执行,如果执行在马上又进行通知事务;如果未执行,需要重新执行由业务保证幂等,也就是不会多扣一瓶水。

本地消息队列是 BASE 理论,是最终一致模型,适用于对一致性要求不高的情况。实现这个模型时需要注意重试的幂等。

MQ 事务

在 RocketMQ 中实现了分布式事务,实际上是对本地消息表的一个封装,将本地消息表移动到了 MQ 内部。

下面简单介绍一下MQ事务,如果想对其详细了解可以参考:https://www.jianshu.com/p/453c6e7ff81c。

基本流程如下:

***阶段 Prepared 消息,会拿到消息的地址。

第二阶段执行本地事务。

第三阶段通过***阶段拿到的地址去访问消息,并修改状态。消息接受者就能使用这个消息。

如果确认消息失败,在 RocketMQ Broker 中提供了定时扫描没有更新状态的消息。

如果有消息没有得到确认,会向消息发送者发送消息,来判断是否提交,在 RocketMQ 中是以 Listener 的形式给发送者,用来处理。

如果消费超时,则需要一直重试,消息接收端需要保证幂等。如果消息消费失败,这时就需要人工进行处理,因为这个概率较低,如果为了这种小概率时间而设计这个复杂的流程反而得不偿失。

Saga 事务

Saga 是 30 年前一篇数据库伦理提到的一个概念。其核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

Saga 的组成:每个 Saga 由一系列 sub-transaction Ti 组成,每个 Ti 都有对应的补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。这里的每个 T,都是一个本地事务。

可以看到,和 TCC 相比,Saga 没有“预留 try”动作,它的 Ti 就是直接提交到库。

Saga 的执行顺序有两种:

T1,T2,T3,...,Tn。

T1,T2,...,Tj,Cj,...,C2,C1,其中 0 < j < n 。

Saga 定义了两种恢复策略:

向后恢复,即上面提到的第二种执行顺序,其中 j 是发生错误的 sub-transaction,这种做法的效果是撤销掉之前所有成功的 sub-transation,使得整个 Saga 的执行结果撤销。

向前恢复,适用于必须要成功的场景,执行顺序是类似于这样的:T1,T2,...,Tj(失败),Tj(重试),...,Tn,其中 j 是发生错误的 sub-transaction。该情况下不需要 Ci。

这里要注意的是,在 Saga 模式中不能保证隔离性,因为没有锁住资源,其他事务依然可以覆盖或者影响当前事务。

还是拿 100 元买一瓶水的例子来说,这里定义:

T1 = 扣 100 元,T2 = 给用户加一瓶水,T3 = 减库存一瓶水。

C1 = 加100元,C2 = 给用户减一瓶水,C3 = 给库存加一瓶水。

我们一次进行 T1,T2,T3 如果发生问题,就执行发生问题的 C 操作的反向。

上面说到的隔离性的问题会出现在,如果执行到 T3 这个时候需要执行回滚,但是这个用户已经把水喝了(另外一个事务),回滚的时候就会发现,无法给用户减一瓶水了。

这就是事务之间没有隔离性的问题。可以看见 Saga 模式没有隔离性的影响还是较大,可以参照华为的解决方案:从业务层面入手加入一 Session 以及锁的机制来保证能够串行化操作资源。

也可以在业务层面通过预先冻结资金的方式隔离这部分资源, ***在业务操作的过程中可以通过及时读取当前状态的方式获取到***的更新。(具体实例:可以参考华为的 Service Comb)

***

还是那句话,能不用分布式事务就不用,如果非得使用的话,结合自己的业务分析,看看自己的业务比较适合哪一种,是在乎强一致,还是最终一致即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值