图解分布式事务实现原理(一)

参考

本文参考https://zhuanlan.zhihu.com/p/648556608,在小徐的基础上做了个人的笔记。


分布式事务场景

事务核心特性

在聊分布式事务之前,我们先理清楚有关于 “事务” 的定义.

事务 Transaction,是一段特殊的执行程序,其需要具备如下四项核心性质:

在这里插入图片描述
当涉及到事务处理时,有四个核心要素,它们被称为事务的ACID四大特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。这些特性在关系型数据库范围内通常较容易实现,因为数据和操作都在同一个数据库内。然而,当一个事务涉及到跨越不同的数据库、服务或存储组件时,这个问题就变得更加复杂和有趣,这正是我们今天要重点讨论的“分布式事务”领域所涉及的问题。

分布式事务概念

由于数据库的拆分或分布式架构(微服务)不可避免的带来了分布式事务的问题。如下为当前针对分布式事务的工程实践和处理方式。

  • 基于业务逻辑和应用场景最小化分布式事务边界
    言外之意就是说应该在设计阶段尽可能规避没必要的分布式事务场景。
  • 基于 XA 的强一致性事务
    XA模式是传统的强一致性分布式事务解决方案,性能较低且锁资源竞争突出。XA的实现方式存在长事务风险且锁资源严重。在实际业务中使用较少,本文不做更多讨论。
  • 追求最终一致性的柔性事务
    柔性事务通过放宽对强一致性要求,而是通过反向补偿来达到最终一致性,同时换取系统吞吐量的提升和缓解锁资源竞争。目前,Seata 框架提供了多种事务管理模式来支持柔性事务的落地实现。

虚拟业务场景设计

下面我们通过一个常见的场景问题引出有关于分布式事务的话题.

假设我们在维护一个电商后台系统,每当在处理一笔来自用户创建订单的请求时,需要执行两步操作:

  • 从账户系统中,扣减用户的账户余额
  • 从库存系统中,扣减商品的剩余库存

从业务流程上来说,这个流程需要保证具备事务的原子性,即两个操作需要能够一气呵成地完成执行,要么同时成功,要么同时失败,不能够出现数据状态不一致的问题,比如发生从用户账户扣除了金额但商品库存却扣减失败的问题。

然而从技术流程上来讲,两个步骤是相对独立的两个操作,底层涉及到的存储介质也是相互独立的,因此无法基于本地事务的实现方式。

在这里插入图片描述

分布式事务的实现确实面临着很高的难度,但在业界已经提出了一套被广泛认可并应用的解决方案。这些解决方案将在后续的章节中介绍。在此之前,我们需要明确在分布式事务的实现中,所谓的数据状态一致性需要做出妥协:

  • 数据状态一致性:在分布式事务中,我们所谈论的数据状态一致性指的是数据的最终一致性,而不是即时一致性。即时一致性通常在分布式系统中难以实现,因为网络延迟和不同组件之间的通信可能导致即时一致性变得不切实际。因此,在分布式环境中,我们更关注确保数据最终达到一致的状态,即经过一段时间后,系统的各个节点都会收敛到相同的数据状态。

  • 百分之百的一致性无法保证:分布式事务中的一个根本挑战是无法百分之百地保证数据状态的一致性。这是因为分布式系统的稳定性和一致性受到网络环境的影响,以及与第三方系统的交互等多种因素的影响。即使采用了复杂的分布式事务协议和机制,也难以消除所有可能的故障和不一致性。

因此,分布式事务的实现需要在数据一致性和系统性能之间寻找平衡。通常情况下,分布式系统会采用某种程度的最终一致性,同时尽力减小数据不一致性的发生概率。这可能涉及到使用分布式事务协议、分布式锁、版本控制等技术手段,以确保在大多数情况下数据状态是一致的。但在特殊情况下,仍然需要处理可能的不一致性问题,并设计恢复机制来纠正这些问题。因此,分布式事务的实现需要权衡各种因素,以满足系统的要求和可用性目标。


事务消息方案

首先,一类偏狭义的分布式事务解决方案是基于消息队列 MessageQueue(后续简称 MQ)实现的事务消息 Transaction Message.

RocketMQ 简介

RocketMQ 是阿里基于 java 实现并托管于 apache 基金会的顶级开源消息队列组件,其中事务消息 TX Msg 也是 RocketMQ 现有的一项能力. 本章将主要基于 RocketMQ 针对事务消息的实现思路展开介绍.

RocketMQ github 地址:https://github.com/apache/rocketmq

在这里插入图片描述

kafka

