栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

分布式事务原理及解决方案

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

分布式事务原理及解决方案

目录
  • 事务分类
  • 数据一致性定义
  • 事务与数据一致性的关系
  • 数据一致性实现理论支撑
    • ACID
    • CAP
      • C:强一致性
      • A:可用性
      • P :分区容错性
      • CAP理论权衡
      • CAP常见误区
    • BASE理论
    • 常见问题
      • ACID、CAP、BASE的关系
      • ACID和CAP中的C其实是一个东西吗
      • 分布式事务是否满足ACID
  • 本地事务解决方案
  • 分布式事务解决方案
    • db层(强一致性事务)
      • 2pc(2 phase commit)
      • 3pc(3 phase commit)
    • 应用层(最终一致性方案)
      • tcc(try confirm cancel)
      • 本地消息表(最常用)
      • 各种分布式事务解决方案适用场景
    • 参考资料


事务分类
  1. 1个服务使用1个数据源:本地事务
  2. 1个服务使用多个数据源:全局事务(一般用的很少,因为可以把一个服务拆成多个)
  3. 多个服务使用1个数据源:共享事务(不会这么使用,完全可以把多个服务合一)
  4. 多个服务使用多个数据源:分布式事务。一般采用1个服务使用1个数据源,外加服务间rpc调用实现。

一般1个服务只会使用1个数据源(一个库),很少会出现1对多或多对一的场景。

  • 1个服务对应多个数据源,此时也无法进行关联查询,不如直接拆库
  • 多个服务使用1个数据源,说明这些服务业务强相关,直接抽象为1个服务效果更佳。

数据一致性定义

一般说数据一致性,都指代「强一致性」,在单实例系统中,通过事务就可以实现,天然满足ACID属性。在分布式系统中,也可实现。但由于在分布式系统中,一味追求实现CAP中的强一致性会出现问题,所以经过BASE理论权衡妥协后,产生了「弱一致性」和「最终一致性」的定义。

强一致性:数据更新成功后,「任意时刻」「所有相同请求」得到的数据都是一致的,一般采用同步的方式实现。

弱一致性:数据更新成功后,系统不承诺立即可以读到最新写入的值,也不承诺具体多久之后可以读到。

最终一致性:弱一致性的一种形式,数据更新成功后,系统不承诺立即可以返回最新写入的值,但是保证最终会返回上一次更新操作的值。

事务与数据一致性的关系

事务存在的最终目的是保证数据的状态是「强一致」的。但实际上,强一致很难达到,因此在进行权衡让步后,一般会退而求其次选择实现「弱一致」。

实现强一致性: 事务是实现数据强一致性最常见的手段。

实现弱一致性: 对于弱一致性和最终一致性,并不是使用事务实现,而是使用其他方案。

数据一致性实现理论支撑 ACID

ACID为事务必须满足的四个基本要素,一般用于指导「数据库事务」设计。一般A、I、D为手段,C为目的。

  • Atomicity(原子性):事务是一个不可分割的最小工作单位,一个事务中的所有操作,要么全部成功,要么全部回滚,

  • Consistency(强一致性):即所有操作是符合现实当中的期望的。

    例如,A有300,B有200,A给B转账,此时A和B的转账是符合期望的,即A有200,B有300.

  • Isolation(隔离性):多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。

  • Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

CAP

一个分布式系统不可能同时满足 C, A, P ,只能满足其中两个

C:强一致性

论文中的定义

引用Seth Gilbert and Nancy Lynch的论文 中的定义

There must exist a total order on all operations such that each operation loks as if it were completed at a single instant. This is equivalent to requiring requests of the distributed shared memory to act as if they were executing on a single node, responding to operations one at a time.

所有的操作拥有一个顺序,这个顺序可以看作是在一个单机上完成的。对分布式内存的请求像处理单机请求一样,一次只响应一个操作请求。

理解一下,这段定义主要说的是

  1. 分布式系统的数据操作效果要和单节点系统效果一样
  2. 一次只执行一个操作更像是一个单线程操作
  3. 关注点在操作的执行顺序

