SpringCloud Alibaba系列——16Seata原理及应用(上)

老老老JR老北
发布于 2022-8-16 13:58
浏览
0收藏

作者 | 一起撸Java
来源 |今日头条

学习目标

  1. 了解分布式事务。
  2. 理解Seata的概念
  3. Springboot如何整合Seata
    第1章 分布式系统关注点
    1.1 CAP理论
  • C:consistency(强一致性):所有的节点上的数据时刻保持同步
  • A:Avaliablity(可用性):每个请求都能接受到一个响应,无论响应成功或失败
  • P:Partition Tolerance(分区容错):系统应该能持续提供服务,即使系统内部有消息丢失(分区)
    高可用、数据一致是很多系统设计的目标,但是分区又是不可避免的事情:

CA:如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但其实分区不是你想不想的问题,而是始终会存在,因此CA的系统更多的是允许分区后各子系统依然保持CA。(单体架构)

CP:如果不要求A(可用),相当于每个请求都需要在Server之间强一致,而P(分区)会导致同步时间无限延长,如此CP也是可以保证的。很多传统的数据库分布式事务都属于这种模式。

AP:要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。现在众多的NoSQL都属于此类。

1.2 Base理论
BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网分布式系统实践的总结,是基于CAP定律逐步演化而来。其核心思想是即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

1、Basically Available(基本可用):假设系统,出现了不可预知的故障,但还是能用,相比较正常的系统而言;

  • 响应时间上的损失:正常情况下的搜索引擎0.5秒即返回给用户结果,而基本可用的搜索引擎可以在2秒作用返回结果。
  • 功能上的损失:在一个电商网站上,正常情况下,用户可以顺利完成每一笔订单。但是到了大促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
    2、Soft State(软状态):相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种“硬状态”。软状态允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。

3、Eventually Consistent(最终一致性):上面说软状态,然后不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性,从而达到数据的最终一致性。这个时间期限取决于网络延时、系统负载、数据复制方案设计等等因素。

第2章 数据一致性问题
2.1 2PC
2PC 是二阶段提交(Two-phase Commit)的缩写,顾名思义,这个协议分两阶段完成。第一个阶段是准备阶段,第二个阶段是提交阶段,准备阶段和提交阶段都是由事务管理器(协调者)发起的,协调的对象是资源管理器(参与者)。二阶段提交协议的概念来自 X/Open 组织提出的分布式事务的规范 XA 协议,协议主要定义了(全局)事务管理器和(局部)资源管理器之间的接口。XA 接口是双向的系统接口,在事务管理器以及一个或多个资源管理器之间形成通信桥梁。Java 平台上的事务规范 JTA(Java Transaction API)提供了对 XA 事务的支持,它要求所有需要被分布式事务管理的资源(由不同厂商实现)都必须实现规定接口(XAResource 中的 prepare、commit 和 rollback 等)。

  • 准备阶段:协调者向参与者发起指令,参与者评估自己的状态,如果参与者评估指令可以完成,参与者会写 redo 和 undo 日志,然后锁定资源,执行操作,但是并不提交。
  • 提交阶段:如果每个参与者明确返回准备成功,也就是预留资源和执行操作成功,协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者明确返回准备失败,也就是预留资源或者执行操作失败,协调者向参与者发起中止指令,参与者取消已经变更的事务,执行 undo 日志,释放锁定的资源。
    SpringCloud Alibaba系列——16Seata原理及应用(上)-鸿蒙开发者社区两阶段提交协议在准备阶段锁定资源,是一个重量级的操作,并能保证强一致性,但是实现起来复杂、成本较高,不够灵活,更重要的是它有如下致命的问题:

阻塞:从上面的描述来看,对于任何一次指令必须收到明确的响应,才会继续做下一步,否则处于阻塞状态,占用的资源被一直锁定,不会被释放。