在这里插入图片描述
Kafka(Apache Kafka)是一种高吞吐量、分布式、持久性的消息传递系统,最初由LinkedIn开发,并且后来成为了Apache软件基金会的一个顶级项目。Kafka旨在处理大量数据流,并支持实时数据流处理应用程序。
Kafka的典型用例包括日志聚合、事件溯源、监控和度量、实时数据分析、日志流式处理、电子商务订单处理等。它在大规模数据处理、实时数据流和事件驱动架构中广泛使用。

基于 MQ 实现分布式事务

我们知道在 MQ 组件中,通常能够为我们保证的一项能力是:投递到 MQ 中的消息能至少被下游消费者 consumer 消费到一次,即所谓的 at least once 语义.

基于此,MQ 组件能够保证消息不会在消费环节丢失,但是无法解决消息的重复性问题. 因此,倘若我们需要追求精确消费一次的目标,则下游的 consumer 还需要基于消息的唯一键执行幂等去重操作,在 at least once 的基础上过滤掉重复消息,最终达到 exactly once 的语义.

在这里插入图片描述
依赖于 MQ 中 at least once 的性质,我们简单认为,只要把一条消息成功投递到 MQ 组件中,它就一定被下游 consumer 端消费端,至少不会发生消息丢失的问题.

倘若我们需要执行一个分布式事务,事务流程中包含需要在服务 A 中执行的动作 I 以及需要在服务 B 中执行的动作 II,此时我们可以基于如下思路串联流程:

  • 以服务 A 作为 MQ 生产方 producer,服务 B 作为 MQ 消费方 consumer
  • 服务 A 首先在执行动作 I,执行成功后往 MQ 中投递消息,驱动服务 B 执行动作 II
  • 服务 B 消费到消息后,完成动作 II 的执行

对上述流程进行总结,其具备如下优势:

  • 服务 A 和服务 B 通过 MQ 组件实现异步解耦,从而提高系统处理整个事务流程的吞吐量
  • 当服务 A 执行 动作 I 失败后,可以选择不投递消息从而熔断流程,保证不会出现动作 II 执行成功,而动作 I 执行失败的不一致的问题
  • 基于 MQ at least once 的语义,服务 A 只要成功消息的投递,就可以相信服务 B 一定能消费到该消息,至少服务 B 能感知到动作 II 需要执行的这一项情报
  • 依赖于 MQ 消费侧的 ack 机制,可以实现服务 B 有限轮次的重试能力. 即当服务 B 执行动作 II 失败后,可以给予 MQ bad ack,从而通过消息重发的机制实现动作 II 的重试,提高动作 II 的执行的成功率

与之相对的,上述流程也具备如下几项局限性:

  • 问题 1:服务 B 消费到消息执行动作 II 可能发生失败,即便依赖于 MQ 重试也无法保证动作一定能执行成功,此时缺乏令服务 A 回滚动作 I 的机制. 因此很可能出现动作 I 执行成功,而动作 II 执行失败的不一致问题
  • 问题 2:在这个流程中,服务 A 需要执行的操作有两步:(1)执行动作 I;(2)投递消息. 这两个步骤本质上也无法保证原子性,即可能出现服务 A 执行动作 I 成功,而投递消息失败的问题.

在这里插入图片描述

本地事务+消息投递

上面的小节中,聊到的服务 A 所要执行的操作分为两步:本地事务+消息投递. 这里我们需要如何保证这两个步骤的执行能够步调统一呢,下面不妨一起来推演一下我们的流程设计思路:

首先,这两个步骤在流程中一定会存在一个执行的先后顺序,我们首先来思考看看不同的组织顺序可能会分别衍生出怎样的问题:

组合 I:先执行本地事务,后执行消息投递

在这里插入图片描述
组合 I 的优势:

  • 消息投递成功与本地事务一致:当使用组合 I 策略时,可以确保消息的投递与本地事务的执行是一致的。这意味着只有在本地事务执行成功时,消息才会被投递。这可以防止消息投递成功但本地事务失败的情况
  • 熔断机制:如果本地事务执行失败,您可以主动停止或熔断消息的投递动作。这可以防止错误的消息被发送,降低了系统可能面临的问题。

组合 I 的劣势:

  • 消息投递失败可能导致消息丢失:虽然组合 I 确保了消息投递与本地事务的一致性,但在某些情况下,消息投递可能会失败。例如,即使本地事务成功,但消息投递由于网络或其他问题而失败,导致消息丢失。此时,由于本地事务已经提交,要执行回滚操作会非常复杂和昂贵。

组合 II:先执行消息投递,后执行本地事务