网上定义阐述

看了很多CAP的文章,为什么网上对C的定义这么繁杂呢?

可以看到,前面论文中定义要求所有的操作拥有一个顺序,这个顺序可以看作是在一个单机上完成的,这句话只具有理论意义,但不具有工程意义。在工程中,什么情况下才算是满足这种要求呢?

实际上,在分布式系统中,可以通过分布式系统的多次读写来验证是否满足此定义。结合读写验证结果,网上演化出了很多对C的不同定义,如此处引用

all clients see the same data at the same time

所有客户端在同一时间(同类请求)看到的数据完全一致

由论文中的定义可以推导出此定义,即当一个分布式系统的所有操作都类似于在一个单线程上完成时,那么所有读操作是顺序执行的,只要中间不执行其他写操作,客户端看到的数据必定是完全一致的。

结论

满足一致性:「所有客户端」在「同一时间」read 的数据完全一致

不满足一致性:「有客户端」在「同一时间」read 的数据不一致

A:可用性

论文中的定义

For a distributed system to be continuously available, every request received by a non-failing node in the system must result in a response.

分布式系统可用,系统中非故障节点接收到的每个请求(无论是读请求还是写请求)都必须得到响应(给了响应,响应的对不对不重要)。

什么是非故障节点?

  1. 情况一:node级别部分崩溃,崩溃的node认为是故障节点,不参与可用性评估。

    如果节点不能正常接收request了,比如宕机了,系统崩溃了,而其他节点依然能正常接收请求,那么系统依然是可用的,也就是说,部分宕机没事儿,不影响可用性指标。

  2. 情况二:node内部数据异常,这部分node还认为是正常非故障节点,参与可用性评估

    如果节点能正常接收request,但是发现节点内部数据有问题,那么也必须返回结果,哪怕返回的结果是有问题的。

什么算是响应成功?

  1. 一定要返回一个值,这个值是否合理不在可用性讨论范围内。

    比如,系统有两个节点,其中有一个节点数据版本是v0,另一个节点版本是v1,如果,一个read请求跑到了v0版本的节点上,抱歉,这个节点也不能拒绝,必须返回这个v0数据,即使它可能不太合理。

  2. 响应成功一般认为还应满足时延要求。(此条在定理中没有单独做要求)

    返回结果应在合理的时间以内,这个合理的时间是根据业务来定的。比如,业务说必须 100 毫秒内返回,合理的时间就是 100 毫秒,需要 1 秒内返回,那就是 1 秒,如果业务定的 100 毫秒,结果却在 1 秒才返回,那么这个系统就不满足可用性

结论

满足可用性:所有的非故障node都给出了响应

不满足可用性:有非故障node没有给出响应

P :分区容错性

论文中的定义

In order to model partion tolerance, the network will be allowed to lose arbitrarily many messages sent from one node to another.

为了做到分区容错,网络要允许丢失任意从某个结点到另一结点的数据。(系统依然运行)

分区容错发生的两种可能

  1. network故障导致的分区。

    特点: A、B 依然都在运行,都在对外提供服务

    我们的分布式存储系统有 A、B 两个节点。那么,当 A、B 之间由于可能路由器、交换机等底层网络设备出现了故障,A 和 B 通信出现了问题,这时候,就说 A 和 B 发生了分区。

  2. node故障导致的分区

    特点: 只有B在对外提供服务

    我们的分布式存储系统有 A、B 两个节点。当 A 出现了宕机,A 和 B 节点之间通信也是出现了问题,那么我们也称 A 和 B 发生了分区。

什么是系统运行

当一个系统,哪怕只有一个node,仍能够处理各类请求的时候,就算是系统运行。只不过此时,不算是高可用。

结论

满足分区容错性:出现了分区问题,我们的分布式存储系统还需要继续运行。

不满足分区容错性:出现了分区问题后,分布式存储系统不能正常运行。

CAP理论权衡

