常见的分布式事务解决方案,你会几种?

今天我们来聊一聊分布式事务,在传统的单体应用中,事务的控制非常简单,Spring框架都为我们做了封装,我们只需简单地使用@Transactional注解就能进行事务的控制,然而在分布式应用中,传统的事务方案就出现了极大的问题:

图片

当用户进行下单操作时,我们需要查询出当前下单的用户、购物车中的商品、扣减商品库存,这些内容都被划分到不同的模块中,所以一个下单的操作往往需要远程调用多个模块。假设在执行下单流程中,扣减库存成功,但是在扣减用户余额的时候出现了异常,此时用户模块中会回滚扣减的金额,但由于用户模块和库存模块是相互独立的,库存模块无法感知到用户模块出现了异常并执行回滚操作,由此导致用户没有花费一分钱就白嫖到了商品,这显然是不允许存在的。

分布式事务解决方案

下面列举一些分布式事务的解决方案:

  • 2PC模式

  • TCC事务补偿

  • 最大努力通知

  • 可靠消息

2PC模式

2PC意为二阶段提交,又叫XA Transactions,其中,XA是一个两阶段提交协议,该协议将事务分为两个阶段:

  1. 第一阶段:事务协调器要求每个涉及到事务的数据库预提交此操作,并响应是否可以提交

  2. 第二阶段:事务协调器要求每个数据库提交数据,其中,如果有任何一个数据库否决此次提交,那么所有的数据库都会被要求回滚它们在此事务中修改的内容

2PC非常好理解,就是在所有涉及事务操作的数据库之上建立了一个事务协调器,该协调器能够管理这些数据库的事务,如下图所示,第一阶段:

图片

第二阶段:

图片

2PC的优势是简单,实现成本低,但缺点也非常明显,性能低下,在MySQL和一些NoSQL数据库中的支持也不够。

TCC事务补偿

2PC模式遵循的是ACID原则,即:原子性、一致性、隔离性、持久性,它是一种强一致性的设计,而事实上,很多情况下我们都无法做到强一致性,或者想要实现强一致性成本比较高。eBay 的架构师 Dan Pritchett在ACM上发表文章提出了BASE理论,该理论指出即使无法做到强一致性,但应用可以采用合适的方式达到最终一致性,最终一致性对于数据的要求并不是非常严格,它不需要系统做到数据实时保持最新状态,而是数据在经过一段时间后,最终能够达到一致即可,TCC事务补偿方案就是一种柔性事务的设计,它能够保证数据的最终一致性,一般是在业务层进行实现的。

图片

如图所示,我们必须在需要被事务控制的服务中编写Try、Confirm、Cancel三个方法,此时,业务服务会尝试调用每个服务的Try方法,若是没有出现问题,则由事务管理器调用Confirm方法进行提交,若是出现问题,则调用Cancel方法进行回滚。TCC事务补偿方案也比较好理解,但它对业务的侵入较大,且与业务耦合在了一起。

最大努力通知

最大努力通知方案也是柔性事务的设计,它按规律进行消息通知,不保证数据一定能通知成功,但会提供可查询接口进行核对。如果你在项目中对接过支付宝支付服务,就应该清楚,支付宝在付款后采用的就是最大努力通知方案,支付宝会每隔一段时间发送一个通知来告诉开发者订单的支付情况,只有返回了 success 数据后支付宝才会停止通知。

可靠消息

可靠消息仍然只保证数据的最终一致性,且它需要借助消息中间件来完成,当某个服务在事务提交之前,会向消息中间件发送一条消息,再根据本地事务的执行状态发送Commit或者RollBack给消息中间件,消费方根据对应的状态对事务进行对应的处理。

RabbitMQ实现数据的最终一致性

想象一个场景,用户在进行下单操作之后,会有30分钟的时间让用户进行付款,在用户付款之前,商品的库存并没有真正的扣除,而是进行锁定。若是用户在规定时间内付款成功,则需要真正扣减库存;若是用户在规定时间内没有付款,则需要将锁定的商品库存重新解锁,这里涉及到两个模块之间的事务操作:

图片