在这里插入图片描述
组合 II 的优势:

  • 避免不必要的本地事务:如果消息投递失败,您可以避免执行不必要的本地事务。这可以提高系统的效率,因为不会浪费资源在本地事务上,除非消息可以被成功投递。

组合 II 的劣势:

  • 消息投递成功可能导致问题:尽管组合 II 确保了本地事务与消息投递的一致性,但在某些情况下,消息投递成功可能导致问题。例如,如果消息成功发送后,本地事务一直无法成功执行,那么可能会出现数据不一致或其他问题。

对上面对流程进行梳理总结实现思路是:基于本地事务包裹消息投递操作的实现方式,对应执行步骤如下:

  • 首先 begin transaction,开启本地事务
  • 在事务中,执行本地状态数据的更新
  • 完成数据更新后,不立即 commit transaction
  • 执行消息投递操作
  • 倘若消息投递成功,则 commit transaction
  • 倘若消息投递失败,则 rollback transaction

在这里插入图片描述
这个流程乍一看没啥毛病,重复利用了本地事务回滚的能力,解决了本地修改操作成功、消息投递失败后本地数据修正成本高的问题.

然而,这仅仅是表现. 上述流程实际上是经不住推敲的,其中存在三个致命问题:

  • 本地事务中夹杂第三方组件的IO操作:在本地事务中执行与第三方组件的IO操作可能引发长事务的风险。长事务可能会导致数据库锁定、性能问题和资源浪费。为了缓解这个问题,可以考虑将IO操作与数据库事务解耦,将其移到事务之外,或者采用异步处理方法。
  • 消息投递可能因超时或其他问题导致异常:当消息在实际上已成功投递但生产者未能获得投递响应时,可能会导致本地事务被误回滚的问题。为了避免这种情况,可以实现幂等性操作,确保消息处理具有幂等性,以便在重试消息时不会引发问题。
  • 事务提交失败可能导致无法回滚消息:如果在执行事务提交操作时发生失败,数据库修改操作会回滚,但已经发送的MQ消息无法回收。这可能导致数据不一致性。为了处理这个问题,可以采用两阶段提交(2PC)或者分布式事务管理器来确保事务操作的一致性,包括数据库和消息的一致性。

事务消息原理TX Msg

我们以 RocketMQ 中 TX Msg 的实现方案为例展开介绍。首先抛出结论,TX Msg 能保证我们做到在本地事务执行成功的情况下,后置的投递消息操作能以接近百分之百的概率被发出. 其实现的核心流程为:

  • 生产方 producer 首先向 RocketMQ 生产一条半事务消息,此消息处于中间态,会暂存于 RocketMQ 不会被立即发出
  • producer 执行本地事务
  • 如果本地事务执行成功,producer 直接提交本地事务,并且向 RocketMQ 发出一条确认消息
  • 如果本地事务执行失败,producer 向 RocketMQ 发出一条回滚指令
  • 倘若 RocketMQ 接收到确认消息,则会执行消息的发送操作,供下游消费者 consumer 消费
  • 倘若 RocketMQ 接收到回滚指令,则会删除对应的半事务消息,不会执行实际的消息发送操作
  • 此外,在 RocketMQ 侧,针对半事务消息会有一个轮询任务,倘若半事务消息一直未收到来自 producer 侧的二次确认,则 RocketMQ 会持续主动询问 producer 侧本地事务的执行状态,从而引导半事务消息走向终态。

在这里插入图片描述
在 TX Msg 的实现流程中,能够保证 前面小节中谈及的各种 bad case 都能被很好地消化:

  • 倘若本地事务执行失败,则 producer 会向 RocketMQ 发出删除半事务消息的回滚指令,因此保证消息不会被发出
  • 倘若本地事务执行成功, 则 producer 会向 RocketMQ 发出事务成功的确认指令,因此消息能够被正常发出
  • 倘若 producer 端在发出第二轮的确认或回滚指令前发生意外状况,导致第二轮结果指令确实. 则 RocketMQ 会基于自身的轮询机制主动询问本地事务的执行状况,最终帮助半事务消息推进进度.

总结一下:

  • 保证本地事务成功后消息投递接近百分之百的概率:RocketMQ的TX Msg机制确保了在本地事务执行成功的情况下,消息会以接近百分之百的概率被成功发出。这是因为只有在本地事务成功后,才会向RocketMQ发送确认消息,从而触发消息的真正发送。

  • 事务性保障:RocketMQ的TX Msg允许生产者执行本地事务,确保了消息发送与事务的一致性。如果本地事务失败,消息不会被发送,从而维护了数据的一致性。

  • 回滚机制:如果本地事务执行失败,RocketMQ会接收到回滚指令,然后删除对应的半事务消息,而不会执行实际的消息发送操作。这意味着即使本地事务失败,不会导致消息被误发送,保持了数据的一致性。

  • 轮询任务:RocketMQ会定期轮询半事务消息的状态,如果长时间未收到本地事务的二次确认,RocketMQ会主动询问生产者本地事务的执行状态,确保半事务消息能够最终达到终态。