在分布式系统内,P 是天然需要满足的,因为必须满足出现了分区问题,我们的分布式存储系统还需要继续运行,如果不满足P,一旦发生分区错误,整个分布式系统就会完全崩溃。

所以开源的分布式系统往往又被分为 CP 系统和 AP 系统

**CP 系统:**当一套系统在发生分区故障后,「客户端的任何请求都被卡死或者超时」,但是,系统的每个节点总是会返回一致的数据,经典的比如 Zookeeper。

**AP系统:**如果一套系统发生分区故障后,客户端依然可以访问系统,「但是获取的数据有的是新的数据,有的还是老数据」,经典的比如 Eureka。

可以看到,牺牲A常见的方式就是阻塞错误请求,牺牲C只需要不做数据校验同步即可。

CAP 就是告诉程序员们当分布式系统出现内部问题了,你要做两种选择:

  • 要么让外部服务迁就你,像银行。( CP 系统,Zookeeper)
  • 要么迁就外部服务,像外包公司。(AP系统,Eureka)
CAP常见误区

误解一:分布式系统必须放弃 C 或者 A 中的其中一个

当没有出现分区问题的时候,系统就应该有完美的数据一致性和可用性。只有在出现分区问题时,才需进行权衡。

(待补充)

BASE理论

即使无法做到强一致性,但每个应用都可以根据自身业务特点采用适当的方式来使系统达到最终一致性

  1. Basically Available(基本可用):如何牺牲A

    当分布式系统出现故障时,允许损失部分可用性,保证核心模块的可用性。这个很好理解,当出现故障时,首先考虑的肯定是核心模块,但也不是说要牺牲掉其他模块,只是在极端情况下对服务做的降级。

  2. Soft state(软状态):如何牺牲C中的原子性

    软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。例如mysql的主从同步就是存在延时的,只要上游可以接受这种延时,就没有问题。

  3. Eventually consistent(最终一致性):如何牺牲C中的强一致性

    系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。针对不同的业务,数据达到一致状态的容忍时间也是不同的。我注册之后,可以几秒钟就需要登陆,需要数据一致,但是发表一篇文章,几分钟之后达到一致性也不会有影响。

常见问题 ACID、CAP、BASE的关系
  1. ACID是「事务」的基本要素,事务是实现C的手段,但不是唯一手段。
  2. CAP是一种C与其他性质的权衡「定理」,CAP 是学术理论,得到过证明。
  3. BASE是由于CAP定理不具备工程特性,进而基于CAP理论演化出的一种「工程权衡方案」

ACID告诉我们,要实现「强一致性」,就必须实现A、I、D。这套理论在单实例系统中应用起来没问题,但在分布式系统中,会出现其他问题。问题的来源并不是在分布式系统中无法保证「强一致性」,如果能够在分布式系统中实现A、I、D一样能够实现「强一致性」。只是如果一味地强调实现「强一致性」,那么这个系统一定会有「可用性」或「分区容错性」问题。

因而,在分布式系统中,不能一味地追求「强一致性」,必须进行一定的权衡妥协。而CAP定理出于数学证明角度考虑,未考虑实际工程可实现性,进而演化出了BASE理论,用于指导实际工程中的C、A、P之间的权衡。

ACID和CAP中的C其实是一个东西吗

在了解ACID和CAP定理关系之前,我们需要先讨论一下ACID和CAP中的C其实是一个东西吗。

网上很多文章提到ACID中的C和CAP理论中的C不是一种东西,这里我们看一下二者的定义和理解。首先ACID是对事务基本要素的描述

ACID中C,引用wiki的定义

一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。

CAP中的C,引用Seth Gilbert and Nancy Lynch的论文 中的解释

Discussing atomic consistency is somewhat different than talking about an ACID database, as database consistency refers to transactions, while atomic consistency refers only to a property of a single request/response operation sequence. And it has a different meaning than the Atomic in ACID, as it subsumes the database notions of both Atomic and Consistent