当用户下单后,订单模块需要保存本次订单的库存工作单,记录的是哪些商品需要被锁定的库存数,并远程调用库存模块锁定库存,此时需要等待用户进行付款,若是用户未付款,则解锁库存。那么如何知道哪些订单是用户超时未付款需要解锁的呢?我们可以编写一个定时任务,让其每隔一定的时间就去扫描订单,并判断是否超时,若是超时则解锁库存。不过这种方式并不理想,不仅效率低,而且扫描时间也不好确定。我们可以使用消息中间件来解决这一问题:

图片

订单模块在生成订单后向RabbitMQ发送一条消息,并由RabbitMQ暂时保存,当30分钟过后,订单过期,RabbitMQ再将消息提供给库存模块进行消费, 当库存模块得到消息后就明白有订单过期了,再去解锁对应的库存即可,所以接下来我们面临的问题就是:如何将消息在RabbitMQ中保存30分钟,当消息过期后再交给库存模块消费。

延时队列

在RabbitMQ中,我们可以实现一个延时队列,消息进入延时队列后不会立马被消费,而是需要等待设定的时间,在实现之前,需要清楚两个概念:

  • 死信

  • 死信路由

死信

在RabbitMQ中,我们可以为消息设置一个存活时间TTL(time to live),当消息超过了存活时间,就可以认为这个消息已经死了,称为 死信 。一个消息在满足如下条件时会进入死信路由:

  1. 消息被Consumer拒收,并且reject方法的参数中requeue值为false,即:该消息被拒收后,不会再进入消息队列

  2. 消息超过了TTL时间,导致消息过期

  3. 消息队列满了,排在前面的消息会被丢弃或者扔到死信路由上

死信路由

在死信概念中,我们一直强调一个词, 死信路由 ,其实,它就是一个普通的路由,只是当某个队列绑定了死信路由后,该消息队列中的消息过期了,就会自动触发消息的转发,消息会被扔到死信路由上。通过死信和死信路由,我们就能够实现一个延时队列,如图所示:

图片

当生产者生产了一个消息后,会通过交换器放入一个队列,该队列比较特殊,在该队列中的消息存活时间均为30分钟,并且当这些消息过期成为死信后,会被交给死信路由,死信路由再将消息放入另一个消息队列,这样,该消息队列便保证了每次放入的消息都经过了30分钟,因为只有死信才能进入该队列,要想实现这一过程,我们需要为那个特殊的消息队列设置一些属性值:

  • x-dead-letter-exchange:xxx        设置死信路由

  • x-dead-letter-routing-key:xxx     设置死信路由键

  • x-message-ttl:1800 000               设置消息的存活时间为30分钟

将其类比到具体的业务场景中,就比如订单超时自动解锁库存的需求,其设计如下图所示:

图片

执行流程如下:

  1. Publisher发送消息给路由order.delay.exchange,路由键为order.delay

  2. 路由order.delay.exchange将消息放入绑定关系为order.delay的消息队列order.delay.queue

  3. 消息队列order.delay.queue中消息的存活时间为30分钟,当消息过期后,消息会交给路由order.exchange,路由键为order

  4. 路由order.exchange将消息放入绑定关系为order的消息队列order.queue

  5. Consumer监听消息队列order.queue,保证了消费的消息均是过期的

不过这一过程还可以再简化一下:

图片

它与刚才唯一的区别在于少了一个路由,Publisher在将消息发送给路由order.delay.exchange之后,会将消息放入队列order.delay.queue,我们让该队列在消息过期后仍然将消息交给路由order.delay.exchange,这样就节省了一个路由的资源。

代码实现

概念吹得天花乱坠,不动手实现一下始终是无法深刻理解的,所以,我们就通过一个案例来感受一下延迟队列的效果。

创建一个SpringBoot应用,并使用代码创建出路由、消息队列及其它们之间的关系:

@Configuration
public class MyRabbitMQConfig {

    @Bean
    public Queue orderDelayQueue() {
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", "order.delay.exchange");
        arguments.put("x-dead-letter-routing-key", "order.release.order");
        arguments.put("x-message-ttl", 1000 * 10);
        return new Queue("order.delay.queue", true, false, false, arguments);
    }

    @Bean
    public Queue orderReleaseOrderQueue() {
        return new Queue("order.release.order.queue", true, false, false);
    }

    @Bean
    public Exchange orderDelayExchange() {
        return new TopicExchange("order.delay.exchange", true, false);
    }