RocketMQ 中半事务消息轮询流程示意如下:

在这里插入图片描述

最后,我们再回过头把 RocketMQ TX Msg 的使用交互流程总结梳理如下:

在这里插入图片描述

事务消息局限性

现在我们就来总结梳理一下,TX Msg 中存在的几项局限性:

  • 流程高度抽象:TX Msg 把流程抽象成本地事务+投递消息两个步骤. 然而在实际业务场景中,分布式事务内包含的步骤数量可能很多,因此就需要把更多的内容更重的内容糅合在所谓的“本地事务”环节中,上游 producer 侧可能会存在比较大的压力
  • 不具备逆向回滚能力:倘若接收消息的下游 consumer 侧执行操作失败,此时至多只能依赖于 MQ 的重发机制通过重试动作的方式提高执行成功率,但是无法从根本上解决下游 consumer 操作失败后回滚上游 producer 的问题. 这一点正是 TX Msg 中存在的最大的局限性.

关于上面第二点,我们再展开谈几句. 我们知道,并非所有动作都能通过简单的重试机制加以解决.

打个比方,倘若下游是一个库存管理系统,而对应商品的库存在事实上已经被扣减为 0,此时无论重试多少次请求都是徒然之举,这就是一个客观意义上的失败动作.

而遵循正常的事务流程,后置操作失败时,我们应该连带前置操作一起执行回滚,然而这部分能力在 TX Msg 的主流程中并没有予以体现.

要实现这种事务的逆向回滚能力,就必然需要构筑打通一条由下游逆流而上回调上游的通道,这一点并不属于 TX Msg 探讨的范畴.

在这里插入图片描述

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

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

相关文章

基于ChatGPT的文本生成艺术框架—WordArt Designer

WordArt Designer是一个基于gpt-3.5 turbo的艺术字生成框架,包含四个关键模块:LLM引擎、SemTypo、Styltypo和TextTypo模块。由gpt-3.5 turbo驱动的LLM引擎可以解释用户输入,从而将抽象概念转化为具体的设计。 SemTypo模块使用语义概念优化字体设计&…

C++入门(1)—命名空间、缺省参数

目录 一、什么是C 1、C关键字(C98) 2、C兼容C 二、C程序预处理指令 三、命名空间 1、命名冲突 第一种: 第二种: 2、域作用限定符 3、实现命名空间 4、命名空间冲突 5、访问命名空间 6、命名空间“std” 四、输入输出 1、定义 2、自动识…

【Git企业开发】第七节.多人协作开发

文章目录 前言 一、多人协作开发 1.1 多人协作一 1.2 多人协作二 1.3 远程分支删除后,本地 git branch -a 依然能看到的解决办法 总结 前言 一、多人协作开发 1.1 多人协作一 目前,我们所完成的工作如下: 基本完成Git的所有本地库的相关操作&#xff0…

demo(二)eurekaribbon----服务注册、提供与消费

前一篇实现了服务注册中心的搭建,并提供服务注册到注册中心上。在之前的基础上,实现服务消费。 一、相关介绍 1、RestTemplate工具 2、LoadBalanced注解 二、ribbon示例: 先启动eureka-service注册中心,再将eureka-client修改…

第十九章总结

一.Java绘图类 1.Graphics类 Graphics类是所有图形上下文的抽象基类,它允许应用程序在组件以及闭屏图像上进行绘制。Graphics类封装了Java支持的基本绘图操作所需的状态信息,主要包括颜色、字体、画笔、文本、图像等。 2.Graphics2D类 Graphics2…

Android 10.0 framework层设置后台运行app进程最大数功能实现

1. 前言 在10.0的定制开发中,在系统中,对于后台运行的app过多的时候,会比较耗内存,导致系统运行有可能会卡顿,所以在系统优化的 过程中,会限制后台app进程运行的数量,来保证系统流畅不影响体验,所以需要分析下系统中关于限制app进程的相关源码来实现 功能 2.framewo…

过滤器模式 rust和java的实现

文章目录 过滤器模式实现 过滤器模式实现javarustjavarust rust代码仓库 过滤器模式 过滤器模式(Filter Pattern)或标准模式(Criteria Pattern)是一种设计模式,这种模式允许开发人员使用不同的标准来过滤一组对象&…