单点故障:如果协调者宕机,参与者没有了协调者指挥,会一直阻塞,尽管可以通过选举新的协调者替代原有协调者,但是如果之前协调者在发送一个提交指令后宕机,而提交指令仅仅被一个参与者接受,并且参与者接收后也宕机,新上任的协调者无法处理这种情况。

脑裂:协调者发送提交指令,有的参与者接收到执行了事务,有的参与者没有接收到事务,就没有执行事务,多个参与者之间是不一致的。

举个例子:假如一对夫妻要离婚,夫妻二人就是离婚这个事务的参与者,这个时候有一个律师则作为协调者;

首先第一阶段,律师给夫妻二人发律师函,通知二人打印离婚协议,然后在离婚协议上签字,OK,如果二人都完成了,然后就给律师一个回复通知;

第二阶段,律师收到两个人的完成通知,然后在通知夫妻二人可以去民政局,正式提交离婚。

2.2 3PC
三阶段提交协议(3PC 协议)是两阶段提交协议的改进版本。它通过超时机制解决了阻塞的问题,并且把两个阶段增加为三个阶段:

  • 询问阶段:协调者询问参与者是否可以完成指令,协调者只需要回答是还是不是,而不需要做真正的操作,这个阶段参与者在等待超时后会自动中止。
  • 准备阶段:如果在询问阶段所有的参与者都返回可以执行操作,协调者向参与者发送预执行请求,然后参与者写 redo 和 undo 日志,锁定资源,执行操作,但是

不提交操作;如果在询问阶段任何参与者返回不能执行操作的结果,则协调者向参与者发送中止请求,这里的逻辑与两阶段提交协议的的准备阶段是相似的,这个阶段参与者在等待超时后会自动提交。

  • 提交阶段:如果每个参与者在准备阶段返回准备成功,也就是预留资源和执行操作成功,协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者返回准备失败,也就是预留资源或者执行操作失败,协调者向参与者发起中止指令,参与者取消已经变更的事务,执行 undo 日志,释放锁定的资源,这里的逻辑与两阶段提交协议的提交阶段一致。
    SpringCloud Alibaba系列——16Seata原理及应用(上)-鸿蒙开发者社区询问阶段:询问阶段可以确保尽可能早地发现无法执行操作而需要终止的行为,但是它并不能发现所有的这种行为,只会减少这种情况的发生。

等待超时:如果在询问阶段等待超时,则自动中止;如果在准备阶段之后等待超时,则自动提交。这也是根据概率统计上的正确性最大。

2.3 TCC
TCC 将事务的提交过程分为 try-confirm-cancel(实际上 TCC 就是 try、confirm、cancel 的简称) 三个阶段:

1、try:完成业务检查、预留业务资源

2、confirm:使用预留的资源执行业务操作(需要保证幂等性)

3、cancel:取消执行业务操作,释放预留的资源(需要保证幂等性)

①事务发起方向事务协调器发起事务请求,事务协调器调用所有事务参与者的 try 方法完成资源的预留,这时候并没有真正执行业务,而是为后面具体要执行的业务预留资源,这里完成了一阶段。

②如果事务协调器发现有参与者的 try 方法预留资源时候发现资源不够,则调用参与方的 cancel 方法回滚预留的资源,需要注意 cancel 方法需要实现业务幂等,因为有可能调用失败(比如网络原因参与者接受到了请求,但是由于网络原因事务协调器没有接受到回执)会重试。

③如果事务协调器发现所有参与者的 try 方法返回都 OK,则事务协调器调用所有参与者的 confirm 方法,不做资源检查,直接进行具体的业务操作。

④如果协调器发现所有参与者的 confirm 方法都 OK 了,则分布式事务结束。

⑤如果协调器发现有些参与者的 confirm 方法失败了,或者由于网络原因没有收到回执,则协调器会进行重试。这里如果重试一定次数后还是失败,会做事务补偿。SpringCloud Alibaba系列——16Seata原理及应用(上)-鸿蒙开发者社区TCC与2PC的区别:

如果拿TCC事务的处理流程与2PC两阶段提交做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面的处理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使得降低锁冲突、提高吞吐量成为可能。 TCC的不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。

2.4 MQ(非事务消息)
通常情况下,在使用非事务消息支持的MQ产品时,我们很难将业务操作与对MQ的操作放在一个本地事务域中管理。通俗点描述,还是以“跨行转账”为例,我们很难保证在扣款完成之后对MQ投递消息的操作就一定能成功。这样一致性似乎很难保证。

先从消息生产者这端来分析,请看伪代码:

public void trans(){    try{        //1.操作数据库        boolean result = dao.update(model);//操作数据库失败,会抛异常        //2.如果第一步成功,则操作消息队列(投递消息)        if(result){            mq.append(model);//如果mq.append方法执行失败(投递消息失败),方法内部会抛异常        }    }catch(Exception e){        rollback();//如果发生异常,则回滚    }}

 

根据上述代码及注释,我们来分析下可能的情况:

  1. 操作数据库成功,向MQ中投递消息也成功,皆大欢喜
  2. 操作数据库失败,不会向MQ中投递消息了
  3. 操作数据库成功,但是向MQ中投递消息时失败,向外抛出了异常,刚刚执行的更新数据库的操作将被回滚

从上面分析的几种情况来看,貌似问题都不大的。那么我们来分析下消费者端面临的问题:

  1. 消息出列后,消费者对应的业务操作要执行成功。如果业务执行失败,消息不能失效或者丢失。需要保证消息与业务操作一致
  2. 尽量避免消息重复消费。如果重复消费,也不能因此影响业务结果

如何保证消息与业务操作一致,不丢失?

主流的MQ产品都具有持久化消息的功能。如果消费者宕机或者消费失败,都可以执行重试机制的(有些MQ可以自定义重试次数)。

如何避免消息被重复消费造成的问题?

  1. 保证消费者调用业务的服务接口的幂等性 。
  2. 通过消费日志或者类似状态表来记录消费状态,便于判断(建议在业务上自行实现,而不依赖MQ产品提供该特性)。
    总结:这种方式比较常见,性能和吞吐量是优于使用关系型数据库消息表的方案。如果MQ自身和业务都具有高可用性,理论上是可以满足大部分的业务场景的。不过在没有充分测试的情况下,不建议在交易业务中直接使用。

2.5 MQ(事务消息)
举个例子,Bob向Smith转账,那我们到底是先发送消息,还是先执行扣款操作? 好像都可能会出问题。如果先发消息,扣款操作失败,那么Smith的账户里面会多出一笔钱。反过来,如果先执行扣款操作,后发送消息,那有可能扣款成功了但是消息没发出去,Smith收不到钱。除了上面介绍的通过异常捕获和回滚的方式外,还有没有其他的思路呢?

下面以阿里巴巴的RocketMQ中间件为例,分析下其设计和实现思路。

RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,第二阶段执行本地事物,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。细心的读者可能又发现问题了,如果确认消息发送失败了怎么办?

RocketMQ会定期扫描消息集群中的事物消息,这时候发现了Prepared消息,它会向消息发送者确认,Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。如下图:SpringCloud Alibaba系列——16Seata原理及应用(上)-鸿蒙开发者社区总结:各大知名的电商平台和互联网公司,几乎都是采用类似的设计思路来实现“最终一致性”的。这种方式适合的业务场景广泛,而且比较可靠。不过这种方式技术实现的难度比较大。目前主流的开源MQ(ActiveMQ、RabbitMQ、Kafka)均未实现对事务消息的支持,所以需二次开发或者新造轮子。

2.6 如何保证幂等性

  • 衰减重试
  • 消息重发
    2.7 幂等的实现
  • 判断重复请求。
  • 状态机
  • 生成token (唯一标记)
  • 数据库的唯一约束(Unique Key) -> ( MD5 )
  • redis 。 setNX

分类
已于2022-8-16 13:58:18修改
收藏
回复
举报
回复
    相关推荐