230_RabbitMQ-高级-分布式事务

简述

:::info
分布式事务指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。
例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。
:::

分布式事务的方式

:::info
在分布式系统中,要实现分布式事务,无外乎那几种解决方案。
:::

两阶段提交(2PC)需要数据库厂商的支持,java组件有atomikos等。

:::info
两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。
:::
:::info
准备阶段
协调者询问参与者事务是否执行成功,参与者发回事务执行结果。
:::

230_RabbitMQ-高级-分布式事务
:::info
提交阶段
如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。
:::
230_RabbitMQ-高级-分布式事务
:::info

存在的问题

  • 同步阻塞 所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。
  • 单点问题 协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。
  • 数据不一致 在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
  • 太过保守 任意一个节点失败就会导致整个事务失败,没有完善的容错机制。
    :::

补偿事务(TCC) 严选,阿里,蚂蚁金服。

:::info
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:

  • Try 阶段主要是对业务系统做检测及资源预留
  • Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 – – – Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
  • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
    :::
    :::info
    举个例子,假入 Bob 要向 Smith 转账,思路大概是: 我们有一个本地方法,里面依次调用
    1:首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来。
    2:在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
    3:如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
    :::
    :::info
    优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些
    缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。
    :::

本地消息表(异步确保)比如:支付宝、微信支付主动查询支付状态,对账单的形式

:::info
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。

  • 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
  • 之后将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
  • 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
    :::
    230_RabbitMQ-高级-分布式事务
    :::info
    优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。
    缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
    :::

MQ 事务消息 异步场景,通用性较强,拓展性较高。

:::info
有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 Kafka 不支持。
以阿里的 RabbitMQ 中间件为例,其思路大致为:

  • 第一阶段Prepared消息,会拿到消息的地址。 第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
  • 也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RabbitMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RabbitMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
    :::
    230_RabbitMQ-高级-分布式事务
    :::info
    优点: 实现了最终一致性,不需要依赖本地数据库事务。
    缺点: 实现难度大,主流MQ不支持,RocketMQ事务消息部分代码也未开源。
    :::

总结

:::info
通过本文我们总结并对比了几种分布式分解方案的优缺点,分布式事务本身是一个技术难题,是没有一种完美的方案应对所有场景的,具体还是要根据业务场景去抉择吧。阿里RocketMQ去实现的分布式事务,现在也有除了很多分布式事务的协调器,比如LCN等,大家可以多去尝试。
:::

具体实现

分布式事务的完整架构图

230_RabbitMQ-高级-分布式事务
230_RabbitMQ-高级-分布式事务
美团外卖架构:
230_RabbitMQ-高级-分布式事务

系统与系统之间的分布式事务问题-数据不一致问题

230_RabbitMQ-高级-分布式事务

系统间调用过程中事务回滚问题-数据不一致问题

:::info
如果因为网络问题,运单接口新增运单成功,但接口超时,会造成订单接口新增订单回滚,结果就是:订单不新增,运单新增,出现数据不一致问题
:::

package com.xuexiangban.rabbitmq.service;
import com.xuexiangban.rabbitmq.dao.OrderDataBaseService;
import com.xuexiangban.rabbitmq.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
@Service
public class OrderService {
    @Autowired
    private OrderDataBaseService orderDataBaseService;
    // 创建订单
    @Transactional(rollbackFor = Exception.class) // 订单创建整个方法添加事务
    public void createOrder(Order orderInfo) throws Exception {
        // 1: 订单信息--插入丁订单系统,订单数据库事务
        orderDataBaseService.saveOrder(orderInfo);
        // 2:通过Http接口发送订单信息到运单系统
        String result = dispatchHttpApi(orderInfo.getOrderId());
        if(!"success".equals(result)) {
            throw new Exception("订单创建失败,原因是运单接口调用失败!");
        }
    }
    /**
     *  模拟http请求接口发送,运单系统,将订单号传过去 springcloud
     * @return
     */
    private String dispatchHttpApi(String orderId) {
        SimpleClientHttpRequestFactory factory  = new SimpleClientHttpRequestFactory();
        // 链接超时 > 3秒
        factory.setConnectTimeout(3000);
        // 处理超时 > 2秒
        factory.setReadTimeout(2000);
        // 发送http请求
        String url = "http://localhost:9000/dispatch/order?orderId="+orderId;
        RestTemplate restTemplate = new RestTemplate(factory);//异常
        String result = restTemplate.getForObject(url, String.class);
        return result;
    }
}