【miniQMT实盘量化3】获取历史行情数据

前言 上篇文章,介绍了如何与miniQMT建立连接,这篇开始,我们会深入探讨miniQMT的每个功能接口。首先,从获取历史数据开始。 迅投的官方文档目前已经更新,miniQMT对应原生API部分 接口汇总 与历史行情数据相关的接口&a…

2023.11.15 每日一题(AI自生成应用)【C++】【Python】【Java】【Go】 动态路径分析

目录 一、题目 二、解决方法 三、改进 一、题目 背景: 在一个城市中,有数个交通节点,每个节点间有双向道路相连。每条道路具有一个初始权重,代表通行该路段的成本(例如时间、费用等)。随着时间的变化&am…

PPT转PDF转换器:便捷的批量PPT转PDF转换软件

在数字化时代,文档转换已成为日常工作不可或缺的一环。特别是对于那些需要转发或发布演示文稿的人来说,如果希望共享给他人的PPT文件在演示过程中不被修改,那么将PPT文件转换为PDF格式已经成为一个常见的选择。大多数PDF阅读器程序都支持全屏…

debian 修改镜像源为阿里云【详细步骤】

文章目录 修改步骤第 1 步:安装 vim 软件第 2 步:备份源第 3 步:修改为阿里云镜像参考👉 背景:在 Docker 中安装了 jenkins 容器。查看系统,发现是 debian 11(bullseye)。 👉 目标:修改 debian bullseye 的镜像为阿里云镜像,加速软件安装。 修改步骤 第 1 步:…

深度学习+python+opencv实现动物识别 - 图像识别 计算机竞赛

文章目录 0 前言1 课题背景2 实现效果3 卷积神经网络3.1卷积层3.2 池化层3.3 激活函数:3.4 全连接层3.5 使用tensorflow中keras模块实现卷积神经网络 4 inception_v3网络5 最后 0 前言 🔥 优质竞赛项目系列,今天要分享的是 🚩 *…

Synchronized面试题

一:轻量锁和偏向锁的区别: (1)争夺轻量锁失败时,自旋尝试抢占锁 (2)轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁,线程不会主动释放偏向锁 二&…

浅尝:iOS的CoreGraphics和Flutter的Canvas

iOS的CoreGraphic 基本就是创建一个自定义的UIView&#xff0c;然后重写drawRect方法&#xff0c;在此方法里使用UIGraphicsGetCurrentContext()来绘制目标图形和样式 #import <UIKit/UIKit.h>interface MyGraphicView : UIView endimplementation MyGraphicView// Onl…

酷开系统 酷开科技,将家庭娱乐推向新高潮

在当今数字化时代&#xff0c;家庭娱乐已经成为人们日常生活中不可或缺的一部分。如果你厌倦了传统的家庭娱乐方式&#xff0c;想要一种全新的、充满惊喜的娱乐体验&#xff0c;那么&#xff0c;不妨进入到酷开科技的世界&#xff0c;作为智能电视行业领军企业&#xff0c;酷开…

理解 R-CNN:目标检测的一场革命

一、介绍 对象检测是一项基本的计算机视觉任务&#xff0c;涉及定位和识别图像或视频中的对象。多年来&#xff0c;人们开发了多种方法来应对这一挑战&#xff0c;但基于区域的卷积神经网络&#xff08;R-CNN&#xff09;的发展标志着目标检测领域的重大突破。R-CNN 及其后续变…

深度学习之基于Pytorch和OCR的识别文本检测系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介深度学习与OCRPyTorch在OCR中的应用文本检测系统的关键组成部分1. 图像预处理2. 深度学习模型3. 文本检测算法4. 后处理 二、功能三、系统四. 总结 一项目简…

后端接口性能优化分析-问题发现问题定义

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码&#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44d;三连支持&…

图解系列--密码

1.概念 _1.对称密码与公钥密码 对称密码是指在加密和解密时使用同一密钥的方式。 公钥密码则是指在加密和解密时使用不同密钥的方式。因此&#xff0c;公钥密码又称为非对称密码。 _2.混合密码系统 对称密码和公钥密码结合起来的密码方式 _3.散列值 散列值就是用单向散列函数计…

CSDN每日一题学习训练——Java版(二叉搜索树迭代器、二叉树中的最大路径和、按要求补齐数组)

版本说明 当前版本号[20231115]。 版本修改说明20231115初版 目录 文章目录 版本说明目录二叉搜索树迭代器题目解题思路代码思路参考代码 二叉树中的最大路径和题目解题思路代码思路参考代码 按要求补齐数组题目解题思路代码思路参考代码 二叉搜索树迭代器 题目 实现一个二…