    @Bean
    public Binding orderCreateOrderBinding() {
        return new Binding("order.delay.queue",
                           Binding.DestinationType.QUEUE,
                           "order.delay.exchange",
                           "order.create.order", null);
    }

    @Bean
    public Binding orderReleaseOrderBinding() {
        return new Binding("order.release.order.queue",
                           Binding.DestinationType.QUEUE,
                           "order.delay.exchange",
                           "order.release.order", null);
    }
}

需要注意的是使用这种方式创建需要发送一下消息:

@RestController
public class TestController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/test")
    public String test(){
        rabbitTemplate.convertAndSend("order.delay.queue","message");
        return "test";
    }
}

当发送消息到队列 order.delay.queue 时,RabbitMQ便会创建出队列和路由:

图片

图片

接下来我们编写一个监听方法,它用来监听队列 order.release.order.queue :

@RabbitListener(queues = "order.release.order.queue")
public void listener(Message message) {
    System.out.println("收到消息:" + message);
}

此时我们访问 http://localhost:8080/test ,就会发送一条消息到队列,再经过10秒钟的时间,消息会过期,消息便会进入队列 order.release.order.queue ,而我们监听的又是这个队列,所以总能收到10秒后过期的消息:

收到消息:(Body:'message' ......)
收到消息:(Body:'message' ......)
收到消息:(Body:'message' ......)

通过这样的方式,我们便能够实现数据的最终一致性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/219539.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

通信标准化协会,信通院及量子信息网络产业联盟调研玻色量子,共绘实用化量子未来!

8月14日&#xff0c;中国通信标准化协会&#xff0c;信通院标准所及量子信息网络产业联盟等单位领导走访调研北京玻色量子科技有限公司&#xff08;以下简称“玻色量子”&#xff09;&#xff0c;参观了玻色量子公司及自建的十万颗粒洁净度的光量子信息技术实验室&#x1f517;…

ROS2教程02 ROS2的安装、配置和测试

ROS2的安装和配置 版权信息 Copyright 2023 Herman YeAuromix. All rights reserved.This course and all of its associated content, including but not limited to text, images, videos, and any other materials, are protected by copyright law. The author holds a…

python超详细基础文件操作【建议收藏】

文章目录 前言1 文件操作1.1 文件打开与关闭1.1.1 打开文件1.1.2 关闭文件 1.2 访问模式及说明 2 文件读写2.1 写数据&#xff08;write&#xff09;2.2 读数据&#xff08;read&#xff09;2.3 读数据&#xff08;readlines&#xff09;2.3 读数据&#xff08;readline&#x…

机器学习-ROC曲线:技术解析与实战应用

本文全面探讨了ROC曲线&#xff08;Receiver Operating Characteristic Curve&#xff09;的重要性和应用&#xff0c;从其历史背景、数学基础到Python实现以及关键评价指标。文章旨在提供一个深刻而全面的视角&#xff0c;以帮助您更好地理解和应用ROC曲线在模型评估中的作用。…

CSS中 设置文字下划线 的几种方法

在网页设计和开发中&#xff0c;我们经常需要对文字进行样式设置&#xff0c;包括字体,颜色&#xff0c;大小等&#xff0c;其中&#xff0c;设置文字下划线是一种常见需求 一 、CSS种使用 text-decoration 属性来设置文字的装饰效果&#xff0c;包括下划线。 常用的取值&…

蓝桥杯算法心得——仙界诅咒(dfs)

大家好&#xff0c;我是晴天学长&#xff0c;搜索型的dfs&#xff0c;差点开二维矩阵了&#xff0c;仔细一想&#xff0c;没那么夸张啊&#xff0c;哈哈哈&#xff0c;需要的小伙伴可以关注支持一下哦&#xff01;后续会继续更新的。&#x1f4aa;&#x1f4aa;&#x1f4aa; 1…

计算机视觉之手势、面部、姿势捕捉以Python Mediapipe为工具

计算机视觉之手势、面部、姿势捕捉以 Python Mediapipe为工具 文章目录 1.Mediapipe库概述2.手势捕捉(hands)3.面部捕捉(face)4.姿势捕捉(pose) 1.Mediapipe库概述 Mediapipe是一个开源且强大的Python库&#xff0c;由Google开发和维护。它提供了丰富的工具和功能&#xff0c…

java--接口概述