CAP中的C和ACID中的C是不同的,ACID中的C涉及事务处理,但CAP中的C只讨论一系列读写操作顺序。同时原子一致性与ACID中的A也是不同的,CAP中的C更像是ACID中A和C的融合概念

由充分必要性可以看出,看到CAP中的C其实是ACID中A和C的融合,换而言之,当满足CAP中的C时,天然满足ACID中的C和A。

另外,其实上述都是在描述一件事,即「系统中数据的合理性」,ACID中的C是从系统内部数据的角度讨论合理性,CAP中的C是从数据执行顺序角度讨论合理性。在实际理解过程中,多数情况下没有必要过于锱铢必较,可以认为两者“一致”

分布式事务是否满足ACID

实现「强一致性」的事务,无论是否是分布式事务,一定会满足ACID。但分布式系统中,一般把实现弱一致性的方案也叫做分布式事务,这类事务是不满足ACID要求的。

本地事务解决方案

一般本地事务可以直接利用db的事务能力解决。在单实例场景下,实现数据强一致性不会引入其他问题,所以一般直接基于db能力实现强一致性

  • A是通过 undo log 来实现的
  • D是通过 redo log 来实现的
  • I是通过 (读写锁+MVCC)来实现的
  • C是通过原以上三种属性来实现的

分布式事务解决方案

「强一致性事务」实现方案一般都集中在「数据库层」,在业务层面实现的分布式事务方案,由于数据已经提交过,再通过补偿等方式回滚,很有可能会使业务读到脏数据,所以「业务层」实现的一般都是「弱一致性」。

所有分布式事务的解决方案都是将分布式事务解耦,将一个分布式事务,解锁为多个本地事务,通过引入「事务协调器」将多个本地事务进行重新组合

db层(强一致性事务) 2pc(2 phase commit)

方案

详细2PC方案可参见:https://zhuanlan.zhihu.com/p/91263461

2PC引入一个「事务管理器(TM:Transaction Manager)」的角色来协调管理「事务参与者(Resource Manager)」的提交和回滚,二阶段分别指的是prepare和commit两个阶段。

(1)准备阶段:TM给每个RM发送Prepare消息,每个RM在本地执行事务,并写Undo/Redo log,此时本地事务没有commit。

(2)提交阶段:TM收到每个RM的执行success或者failed消息时,给RM发送回滚(Rollback)或提交(Commit)消息。RM根据Undo/Redo log执行Rollback操作,或直接commit。

原理

通过引入TM将分布式事务拆分为多个本地事务,然后通过本地事务的redo log/undo log来实现回滚等能力。

问题

  1. 性能问题:在执行分布式事务过程中,全程TM和RM的资源都被加锁,直到结束才释放,对性能影响较大。

  2. 死锁问题:由于prepare执行完成后,RM都处于阻塞状态,一旦TM发生故障,所有的RM都会死锁。

  3. 单点故障问题:

    1) TM正常,RM宕机

    此时,其他RM和TM不知道故障RM的情况,不知道是否进行到下一阶段。

    解决方案: TM引入超时机制,如果TM在超过指定的时间还没有收到参与者的反馈,事务就失败,向所有节点发送终止事务请求。

    2) TM宕机,RM正常

    TM相当于是个裁判,无论哪个阶段TM宕机,都没人发送commit、rollback等指令,事务都没法继续下去。

    解决方案: 引入TM备份,同时TM需记录操作日志.当检测到TM宕机一段时间后,TM备份取代TM,并读取操作日志,向所有RM询问状态。

    3) TM和RM都宕机

    1. 发生在第一阶段: 因为第一阶段,所有RM都没有真正执行commit,所以只需重新在剩余的RM中重新选出一个TM,新的TM在重新执行第一阶段和第二阶段就可以了。
    2. 发生在第二阶段 并且 有部分RM已经执行完commit操作。就好比这里订单服务A和支付服务B都收到TM 发送的commit信息,开始真正执行本地事务commit,但突发情况,A commit成功,B挂了。这个时候目前来讲数据是不一致的。2PC 无法解决这个问题。
3pc(3 phase commit)

方案