230_RabbitMQ-高级-分布式事务
230_RabbitMQ-高级-分布式事务
:::info
测试:订单接口调用运单接口设置的处理超时时间是2s,现在让运单接口睡眠3s,造成接口超时,会造成运单新增成功,订单接口新增订单回滚,结果就是:订单不新增,运单新增,出现数据不一致问题
:::
230_RabbitMQ-高级-分布式事务

基于MQ的分布式事务整体设计思路

230_RabbitMQ-高级-分布式事务

基于MQ的分布式事务消息的可靠生产问题

230_RabbitMQ-高级-分布式事务
:::info
注:基于MQ的分布式事务消息代码中不需要加事务注解@Transactional
前置条件:
  1. 创建消息状态表,状态字段:0未收到消息,1收到消息,2发送次数超限,需管理员排查,默认为0;消息发送次数字段:默认0
  2. 定时任务:发送消息状态表中状态为0消息到消息队列

生产者发送流程:

  1. 生产者发送消息到消息队列,消息队列接收到消息后,给予生产者消息回执
  2. 生产者收到消息回执后,有两种情况:一是发送成功,二是发送失败
  3. 发送成功,更新消息状态表中状态字段为1收到消息,消息发送次数字段加1
  4. 发送失败,更新消息状态表中消息发送次数字段加1,如果消息发送次数超过限制,如5次,更新状态字段为2发送次数超限,需管理员排查
  5. 定时任务,定时查询消息状态表中状态为0消息,发送到消息队列,重复1234步骤
    :::
    230_RabbitMQ-高级-分布式事务
    :::info
    配置为发布确认模式
    :::
    230_RabbitMQ-高级-分布式事务
    如果这个时候MQ服务器出现了异常和故障,那么消息是无法获取到回执信息。怎么解决呢?

定时重发

230_RabbitMQ-高级-分布式事务

基于MQ的分布式事务消息的可靠消费

230_RabbitMQ-高级-分布式事务
230_RabbitMQ-高级-分布式事务

消息重发

230_RabbitMQ-高级-分布式事务
:::info
消费者消费消息异常时,会重复消费消息,进入死循环,解决这个问题的方案如下:
控制重发的次数 + 死信队列,如果只有控制重发的次数,会造成消息丢失,所以要加上死信队列
:::
230_RabbitMQ-高级-分布式事务

230_RabbitMQ-高级-分布式事务

基于MQ的分布式事务消息的死信队列消息转移 + 人工处理

230_RabbitMQ-高级-分布式事务
如果死信队列报错就进行人工处理
230_RabbitMQ-高级-分布式事务
230_RabbitMQ-高级-分布式事务
230_RabbitMQ-高级-分布式事务
230_RabbitMQ-高级-分布式事务

基于MQ的分布式事务消息的死信队列消息重试注意事项

基于MQ的分布式事务消息的定式重发

总结

基于MQ的分布式事务解决方案优点:

1、通用性强
2、拓展方便
3、耦合度低,方案也比较成熟

基于MQ的分布式事务解决方案缺点:

1、基于消息中间件,只适合异步场景
2、消息会延迟处理,需要业务上能够容忍

建议

1、尽量去避免分布式事务
2、尽量将非核心业务做成异步