1.认识接口 ①java提供了一个关键字interface&#xff0c;用这个关键字我们可以定义出一个特殊的结构&#xff1a;接口。 ②注意&#xff1a;接口不能创建对象&#xff1b;接口是用来被类实现(implements)的&#xff0c;实现接口的类称为实现类。 ③一个类可以实现多个接口(接…

ROS2教程05 ROS2服务

ROS2服务 版权信息 Copyright 2023 Herman YeAuromix. All rights reserved.This course and all of its associated content, including but not limited to text, images, videos, and any other materials, are protected by copyright law. The author holds all right…

热点新闻 | 许战海:零食行业的革新之道

2023年11月29日&#xff0c;华糖万商大会在南京国际会展中心隆重举行。著名战略定位咨询专家许战海受邀出席&#xff0c;在“量贩零食产业年度盛典”上发表了主题为《如何通过竞争战略布局年度规划》的精彩演讲&#xff0c;吸引了众多业界关注。 演讲中&#xff0c;许战海老师指…

23史上最全版---SQL注入详解

漏洞原因 一些概念&#xff1a; SQL&#xff1a;用于数据库中的标准数据查询语言。 web分为前端和后端&#xff0c;前端负责进行展示&#xff0c;后端负责处理来自前端的请求并提供前端展示的资源。 而数据库就是存储资源的地方。 而服务器获取数据的方法就是使用SQL语句进…

c语言编译优化引发问题

问题描述 同样的代码,不优化编译,可以正常执行,经过-O2优化编译后,代码被卡住.整体功能涉及多进程,多线程操作. 问题发现 经过加打印,发现卡在while(a!0);//死循环,等待特殊事件发生来解开循环 a初始化为-1; 过一会后,另外有个线程,当特定事件发生的时候,将a置为0; 通过加打…

【云备份】客户端实现 及 项目整体总结

文章目录 客户端客户端实现思想客户端文件操作类的设计与拷贝Util.hpp的设计data.hpp的设计Storage —— 持久化存储Initload——数据初始化加载 cloud.hpp的设计GetFileIdentifier——创建文件唯一标识Upload—— 文件上传IsNeedupload —— 客户端文件是否需要上传判断RunMod…

【原创分享】高功率电源PCB设计中变压器下方走线的关键技巧

高功率电源的设计中&#xff0c;变压器起到了电能的传递与转换的重要作用。变压器下方的走线设计不仅涉及到电路的功率传输效率&#xff0c;还与电磁兼容性&#xff08;EMC&#xff09;、热管理以及电路的可靠性密切相关。 1. 走线布局 在进行变压器下方走线设计时&#xff0c…

Vmware虚拟机简介和安装

作者&#xff1a;余小小 常见的虚拟机 vmwarevirtualBox Vmware 运行在win系统上centos运行在Vm上 先安装vm&#xff0c;在安装centos系统 Vmware介绍 不用分区或者重开机&#xff0c;就可以在同一台pc上使用多种操作系统完全隔离&#xff0c;且保护不同的操作系统环境和文…

Kubernetes常用工作负载控制器

文章目录 一、常用负载控制器是什么二、Deployment控制器1.介绍2.使用流程3.应用部署4.应用升级5.滚动升级实现原理&#xff08;replicaset控制器&#xff09;6.滚动升级实现流程7.滚动升级策略8.应用实例扩容和缩容9.应用发布失败回滚10.应用下线 三、DaemonSet控制器四、Job控…

Linux修改时区失败,手动修改localtime无效

有时候改了这个也不行&#xff0c;用命令行修改也不行 解决办法 &#xff1a;cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 或者想改其他时区的直接 ll /usr/share/zoneinfo/ 查看

2023年1月18日 Go生态洞察:开发者的声音与Go的进化

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

WebGL开发交互式艺术品技术方案

开发交互式艺术品需要使用 WebGL 技术&#xff0c;并结合其他前端技术以实现丰富的用户体验。以下是一个可能的技术方案&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.WebGL 框架&#xff1a; 选…

AWS攻略——子网

文章目录 分配子网给Public子网分配互联网网关创建互联网网关附加到VPC 给Public子网创建路由表关联子网 打通Public子网和互联网网关 创建Public子网下的EC2进行测试配置Private子网路由给Private子网创建路由表附加在Private子网 创建Private子网下的EC2进行测试创建实例在跳…