详细2PC方案可参见:https://zhuanlan.zhihu.com/p/91263461

3pc相较于2pc有两个改动点。

  1. 2pc给TM引入超时,当RM没有反馈时,可以主动发起rollback,3pc给RM也加入了超时,避免当协TM崩溃时,RM不能及时释放锁,造成业务阻塞。

    避免了RM在长时间无法与协调者节点通讯(TM挂掉了)的情况下,无法释放资源的问题,因为RM自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。

  2. 3PC把2PC的prepare阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

    通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各RM节点的状态是一致的

应用层(最终一致性方案) tcc(try confirm cancel)

方案

详细方案可参见:http://t.zoukankan.com/clarino-p-13281843.html

TCC 指的是Try - Confirm - Cancel,tcc方案一般会定义一个事务管理者,来执行confirm或cancel。事务管理者实现逻辑比较复杂,一般选用现有的框架,如开源的框架主要有:ByteTCC,tcc-transaction,himly。

  • Try 指的是预留,即资源的预留和锁定,注意是预留不是直接使用。
  • Confirm 指的是确认操作,这一步其实就是真正的执行了。
  • Cancel 指的是撤销操作,可以理解为把「预留阶段」的动作撤销了。

Try 阶段:主要用于锁定资源,预操作。如库存提前冻结。

比如用户下单的操作,需要扣减库存,同时增加用户积分

  • 某产品现有库存有100,此订单购买2,可以保持原有库存不变,在一个单独的冻结库存字段里,设置一个2,表示库存冻结2。如果有其他扣减库存的线程,可以通过使用现有库存-冻结库存进行库存校验,但查询实际库存时,使用现有库存,类似设置一个缓存字段
  • 某用户现有积分为100,下单赠送10,可以保持原有积分不变,在一个单独的冻结积分字段里,设置一个10,表示有10个积分准备增加。

Confirm/Cancel阶段:针对锁定资源进行确认/取消操作(清空缓存)

  • Confirm:缓存清空,数据合并到现有xx数据字段中
  • Cancel:清空缓存

原理

TCC 是一种补偿性事务思想,通过定义错误补偿方案,来实现事务回滚。

TCC与2PC本质上是一类方案,只不过2PC方案在db层实现,利用了db本地事务的undo/redo log能力实现数据回滚,而TCC方案在应用层实现,只能通过业务代偿实现回滚,而应用层的代偿全都需要手动自定义。

本地消息表(最常用)

这种方案会有一张「本地消息表」再结合「MQ」实现分布式事务。

「本地消息表」用于pub是否向MQ发消息成功,消息表中状态有0-未发送,1-已发送。

「MQ」负责异步存储sub待执行消息。

方案

以pub订单创建、sub扣除库存为例。(实现代码可见下方)

  1. pub方:插入订单记录时,同步(在同一事务中)保存一条消息到pub的本地消息表(orderMapper)中,状态字段变为未0-发送。此步失败直接抛异常

  2. pub方:在第1步执行成功的基础上,立即尝试在与1同一线程中将消发送到 MQ,尽量确保时效性。

    发送成功:调用confirm回调,修改本地消息表(orderMapper)中的状态字段为1-已发送

    发送失败:跳过,问题不大,此线程不再处理,后面有定时线程刷新

  3. 设置一条定时补偿任务,轮询扫描订单的本地消息表,找出所有未发送的记录,进行消息发送。

    ----------至此已可保证消息必进MQ,pub的本地消息表(orderMapper)已用完----------

  4. sub方:处理stock扣减库存的操作,并同步保存一条信息到sub的本地消息表(stockMapper)中,对于操作失败的消息重新放回MQ,由其他消费者处理即可,不抛异常

    必须采用手动ack机制+重试机制,业务处理失败可以将消息重新放回MQ

    接收到消息后需进行幂等性判断,避免重试过程中,多次执行业务操作

orderMapper中保存待发送到MQ的信息,用于消息发送重试

stockMapper中保存已处理信息,用于幂等性验证