Original: https://www.cnblogs.com/wl3pb/p/16590085.html
Author: 清风(学习-踏实)
Title: 230_RabbitMQ-高级-分布式事务

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/581217/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 20年5月面试汇总

    redis: 数据结构 redis的五种基本数据结构: string、hash、set、zset、list、HyperLogLog…. 补充:BloomFilter等 …

    Java 2023年6月8日
    078
  • java如何获取一个文本文件的编码(格式)信息呢?

    文本文件是我们在windows平台下常用的一种文件格式, 这种格式会随着操作系统的语言不同,而出现其默认的编码不同 那么如何使用程序获取”文本文件”的编码方…

    Java 2023年6月15日
    078
  • JVM快速入门

    JVM的位置 JVM是运行在操作系统之上的。 JVM体系结构 类加载器 类加载器的作用:加载class文件 加载器:①.ApplicationClassLoader应用程序类加载器…

    Java 2023年6月8日
    077
  • Reference 引用类

    Reference Queue *Method Summary: Reference *Method Summary: void():Clears this reference o…

    Java 2023年6月8日
    072
  • 建造者设计模式

    可以看到这个学生类的属性是非常多的,所以构造方法不是一般的长,如果我们现在直接通过new的方式去创建: 可以看到,我们光是填参数就麻烦,我们还得一个一个对应着去填,一不小心可能就把…

    Java 2023年6月6日
    097
  • 机器学习(2)特征抽取

    特征抽取对文本等数据进行特征值化 sklearn特征抽取的API:sklearn.feature_extraction 字典特征抽取:对字典数据进行特征值化:sklearn.fea…

    Java 2023年6月8日
    085
  • ZMQ示例:使用 curve 进行加密通信

    1. ZMQ 官方文档 官方文档中提到的几个点都很关键: 一个 curve socket 既可以是服务端,也可以是客户端。但不可以既是服务端的同时又是客户端 一个 socket 可…

    Java 2023年5月30日
    078
  • 【Java】【52】处理报错:java.util.Arrays$ArrayList cannot be cast to java.util.ArrayList

    前言:项目需求:将一个逗号分隔的字符串转换成list,然后将该list克隆后赋值给一个新的newList。执行时出现了标题中的报错 正文: 原代码 参考博客: Original: …

    Java 2023年5月29日
    065
  • 简单日期格式化的使用

    简单日期格式化的应用 简单日期格式化的使用 面试题: 有一个时间 20…

    Java 2023年6月6日
    061
  • 深入剖析堆原理与堆排序

    堆的介绍 完全二叉树:完全二叉树是满二叉树去除最后N个节点之后得到的树((N \geq0, N \in N^*)) 大根堆:节点的父亲节点比自身节点大,比如根节点的值为(8),比其…

    Java 2023年6月8日
    082
  • bsd socket 网络通讯必备工具类

    传输数据的时候都要带上包头,包头有简单的又复杂的,简单的只要能指明数据的长度就够了。 这里我写了一个工具类,可以方便地将整型的数据长度转换为长度为 4 的字节数组。 另一方面,可以…

    Java 2023年5月29日
    084
  • 数据结构——稀疏数组

    数据结构——稀疏数组 棋局存档问题:如何将一局五子棋保存下来,并实现读档 利用稀疏数组进行数据保存,IO流写入/读取文件,实现读档。 简单代码实现: /** * @author 萝…

    Java 2023年6月9日
    086
  • 一文读懂Docker相关命令

    以下命令以centos为例 进程相关命令 启动docker服务 systemctl start docker 停止docker服务 systemctl start docker 重…

    Java 2023年6月7日
    097
  • Vue 利用后端的数据字典和Map对象实现表格列字段动态转义的处理方案

    1、前言 Vue中,使用el-table组件,经常遇到列字段转义的问题。常规处理方法有以下两种: 方法1:在模板中使用v-if,直接转义。如: N Y 方法2:使用formatte…

    Java 2023年6月14日
    0105
  • 理解Java BlockingQueue

    数据结构与算法是天生一对。 BlockingQueue叫做阻塞队列,在Java线程池相关的实现中有广泛的使用。 BlockingQueue实现的功能如下: 当队列为空时,往队列中读…

    Java 2023年5月29日
    081
  • 大厂推荐使用的网关解密:Fizz Gateway后台管理系统功能模块介绍

    前言 Fizz Gateway 是一个基于 Java开发的微服务聚合网关,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目…

    Java 2023年6月9日
    0118
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球