异常分析

  1. 如果第1步失败,订单记录插入失败,但是本地消息表中无内容,可以认为订单创建失败
  2. 如果第2步失败,第2步存在的意义在于在订单记录插入后,尽快把MQ发出去,但是有可能会失败,但是失败问题也不大,后面会有定时线程刷新尝试重新发送
  3. 如果第3步失败,定时线程再次发送失败,过一段时间后会重试。此时可以记录发送次数。可以定期人工查看多次发送的数据,进行手工处理。
  4. 如果第4步失败,操作失败的消息重新放回MQ,供其他sub消费

原理

将分布式事务解耦,拆分成本地事务进行处理。

代码分析

1. pub订单创建

@Slf4j
@Service
public class OrderServiceImpl {

    @Resource
    private ClientStock clientStock;

    // pub订单本地信息表
    @Resource
    private OrdersMapper ordersMapper;

    @Resource
    private OrdersMessageMapper ordersMessageMapper;

    @Resource
    private RabbitTemplate rabbitTemplate;

    
    @Transactional(rollbackFor = Exception.class)
    public boolean createOrder(String orderId) {
        // 添加订单记录
        Orders orders = new Orders();
        orders.setName("蚂蚁课堂");
        orders.setOrderCreatetime(new Date());
        orders.setOrderState(0);
        orders.setOrderMoney(10.0);
        orders.setOrderId(orderId);
        int flage = ordersMapper.insert(orders);

        OrdersMessage ordersMessage = null;
        if (flage == 1) {
            // 订单记录插入成功
            // 本地消息表,添加待加入MQ消息体
            Map map = new HashMap<>();
            map.put("orderId", orderId); 

            ordersMessage = new OrdersMessage();
            ordersMessage.setId(0);
            ordersMessage.setMessageBody(MapperUtils.ObjectToJson(map));
            ordersMessage.setStatus(0);  // 状态为0表示,消息未发送
            ordersMessage.setCreateTime(new Date());
            flage = ordersMessageMapper.insert(ordersMessage);

            if (flage != 1) {
              	// 本地消息表添加失败,直接异常
                throw new RuntimeException("回滚事务!");
            }
        }

        if(flage == 1 && ordersMessage != null){
            // 本地消息表添加成功,且添加订单记录成功
            // 注册回调函数
            this.rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
                @Override
                public void confirm(CorrelationData correlationData, boolean ack, String s) {
                    log.info("消息是否发送成功--->" + ack);
                    if (ack) {
                      	// 消息发送成功,更改消息表的状态为已发送
                        log.info("消息发送成功,更改消息表中的状态....");
                        String id = correlationData.getId();
                        OrdersMessage ordersMessage = new OrdersMessage();
                        ordersMessage.setId(Integer.valueOf(id));
                        ordersMessage.setStatus(1);
                        ordersMessageMapper.updateByPrimaryKeySelective(ordersMessage);
                    }
                }
            });
            this.rabbitTemplate.setMandatory(true);

           // 向MQ中发送本地消息表中内容
    this.rabbitTemplate.convertAndSend("stockExchange","direct.key",ordersMessage.getMessageBody(),
                    new CorrelationData(ordersMessage.getId() + ""));

            // 消息发送成功
            return true;
        }

      	// 消费发送失败,不采用
        return false;
    }
  
}  

2. 后台定时任务,扫描本地消息表

@Slf4j
@Component
public class MqSchedule{

    @Resource
    private OrdersMessageMapper ordersMessageMapper;

    @Resource
    private RabbitTemplate rabbitTemplate;

    @Scheduled(cron = "0/2 * * * * ?")
    public void scheduleScanLocalMessage() {

        log.info("扫描消息表开始...."+System.currentTimeMillis());

        // 查询所有等待发送的消息
        Example example = new Example(OrdersMessage.class);
        example.createCriteria().andEqualTo("status", 0);
        List ordersMessages = ordersMessageMapper.selectByExample(example);
      
        // 对结果轮询发送
        ordersMessages.forEach(item -> {
            log.info("开始发送本地消息表里面的数据.........");
          
            // 注册回调函数(同pub订单创建中的回调函数)
            this.rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
                @Override
                public void confirm(CorrelationData correlationData, boolean ack, String s) {
                    log.info("消息是否发送成功--->" + ack);
                    // 消息发送成功了
                    if (ack) {
                        log.info("消息发送成功,更改消息表中的状态....");
                        // 更改消息表的状态为已发送
                        String id = correlationData.getId();
                        OrdersMessage ordersMessage = new OrdersMessage();
                        ordersMessage.setId(Integer.valueOf(id));
                        ordersMessage.setStatus(1);
                        ordersMessageMapper.updateByPrimaryKeySelective(ordersMessage);
                    }
                }
            });
            this.rabbitTemplate.setMandatory(true);

           // 向MQ中发送本地消息表中内容
            this.rabbitTemplate.convertAndSend("stockExchange","direct.key",item.getMessageBody(),
                    new CorrelationData(item.getId() + ""));

        });

    }

}

3. sub库存消费MQ中内容

@Component
@Slf4j
public class ListenerMessage {
  // sub库存本地信息表
  @Resource
  private StockMapper stockMapper;

  @RabbitListener(bindings = {
    @QueueBinding(
      exchange = @Exchange(value = "stockExchange"),
      value = @Queue(value = "stockQueue"),
      key = "direct.key"
    )
  })
  @Transactional(rollbackFor = Exception.class)
  public void listenerStock(@Payload String messageBody, @Headers Map headers, Channel channel)
    throws IOException {

    log.info("接收到消息,进行手动签收...接收到的消息内容为----->" + messageBody);
    Map map = MapperUtils.JsonToObject(messageBody, Map.class);
    String orderId = map.get("orderId").toString();

    // 幂等性验证
    Example example = new Example(Stock.class);
    example.createCriteria()
      .andEqualTo("orderId", orderId);

    // 检测幂等性,如果stockMapper中已有此记录,表明该记录已处理,直接把MQ信息干掉
    Long deliveryTag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
    if (stockMapper.selectCountByExample(example) > 0) {
      log.info("重复消费触发了....");
      try {
        channel.basicAck(deliveryTag, false);
      } catch (Exception e) {
        try {
          channel.basicNack(deliveryTag, false, false);
        } catch (IOException ex) {
          ex.printStackTrace();
        }
      }
      return;
    }

    // 新增一条减库存记录
    Stock stock = new Stock();
    stock.setOrderId(orderId);
    stock.setStock(100);
    int flag = stockMapper.insert(stock);

    // 插入成功,返回MQ已消费信息
    if (flag == 1) {
      log.info("新增一条减库存记录成功.....分布式事务完成....");
      try {
        channel.basicAck(deliveryTag, false);
      } catch (Exception e) {
        try {
          channel.basicNack(deliveryTag, false, false);
        } catch (IOException ex) {
          ex.printStackTrace();
        }
      }
    }

  }

}
各种分布式事务解决方案适用场景
  1. 相比数据库层的2PC和3PC方案,业务层方案可以「跨数据库、跨不同的业务系统」来实现事务
  2. TCC基于业务补偿操作,但是业务层开发量相较本地消息表方案更大,对业务的侵入较大,业务耦合性较高
  3. 本地消息表结合MQ的方式,要引入MQ,需要解决MQ的可靠性问题。
  4. 业务层方案会涉及大量重试操作,要实现接口的幂等性



参考资料

https://zhuanlan.zhihu.com/p/386324224

https://users.ece.cmu.edu/~adrian/731-sp04/readings/GL-cap.pdf

https://www.zhihu.com/search?type=content&q=cap%E7%90%86%E8%AE%BA%E6%80%8E%E4%B9%88%E7%90%86%E8%A7%A3

https://www.zhihu.com/search?q=%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88&utm_content=search_history&type=content

https://www.jianshu.com/p/3b7da1ebe03c

https://zhuanlan.zhihu.com/p/91263461

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1006068.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号