本地事务、分布式事务以及解决方案

1、本地事务

1.1、事务的基本性质

数据库事务的几个特性:原子性(Atomicity )、一致性( Consistency )、隔离性或独立性( Isolation) 和持久性(Durabilily),简称就是 ACID;

  • 原子性:一系列的操作整体不可拆分,要么同时成功,要么同时失败
  • 一致性:数据在事务的前后,业务整体一致。例如:转账。A:1000;B:1000; 转 200 事务成功; A:800 B:1200
  • 隔离性:事务之间互相隔离。
  • 持久性:一旦事务成功,数据一定会落盘在数据库。

在以往的单体应用中,我们多个业务操作使用同一条连接操作不同的数据表,一旦有异常, 我们可以很容易的整体回滚;
本地事务、分布式事务以及解决方案_第1张图片

  • Business:我们具体的业务代码
  • Storage:库存业务代码;扣库存
  • Order:订单业务代码;保存订单
  • Account:账号业务代码;减账户余额

例如,买东西业务,扣库存,下订单,账户扣款,是一个整体;必须同时成功或者失败 一个事务开始,代表以下的所有操作都在同一个连接里面;

1.2、事务的隔离级别

  • READ UNCOMMITTED(读未提交):该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。
  • READ COMMITTED(读提交): 一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重 复读问题,Oracle 和 SQL Server 的默认隔离级别。
  • REPEATABLE READ(可重复读) :该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间 点的状态,因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。MySQL 的 InnoDB 引擎可以通过 next-key locks 机制(参考下文"行锁的算法"一节)来避免幻读。
  • SERIALIZABLE(序列化): 在该隔离级别下事务都是串行顺序执行的,MySQL 数据库的 InnoDB 引擎会给读操作隐式 加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。

1.3、事务的传播行为

  1. PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务, 就加入该事务,该设置是最常用的设置。
  2. PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当 前不存在事务,就以非事务执行。
  3. PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果 当前不存在事务,就抛出异常。
  4. PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
  5. PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当 前事务挂起。
  6. PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
  7. PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务, 则执行与 PROPAGATION_REQUIRED 类似的操作。

1.4、springboot本地事务设置失效的问题

1.4.1、失效的原因

在SpringBoot项目的同一个类里面,编写两个方法,内部调用的时候,会导致事务设置失效。原因是没有用到代理对象的缘故。

本地事务、分布式事务以及解决方案_第2张图片

1.4.2、解决方案

  1. 导入 spring-boot-starter-aop
  2. @EnableTransactionManagement(proxyTargetClass = true)
  3. @EnableAspectJAutoProxy(exposeProxy=true)
  4. AopContext.currentProxy() 调用方法

本地事务、分布式事务以及解决方案_第3张图片

本地事务、分布式事务以及解决方案_第4张图片

2、分布式事务出现的原因

2.1、为什么有分布式事务

分布式系统经常出现的异常 机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的 TCP、存储数据丢失…
本地事务、分布式事务以及解决方案_第5张图片

2.2、CAP 定理与 BASE 理论

2.2.1、CAP 定理

CAP 原则又称 CAP 定理,指的是在一个分布式系统中一致性(Consistency)、 可用性(Availability)、容错性(Partition tolerance)

  • 一致性(Consistency):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访 问同一份最新的数据副本)
  • 可用性(Availability): 在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据 更新具备高可用性)
  • 分区容错性(Partition tolerance):大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。 分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务 器放在美国,这就是两个区,它们之间可能无法通信。

CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

本地事务、分布式事务以及解决方案_第6张图片
一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们, 剩下的 C 和 A 无法同时做到。

2.2.2、BASE 理论

是对 CAP 理论的延伸,思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但可 以采用适当的采取弱一致性,即最终一致性。

BASE 是指 基本可用(Basically Available) 、软状态( Soft State)、最终一致性( Eventual Consistency)

  • 基本可用(Basically Available):基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、 功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用。 例如

    1. 响应时间上的损失:正常情况下搜索引擎需要在 0.5 秒之内返回给用户相应的 查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了 1~2 秒。
    2. 功能上的损失:购物网站在购物高峰(如双十一)时,为了保护系统的稳定性, 部分消费者可能会被引导到一个降级页面
  • 软状态( Soft State):软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体 现。mysql replication 的异步复制也是一种体现。

  • 最终一致性( Eventual Consistency) :最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状 态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

2.2.3、强一致性、弱一致性、最终一致性

从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。如果能容忍后续的部分或者全部访问不到,则是弱一致性。如果经过一段时间后要求能访问到更新后的数据,则是最终一致性

3、数据一致性协议

3.1、NWR协议

3.1.1、什么是NWR协议

NWR是一种在分布式存储系统中用于控制一致性级别的一种策略。在亚马逊的云存储系统中,就应用NWR来控制一致性。

  • N:在分布式存储系统中,有多少份备份数据
  • W:代表一次成功的更新操作要求至少有w份数据写入成功
  • R: 代表一次成功的读数据操作要求至少有R份数据成功读取

3.1.2、原理

NWR值的不同组合会产生不同的一致性效果,当W+R>N的时候,整个系统对于客户端来讲能保证强一致性。
以常见的N=3、W=2、R=2为例:

  • N=3,表示,任何一个对象都必须有三个副本
  • W=2表示,对数据的修改操作只需要在3个副本中的2个上面完成就返回
  • R=2表示,从三个对象中要读取到2个数据对象,才能返回

在分布式系统中,数据的单点是不允许存在的。即线上正常存在的备份数量N设置1的情况是非常危险的,因为一旦这个备份发生错误,就 >可能发生数据的永久性错误。假如我们把N设置成为2,那么,只要有一个存储节点发生损坏,就会有单点的存在。所以N必须大于2。
N越高,系统的维护和整体 成本就越高。工业界通常把N设置为3。

3.1.2.1、当W是2、R是2的时候,W+R>N,这种情况对于客户端就是强一致性的。

本地事务、分布式事务以及解决方案_第7张图片
在上图中,如果R+W>N,则读取操作和写入操作成功的数据一定会有交集(如图中的Node2),这样就可以保证一定能够读取到最新版本的更新数据,数据的强一致性得到了保证。在满足数据一致性协议的前提下,R或者W设置的越大,则系统延迟越大,因为这取决于最慢的那份备份数据的响应时间。思考:读取到两个数据,怎么去判断哪一个是最新的值?

3.1.2.2、当R+W<=N,无法保证数据的强一致性

本地事务、分布式事务以及解决方案_第8张图片
因为成功写和成功读集合可能不存在交集,这样读操作无法读取到最新的更新数值,也就无法保证数据的强一致性。

3.2、Gossip 协议

3.2.1、 什么是Gossip 协议

Gossip 协议也叫 Epidemic 协议 (流行病协议)。原本用于分布式数据库中节点同步数据使用,后被广泛用于数据库复制、信息扩散、集群成员身份确认、故障探测等。

从 gossip 单词就可以看到,其中文意思是八卦、流言等意思,我们可以想象下绯闻的传播(或者流行病的传播);gossip 协议的工作原理就类似于这个。gossip 协议利用一种随机的方式将信息传播到整个网络中,并在一定时间内使得系统内的所有节点数据一致。Gossip 其实是一种去中心化思路的分布式协议,解决状态在集群中的传播和状态一致性的保证两个问题。
本地事务、分布式事务以及解决方案_第9张图片

3.2.2、Gossip原理

Gossip 协议的消息传播方式有两种:反熵传播谣言传播

  1. 反熵传播
    是以固定的概率传播所有的数据。所有参与节点只有两种状态:Suspective(病原)、Infective(感染)。过程是种子节点会把所有的数据都跟其他节点共享,以便消除节点之间数据的任何不一致,它可以保证最终、完全的一致。缺点是消息数量非常庞大,且无限制;通常只用于新加入节点的数据初始化。
  2. 谣言传播
    是以固定的概率仅传播新到达的数据。所有参与节点有三种状态:Suspective(病原)、Infective(感染)、Removed(愈除)。过程是消息只包含最新 update,谣言消息在某个时间点之后会被标记为 removed,并且不再被传播。缺点是系统有一定的概率会不一致,通常用于节点间数据增量同步。

3.2.3、通信方式

Gossip 协议最终目的是将数据分发到网络中的每一个节点。根据不同的具体应用场景,网络中两个节点之间存在三种通信方式:推送模式、拉取模式、推/拉模式

  1. Push:节点 A 将数据 (key,value,version) 及对应的版本号推送给 B 节点,B 节点更新 A 中比自己新的数据
    本地事务、分布式事务以及解决方案_第10张图片
  2. Pull:A 仅将数据 key, version 推送给 B,B 将本地比 A 新的数据(Key, value, version)推送给 A,A 更新本地

本地事务、分布式事务以及解决方案_第11张图片

  1. Push/Pull:与 Pull 类似,只是多了一步,A 再将本地比 B 新的数据推送给 B,B 则更新本地

本地事务、分布式事务以及解决方案_第12张图片

本地事务、分布式事务以及解决方案_第13张图片

3.2.4、优缺点

综上所述,我们可以得出 Gossip 是一种去中心化的分布式协议,数据通过节点像病毒一样逐个传播。因为是指数级传播,整体传播速度非常快。

  1. 优点
  • 扩展性:允许节点的任意增加和减少,新增节点的状态 最终会与其他节点一致容错:任意节点的宕机和重启都不会影响 Gossip 消息的传播,具有天然的分布式系统容错特性
  • 去中心化:无需中心节点,所有节点都是对等的,任意节点无需知道整个网络状况,只要网络连通,任意节点可把消息散播到全网
  • 最终一致性:Gossip 协议实现信息指数级的快速传播,因此在有新信息需要传播时,消息可以快速地发送到全局节点,在有限的时间内能够做到所有节点都拥有最新的数据。
  1. 缺点
  • 消息延迟:节点随机向少数几个节点发送消息,消息最终是通过多个轮次的散播而到达全网;不可避免的造成消息延迟。
  • 消息冗余:节点定期随机选择周围节点发送消息,而收到消息的节点也会重复该步骤;不可避免的引起同一节点消息多次接收,增加消息处理压力

Gossip 协议由于以上的优缺点,所以适合于 AP 场景的数据一致性处理,常见应用有:P2P 网络通信、Redis Cluster、Consul。

3.3、Paxos协议

3.3.1、什么是Paxos

Paxos协议其实说的就是Paxos算法, Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。

自Paxos问世以来就持续垄断了分布式一致性算法,Paxos这个名词几乎等同于分布式一致性。Google的很多大型分布式系统都采用了Paxos算法来解决分布式一致性问题,如Chubby、Megastore以及Spanner等。开源的ZooKeeper,以及MySQL 5.7推出的用来取代传统的主从复制的MySQL Group Replication等纷纷采用Paxos算法解决分布式一致性问题。然而,Paxos的最大特点就是难,不仅难以理解,更难以实现。

3.3.2、Paxos 解决了什么问题

本地事务、分布式事务以及解决方案_第14张图片
在常见的分布式系统中,总会发生诸如机器宕机或网络异常(包括消息的延迟、丢失、重复、乱序,还有网络分区)等情况。Paxos算法需要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,并且保证不论发生以上任何异常,都不会破坏整个系统的一致性。

注:这里某个数据的值并不只是狭义上的某个数,它可以是一条日志,也可以是一条命令(command)。。。根据应用场景不同,某个数据的值有不同的含义。

在之前的2PC 和 3PC的时候在一定程度上是可以解决数据一致性问题的. 但是并没有完全解决就是协调者宕机的情况

本地事务、分布式事务以及解决方案_第15张图片

3.3.3、如何解决2PC和3PC的存在的问题呢?

  1. 步骤1-引入多个协调者

本地事务、分布式事务以及解决方案_第16张图片

  1. 步骤-引入主协调者,以他的命令为基准
    本地事务、分布式事务以及解决方案_第17张图片
    其实在引入多个协调者之后又引入主协调者.那么这个就是最简单的一种Paxos 算法

3.3.4、Paxos的版本

Paxos的版本有: Basic Paxos , Multi Paxos, Fast-Paxos, 具体落地有Raft 和zk的ZAB协议

本地事务、分布式事务以及解决方案_第18张图片

3.3.5、Basic Paxos

3.3.5.1、Basic Paxos相关概念
  1. 角色介绍
  • Client客户端:客户端向分布式系统发出请求,并等待响应。例如,对分布式文件服务器中文件的写请求。
  • Proposer提案发起者:提案者提倡客户端请求,试图说服Acceptor对此达成一致,并在发生冲突时充当协调者以推动协议向前发展
  • Acceptor决策者: 可以批准提案,Acceptor可以接受(accept)提案;并进行投票, 投票结果是否通过以多数派为准, 以如果某个提案被选定,那么该提案里的value就被选定了
  • Learner: 最终决策的学习者学习者充当该协议的复制因素(不参与投票)
  1. 决策模型

本地事务、分布式事务以及解决方案_第19张图片

  1. basic paxos流程

basic paxos流程一共分为4个步骤:

  • Prepare:Proposer提出一个提案,编号为N, 此N大于这个Proposer之前提出所有提出的编号, 请求Accpetor的多数人接受这个提案
  • Promise:如果编号N大于此Accpetor之前接收的任提案编号则接收, 否则拒绝
  • Accept:如果达到多数派, Proposer会发出accept请求, 此请求包含提案编号和对应的内容
  • Accepted:如果此Accpetor在此期间没有接受到任何大于N的提案,则接收此提案内容, 否则忽略
3.3.5.2、Basic Paxos流程图
  1. 无故障的basic Paxos

本地事务、分布式事务以及解决方案_第20张图片

  1. Acceptor失败时的basic Paxos,在下图中,多数派中的一个Acceptor发生故障,因此多数派大小变为2。在这种情况下,Basic Paxos协议仍然成功。
    本地事务、分布式事务以及解决方案_第21张图片
  2. Proposer失败时的basic Paxos,Proposer在提出提案之后但在达成协议之前失败。具体来说,传递到Acceptor的时候失败了,这个时候需要选出新的Proposer(提案人),那么 Basic Paxos协议仍然成功。

本地事务、分布式事务以及解决方案_第22张图片

  1. 当多个提议者发生冲突时的basic Paxos 最复杂的情况是多个Proposer都进行提案,导致Paxos的活锁问题

本地事务、分布式事务以及解决方案_第23张图片
针对活锁问题解决起来非常简单: 只需要在每个Proposer再去提案的时候随机加上一个等待时间即可.

3.3.6、 Multi-Paxos

3.3.6.1、Multi-Paxos流程图

针对basic Paxos是存在一定得问题,首先就是流程复杂,实现及其困难, 其次效率低(达成一致性需要2轮 RPC调用),针对basic Paxos流程进行拆分为选举复制的过程.

  1. 第一次流程-确定Leader

本地事务、分布式事务以及解决方案_第24张图片

  1. 第二次流程-直接由Leader确认

本地事务、分布式事务以及解决方案_第25张图片

3.3.6.2、Multi-Paxos角色重叠流程图

Multi-Paxos在实施的时候会将Proposer,Acceptor和Learner的角色合并统称为“服务器”。因此,最后只有“客户端”和“服务器”。

本地事务、分布式事务以及解决方案_第26张图片

Paxos 是论证了一致性协议的可行性,但是论证的过程据说晦涩难懂,缺少必要的实现细节,而且工程实现难度比较高, 广为人知实现只有 zk 的实现 zab 协议,zab 协议详情参考该文章

3.4、分布式数据一致性raft算法

Paxos协议的出现为分布式强一致性提供了很好的理论基础,但是Paxos协议理解起来较为困难,实现比较复杂。然后斯坦福大学RamCloud项目中提出了易实现,易理解的分布式一致性复制协议 Raft。Java,C++,Go 等都有其对应的实现之后出现的Raft相对要简洁很多。

分布式一致性raft算法动画演示:http://thesecretlivesofdata.com/raft/

raft是一个实现分布式一致性的协议,主要有领导选举、日志复制两个机制维持数据的一致性

每个节点都有三个状态:

  • follower
  • candidate
  • leader

以及两个时间:

  • 自旋时间:从follower到candidate的时间(150ms-300ms)
  • 心跳时间:leader向follow消息发送心跳的时间,比自旋时间小得多

3.4.1、选举leader机制

  1. 默认都以follower状态启动,刚开始的时候,每个节点会随机一个自旋时间,自选时间的长短在(150ms-300ms)
  2. 由于自旋时间不一样,最先自旋完成的节点被称为candidate,开启投票选举,自己先自投一次,然后向其他follower节点发送消息,向自己投票,一个follower节点只能够投一票,投完票的节点会重新开始自旋,当candidate节点收到票数超过总节点一半就变成leader
  3. 如果两个节点或者多个节点同时自旋完成,成为candidate,并获取相同的follower的投票,回重新进行第二轮选举,每个节点都重新开始自旋,直到一个candidate获取的偷票超过大多数,成为leader
  4. 当leader选举完成之后,会向每一个follower发送心跳信息,以维持连接,当follower超过一个自旋时间未接收到来自leader的心跳信息,就会从follower变成candidate,向其他的follower节点发送信息进行投票。

3.4.2、日志复制机制:

举leader完成后,更新数据时,接下来所有的数据都要先给leader发送,leader先在自己的log中写入更新数据变化,然后leader在下一次向follower发送心跳的时候,也会把日志信息发送给follower,follower更新完成后,会向leader发送确认信息,当leader接收到超过一半的确认信息后,leader会进行提交,写入数据,然后告诉follower也提交写入。

3.4.3、发生分区问题,导致节点断开的情况

还有一种情况,就是当发生分区问题,导致节点断开的情况
例如:5台机器分布在2个机房,1,2节点一个,3,4,5节点一个,两个机房中间用网线来连接,因为网线断裂问题,导致两个机房局域网隔离,这个时候每个机房会根据leader选举,重新选择出自己的leader,导致生成两个leader的问题,网络好了之后,会怎么处理?

对于1,2结点那个leader,当有客户端请求数据,leader更新log后收不到大多数follower的ack,所以改log不成功,一直保存不成功
对于3,4,5结点的leader:收到消息后更新log并且收到follower的ack过半,成功保存。
此时网络又通了,以更高轮选举的leader为主,退位一个leader。那1,2节点日志都回滚,同步新leader的log。这样就都一致性了

4、分布式事务几种方案

4.1、2PC 模式

数据库支持的 2PC【2 phase commit 二阶提交】,又叫做 XA Transactions。
MySQL 从 5.5 版本开始支持,SQL Server 2005 开始支持,Oracle 7 开始支持。 其中,XA 是一个两阶段提交协议,该协议分为以下两个阶段:

  • 第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交.
  • 第二阶段:事务协调器要求每个数据库提交数据。

其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务 中的那部分信息。

本地事务、分布式事务以及解决方案_第27张图片

4.1.1、2PC的执行流程

4.1.1.1、成功执行事务事务提交流程

本地事务、分布式事务以及解决方案_第28张图片

阶段一:

  • 事务询问:协调者向所有的参与者发送事务内容,询问是否可以执行事务(写本地的Undo/Redo日志)提交操作,并开始等待各参与者的响应
  • 反馈响应:各参与者向协调者反馈事务询问的响应

阶段二:

  • 发送提交请求:协调者向所有参与者发出commit请求。
  • 事务提交:参与者收到commit请求后,会正式执行事务提交操作,并在完成提交之后释放整个事务执行期间占用的事务资源。
  • 反馈事务提交结果:参与者在完成事务提交之后,向协调者发送Ack信息。
  • 完成事务:协调者接收到所有参与者反馈的Ack 信息后,完成事务。
4.1.1.2、中断事务流程

假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务
本地事务、分布式事务以及解决方案_第29张图片

阶段一:

  • 事务询问:协调者向所有的参与者发送事务内容,询问是否可以执行事务(写本地的Undo/Redo日志)提交操作,并开始等待各参与者的响应
  • 反馈响应:各参与者向协调者反馈事务询问的响应

阶段二:

  • 发送回滚请求:如果在阶段一有返回失败的反馈信息,则会发起回滚请求,协调者向所有参与者发出回滚请求。
  • 事务回滚:参与者收到回滚请求后,会利用其在阶段一中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。
  • 反馈事务提交结果:参与者在完成回滚之后,向协调者发送Ack信息。
  • 中断事务:协调者接收到所有参与者反馈的Ack 信息后,完成事务中断。

4.1.2、优缺点

4.1.2.1、优点

XA 协议比较简单,而且一旦商业数据库实现了 XA 协议,使用分布式事务的成本也比较低。

4.1.2.2、缺点

  • 同步阻塞:在二阶段提交的执行过程中,所有参与该事务操作的逻辑都处于阻塞状态,即当参与者占有公共资源时,其他节点访问公共资源会处于阻塞状态
  • 单点问题:若协调器出现问题,那么整个二阶段提交流程将无法运转,若协调者是在阶段二中出现问题时,那么其他参与者将会一直处于锁定事务资源的状态中,而无法继续完成事务操作
  • 数据不一致:在阶段二中,执行事务提交的时候,当协调者向所有的参与者发送Commit请求之后,发生了局部网络异常或者是协调者在尚未发送完Commit请求之前自身发生了崩溃,导致最终只有部分参与者收到了Commit请求,于是会出现数据不一致的现象。
  • 太过保守:在进行事务提交询问的过程中,参与者出现故障而导致协调者始终无法获取到所有参与者的响应信息的话,此时协调者只能依靠自身的超时机制来判断是否需要中断事务,这样的策略过于保守,即没有完善的容错机制,任意一个结点的失败都会导致整个事务的失败。
  • 性能问题:XA 性能不理想,特别是在交易下单链路,往往并发量很高,XA 无法满足高并发场景

4.2、3PC 模式

三阶段提交协议出现背景:一致性协议中设计出了二阶段提交协议(2PC),但是2PC设计中还存在缺陷,于是就有了三阶段提交协议,这便是3PC的诞生背景。

4.2.1、3PC提交协议

3PC,全称"three phase commit",是2PC的改进版,将2PC的“提交事务请求”过程一分为二,共形成了由CanCommit.PreCommit和doCommit三个阶段组成的事务处理协议。

本地事务、分布式事务以及解决方案_第30张图片

三阶段提交升级点(基于二阶段):

  • 三阶段提交协议引入了超时机制。
  • 在第一阶段和第二阶段中,引入了一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

简单讲:就是除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit.DoCommit三个阶段

4.2.2、3PC提交流程

第一阶段(CanCommit阶段):类似于2PC的准备(第一)阶段。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

  • 事务询问:协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
  • 响应反馈:参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

第二阶段(PreCommit阶段):协调者根据参生者的反应情况来决定是否可以执行事务的PreCommit操作。根据响应情况,有以下两种可能。

  • Yes
    1. 发送预提交请求:协调者向参与者发送PreCommit请求,并进入Prepared阶段。
    2. 事务预提交:参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中
    3. 响应反馈:如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
  • No
    假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。则有:
    1. 发送中断请求:协调者向所有参与者发送abort请求。
    2. 中断事务:参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

第三阶段(doCqmmit阶段):该阶段进行真正的事务提交,也可以分为执行提交和中断事务两种情况。

  • 执行提交
    1. 发送提交请求:协调者接收到参与者发送的ACK响应,那么它将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
    2. 事务提交:参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
    3. 响应反馈:事务提交完之后,向协调者发送ACK响应。
    4. 完成事务:协调者接收到所有参与者的ACK响应之后,完成事务。
  • 中断事务
    1. 发送中断请求:协调者向所有参与者发送abort请求
    2. 事务回滚:参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
    3. 反馈结果:参与者完成事务回滚之后,向协调者发送ACK消息
    4. 中断事务:协调者接收到所有参与者反馈的ACK消息之后,执行事务的中断。

注意:—旦进入阶段三,可能会出现2种故障

  • 协调者出现问题
  • 协调者和参与者之间的网络故障

本地事务、分布式事务以及解决方案_第31张图片
如果出现了任—一种情况,最终都会导致参与者无法收到doCommit请求或者abort请求,针对这种情况参与者都会在等待超时之后,继续进行事务提交

4.2.3、2PC对比3PC

  1. 首先对于协调者和参与者都设置了超时机制(在2PC中,只有协调者拥有超时机制,即如果在一定时间内没有收到参与者的消息则默认失败),主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
  2. 通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。
  3. PreCommit是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的。

但是3PC协议并没有完全解决数据一致问题,只是比2PC要减少一些数据一致性的几率。

4.2、柔性事务-TCC 事务补偿型方案

  • 刚性事务:遵循 ACID 原则,强一致性。
  • 柔性事务:遵循 BASE 理论,最终一致性; 与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。

本地事务、分布式事务以及解决方案_第32张图片

  • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
  • 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
  • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。

本地事务、分布式事务以及解决方案_第33张图片

4.3、柔性事务-最大努力通知型方案

按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种 方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合 MQ 进行实现,例如:通过 MQ 发送 http 请求,设置最大通知次数。达到通 知次数后即不再通知。

案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对 账文件),支付宝的支付成功异步回调

4.4、柔性事务-可靠消息+最终一致性方案(异步确保型)

实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只 记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确 认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。

5、Seata

5.1、XA模式

XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范提供了支持。

5.1.1、两阶段提交

XA是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交。

5.1.1.1、正常情况:

本地事务、分布式事务以及解决方案_第34张图片

5.1.1.2、异常情况:

本地事务、分布式事务以及解决方案_第35张图片

5.1.1.3、一阶段:
  • 事务协调者通知每个事物参与者执行本地事务
  • 本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁
5.1.1.4、二阶段:
  • 事务协调者基于一阶段的报告来判断下一步操作,如果一阶段都成功,则通知所有事务参与者,提交事务,如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务

5.1.2、Seata的XA模型

Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图:

本地事务、分布式事务以及解决方案_第36张图片

  • 一阶段,首先TM会向TC提交开启事务,TM会调用各个微服务,执行各个微服务的分支事务,RM会先向TC注册分支事务,然后再去执行分支事务,执行完毕之后不去提交,去向TC报告事务的状态。
  • 二阶段,TM发现事务结束了,通知TC,然后TC会去检查分支事务的状态,如果各个分支事务都成功,则进行事务的提交,如果有一个失败,则进行事务的回滚。
  • RM一阶段的工作:
    1. 注册分支事务到TC
    2. 执行分支业务sql但不提交
    3. 报告执行状态到TC
  • TC二阶段的工作:
    1. TC检测各分支事务执行状态,如果都成功,通知所有RM提交事务,如果有失败,通知所有RM回滚事务
  • RM二阶段的工作:
    1. 接收TC指令,提交或回滚事务

5.1.3、优缺点

5.1.3.1、XA模式的优点是什么?
  1. 事务的强一致性,满足ACID原则。
  2. 常用数据库都支持,实现简单,并且没有代码侵入
5.1.3.2、XA模式的缺点是什么?
  1. 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
  2. 依赖关系型数据库实现事务

5.2、AT模式

AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。
Seata的AT模型
基本流程图:

本地事务、分布式事务以及解决方案_第37张图片

  • 一阶段,首先TM会向TC提交开启事务,TM会调用各个微服务,执行各个微服务的分支事务,RM会先向TC注册分支事务,然后再去执行分支事务,执行完毕之后直接提交事务,但是在事务提交之前会进行一个快照备份,然后再去向TC报告事务的状态。TM发现事务结束了,通知TC,然后TC会去检查分支事务的状态,如果各个分支事务都成功,在二阶段,则异步进行快照备份的删除,如果有一个失败,则进行备份文件的恢复操作,然后异步的去删除快照备份文件。

  • 阶段一RM的工作:

  1. 注册分支事务
  2. 记录undo-log(数据快照)
  3. 执行业务sql并提交
  4. 报告事务状态
  • 阶段二提交时RM的工作:
  1. 删除undo-log即可
  • 阶段二回滚时RM的工作:
  1. 根据undo-log恢复数据到更新前

5.2.1、AT与XA的区别

5.2.1.1、简述AT模式与XA模式最大的区别是什么?
  • XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
  • XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
  • XA模式强一致;AT模式最终一致
5.2.1.2、脏写问题

在多线程并发访问AT模式的分布式事务时,有可能出现脏写问题。
本地事务、分布式事务以及解决方案_第38张图片
当事务1执行事务的时候,会先去获取到DB锁,保存快照,然后再去执行业务sql,提交完事务,会释放DB锁,接着应该通知TC,等待快照的恢复或者删除,但是因为整个过程的操作不是原子性的,所以在事务1释放DB锁之后,可能会被另一个事务2所抢占到,执行事务2的事务,在事务2执行完操作之后,更改完数据,提交事务之后,释放DB锁,再次被事务1所抢占到,经过TC判别,要进行快照的恢复,就导致事务2修改的数据被事务1回滚的事务进行覆盖,出现脏写的问题。

5.2.1.3、解决脏写问题

解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。
本地事务、分布式事务以及解决方案_第39张图片
当事务1执行事务的时候,会先去获取到DB锁,保存快照,然后再去执行业务sql,sql执行完毕之后,会先去拿到全局锁,全局锁是由TC去记录的,然后再提交完事务,释放DB锁,接着应该通知TC,等待快照的恢复或者删除,但是因为整个过程的操作不是原子性的,所以在事务1释放DB锁之后,可能会被另一个事务2所抢占到,执行事务2的事务,在事务2进行获取DB锁,保存快照,执行sql,在执行sql之后,也会去获取全局锁,但是因为在TC中记录的全局锁,被事务1所获取,就会进行重试,默认是30次,每次间隔10毫秒,如果时间超时之后,便会进行事务的回滚,释放DB锁,之后被事务1获取到锁,根据TC的状态进行快照的恢复或者删除,利用全局锁,就保证了多线程环境下同时只有一条事务执行成功,不会出现脏写的问题。
如果事务1是被Seata管理的事务,事务2没有被Seata管理,那么事务1在保存快照的时候会保存两份快照一个是修改之前的,一个是修改之后的,当发生快照数据恢复的时候,会先比对数据库的数据与快照备份修改之后的数据是否一致,如果一致的话则照常恢复数据,不是的话则记录异常发送警告。

5.2.2、优缺点

5.2.2.1、AT模式的优点:
  • 一阶段完成直接提交事务,释放数据库资源,性能比较好
  • 利用全局锁实现读写隔离
  • 没有代码侵入,框架自动完成回滚和提交
5.2.2.2、AT模式的缺点:
  • 两阶段之间属于软状态,属于最终一致
  • 框架的快照功能会影响性能,但比XA模式要好很多

5.3、TCC模式

TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:

  • Try:资源的检测和预留;
  • Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
  • Cancel:预留资源释放,可以理解为try的反向操作。

5.3.1、流程分析

举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。
• 阶段一( Try ):检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30
初始余额:
在这里插入图片描述
余额充足,可以冻结:
在这里插入图片描述
此时,总金额 = 冻结金额 + 可用金额,数量依然是100不变。事务直接提交无需等待其它事务。
• 阶段二(Confirm):假如要提交(Confirm),则冻结金额扣减30
确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了:
在这里插入图片描述
此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元
• 阶段二(Canncel):如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30
需要回滚,那么就要释放冻结金额,恢复可用金额:
在这里插入图片描述

5.3.2、Seata的TCC模型

本地事务、分布式事务以及解决方案_第40张图片

  • 一阶段,首先TM会向TC提交开启事务,TM会调用各个微服务,执行各个微服务的分支事务,RM会先向TC注册分支事务,然后再去执行分支事务,进行资源预留,执行完毕之后提交事务,去向TC报告事务的状态。
  • 二阶段,TM发现事务结束了,通知TC,然后TC会去检查分支事务的状态,如果各个分支事务都成功,则进行分支事务的confirm,会完成资源的操作,如果有一个失败,则进行分支事务的cancel,进行预留资源的释放。

5.3.3、优缺点

5.3.3.1、TCC模式的每个阶段是做什么的?
  • Try:资源检查和预留
  • Confirm:业务执行和提交
  • Cancel:预留资源的释放
5.3.3.2、TCC的优点是什么?
  • 一阶段完成直接提交事务,释放数据库资源,性能好
  • 相比AT模型,无需生成快照,无需使用全局锁,性能最强
  • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
5.3.3.3、TCC的缺点是什么?
  • 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
  • 软状态,事务是最终一致
  • 需要考虑Confirm和Cancel的失败情况,做好幂等处理

5.4、SAGA模式

在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
本地事务、分布式事务以及解决方案_第41张图片

5.4.1、Saga也分为两个阶段:

5.4.1.1、一阶段:直接提交本地事务
5.4.1.2、二阶段:成功则什么都不做;失败则通过编写补偿业务来回滚

5.4.2、优缺点

5.4.2.1、优点:
  • 事务参与者可以基于事件驱动实现异步调用,吞吐高
  • 一阶段直接提交事务,无锁,性能好
  • 不用编写TCC中的三个阶段,实现简单
5.4.2.2、缺点:
  • 软状态持续时间不确定,时效性差
  • 没有锁,没有事务隔离,会有脏写

5.5、四种模式对比

我们从以下几个方面来对比四种实现:

  • 一致性:能否保证事务的一致性?强一致还是最终一致?
  • 隔离性:事务之间的隔离性如何?
  • 代码侵入:是否需要对业务代码改造?
  • 性能:有无性能损耗?
  • 场景:常见的业务场景
    本地事务、分布式事务以及解决方案_第42张图片

6、Seata控制分步事务案例分析

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

快速开始:http://seata.io/zh-cn/docs/user/quickstart.html

  • TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
  • TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

本地事务、分布式事务以及解决方案_第43张图片

要执行下单,

  • TM告诉TC开启一个全局事务。
  • storage注册分支事务,实时向TC汇报分支状态
  • account失败,告诉TC失败了,TC回滚全部全局事务。

我们只需要使用一个 @GlobalTransactional 注解在业务方法上:

@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
    ......
}

SEATA AT模式需要 UNDO_LOG 表,记录之前执行的操作。每个涉及的子系统对应的数据库都要新建表

-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

pom

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

开始应用
从 https://github.com/seata/seata/archive/v0.7.1.zip 下载服务器软件包senta-server-0.7.1,将其解压缩。作为TC

结构
在这里插入图片描述

本地事务、分布式事务以及解决方案_第44张图片

本地事务、分布式事务以及解决方案_第45张图片
在大事务的入口标记注解@GlobalTransactional开启全局事务,并且每个小事务标记注解@Transactional

@GlobalTransactional
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo) {
}

地址:https://github.com/seata/seata-samples/tree/master/springcloud-jpa-seata

还需要注入 DataSourceProxy因为 Seata 通过代理数据源实现分支事务,如果没有注入,事务无法成功回滚,以下数据源代理的方式在springboot2.0版本以下可以使用,高版本会出现循环依赖

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource() {
        return new DruidDataSource();
    }

    /**
     * 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚
     * @param druidDataSource The DruidDataSource
     */
    @Primary
    @Bean("dataSource")
    public DataSource dataSource(DruidDataSource druidDataSource) {
        return new DataSourceProxy(druidDataSource);
    }

file.conf 的 service.vgroup_mapping 配置必须和spring.application.name一致,在GlobalTransactionAutoConfiguration类中,
默认会使用 ${spring.application.name}-fescar-service-group作为服务名注册到 Seata Server上(每个小事务也要注册到tc上),
如果和file.conf中的配置不一致,会提示 no available server to connect错误,也可以通过配置yaml的 spring.cloud.alibaba.seata.tx-service-group修改后缀,但是必须和file.conf中的配置保持一致

与上面配置数据源的方式等价,这么配置springboot2.0版本以上使用的配置

@Configuration
public class MySeataConfig {
    @Autowired
    DataSourceProperties dataSourceProperties;

    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties) {
        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())) {
            dataSource.setPoolName(dataSourceProperties.getName());
        }
        return new DataSourceProxy(dataSource);
    }
}

在order、ware中都配置好上面的配置,然后它还要求每个微服务要有register.conf和file.conf,将register.conf和file.conf复制到需要开启分布式事务的根目录,并修改file.conf

vgroup_mapping.${application.name}-fescar-service-group = "default"
service {
  #vgroup->rgroup
  vgroup_mapping.gulimall-ware-fescar-service-group = "default"
  #only support single node  
  default.grouplist = "127.0.0.1:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}

在大事务上@GlobalTransactional,小事务上@Transactional即可
seata的分布式事务不适合高并发的情况下使用,可以在后台功能使用,一般使用使用消息队列来保证最终一致性。

7、使用MQ来保证下单后解锁库存数据的最终一致性

7.1、业务的流程图

本地事务、分布式事务以及解决方案_第46张图片

针对订单模块创建以上消息队列,创建订单时消息会被发送至队列order.delay.queue,经过TTL的时间后消息会变成死信,以order.release.order的路由键经交换机转发至队列order.release.order.queue,再通过监听该队列的消息来实现过期订单的处理

本地事务、分布式事务以及解决方案_第47张图片

7.2、代码实现

  • 由于关闭订单和库存解锁都有可能被执行多次,因此要保证业务逻辑的幂等性,在执行业务是重新查询当前的状态进行判断

7.2.1、下订单失败,库存锁定后延迟检查是否需要解锁库存

创建订单时,会锁定库存,在库存锁定后通过路由键stock.locked发送至延迟队列stock.delay.queue,延迟时间到,死信通过路由键stock.release转发至stock.release.stock.queue,通过监听该队列进行判断当前订单状态,来确定库存是否需要解锁。

库存的锁定、解锁:

  1. 设计图
    本地事务、分布式事务以及解决方案_第48张图片

  2. pom

 <dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-amqpartifactId>
 dependency>
  1. yml
  rabbitmq:
    host: 192.168.56.10
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual
  1. 开启注解Rabbitmq
@EnableRabbit
public class GulimallWareApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallWareApplication.class, args);
    }

}
  1. 配置Rabbitmq
package com.song.gulimall.ware.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;

@EnableRabbit
@Configuration
public class MyRabbitmqConfig {
    @Bean
    public MessageConverter messageConverter() {
        //在容器中导入Json的消息转换器
        return new Jackson2JsonMessageConverter();
    }

    @Bean
    public Exchange stockEventExchange() {
        return new TopicExchange("stock-event-exchange", true, false);
    }

    /**
     * 延迟队列
     * @return
     */
    @Bean
    public Queue stockDelayQueue() {
        HashMap<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", "stock-event-exchange");
        arguments.put("x-dead-letter-routing-key", "stock.release");
        // 消息过期时间 2分钟
        arguments.put("x-message-ttl", 120000);
        return new Queue("stock.delay.queue", true, false, false, arguments);
    }

    /**
     * 普通队列,用于解锁库存
     * @return
     */
    @Bean
    public Queue stockReleaseStockQueue() {
        return new Queue("stock.release.stock.queue", true, false, false, null);
    }


    /**
     * 交换机和延迟队列绑定
     * @return
     */
    @Bean
    public Binding stockLockedBinding() {
        return new Binding("stock.delay.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.locked",
                null);
    }

    /**
     * 交换机和普通队列绑定
     * @return
     */
    @Bean
    public Binding stockReleaseBinding() {
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                null);
    }

}

  1. 下单的时候锁定库存 发送消息
@Transactional
    @Override
    public Boolean orderLockStock(WareSkuLockVo wareSkuLockVo) {

        //因为可能出现订单回滚后,库存锁定不回滚的情况,但订单已经回滚,得不到库存锁定信息,因此要有库存工作单
        WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
        taskEntity.setOrderSn(wareSkuLockVo.getOrderSn());
        taskEntity.setCreateTime(new Date());
        wareOrderTaskService.save(taskEntity);

        List<OrderItemVo> itemVos = wareSkuLockVo.getLocks();
        List<SkuLockVo> lockVos = itemVos.stream().map((item) -> {
            SkuLockVo skuLockVo = new SkuLockVo();
            skuLockVo.setSkuId(item.getSkuId());
            skuLockVo.setNum(item.getCount());
            //找出所有库存大于商品数的仓库
            List<Long> wareIds = baseMapper.listWareIdsHasStock(item.getSkuId(), item.getCount());
            skuLockVo.setWareIds(wareIds);
            return skuLockVo;
        }).collect(Collectors.toList());

        for (SkuLockVo lockVo : lockVos) {
            boolean lock = true;
            Long skuId = lockVo.getSkuId();
            List<Long> wareIds = lockVo.getWareIds();
            //如果没有满足条件的仓库,抛出异常
            if (wareIds == null || wareIds.size() == 0) {
                throw new NoStockException(skuId);
            }else {
                for (Long wareId : wareIds) {
                    Long count=baseMapper.lockWareSku(skuId, lockVo.getNum(), wareId);
                    if (count==0){
                        lock=false;
                    }else {
                        //锁定成功,保存工作单详情
                        WareOrderTaskDetailEntity detailEntity = WareOrderTaskDetailEntity.builder()
                                .skuId(skuId)
                                .skuName("")
                                .skuNum(lockVo.getNum())
                                .taskId(taskEntity.getId())
                                .wareId(wareId)
                                .lockStatus(1).build();
                        wareOrderTaskDetailService.save(detailEntity);
                        //发送库存锁定消息至延迟队列
                        StockLockedTo lockedTo = new StockLockedTo();
                        lockedTo.setId(taskEntity.getId());
                        StockDetailTo detailTo = new StockDetailTo();
                        BeanUtils.copyProperties(detailEntity,detailTo);
                        lockedTo.setDetailTo(detailTo);
                        rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);

                        lock = true;
                        break;
                    }
                }
            }
            if (!lock) throw new NoStockException(skuId);
        }
        return true;
    }
  1. 监死信队列 对库存判断 进行解锁
package com.song.gulimall.ware.listener;

import com.rabbitmq.client.Channel;
import com.song.common.to.mq.OrderTo;
import com.song.common.to.mq.StockLockedTo;
import com.song.gulimall.ware.service.WareSkuService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Slf4j
@Component
@RabbitListener(queues = {"stock.release.stock.queue"})
public class StockReleaseListener {

    @Autowired
    private WareSkuService wareSkuService;

    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo stockLockedTo, Message message, Channel channel) throws IOException {
        log.info("************************收到库存解锁的消息********************************");
        try {
            wareSkuService.unlock(stockLockedTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

  1. 解锁库存
 /**
     *    1、没有这个订单,必须解锁库存
     *          *          2、有这个订单,不一定解锁库存
     *          *              订单状态:已取消:解锁库存
     *          *                      已支付:不能解锁库存
     * 消息队列解锁库存
     * @param stockLockedTo
     */
    @Override
    public void unlock(StockLockedTo stockLockedTo) {
        StockDetailTo detailTo = stockLockedTo.getDetailTo();
        WareOrderTaskDetailEntity detailEntity = wareOrderTaskDetailService.getById(detailTo.getId());
        //1.如果工作单详情不为空,说明该库存锁定成功
        if (detailEntity != null) {
            WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(stockLockedTo.getId());
            R r = orderFeignService.infoByOrderSn(taskEntity.getOrderSn());
            if (r.getCode() == 0) {
                OrderTo order = r.getData("order", new TypeReference<OrderTo>() {
                });
                //没有这个订单||订单状态已经取消 解锁库存
                if (order == null||order.getStatus()== OrderStatusEnum.CANCLED.getCode()) {
                    //为保证幂等性,只有当工作单详情处于被锁定的情况下才进行解锁
                    if (detailEntity.getLockStatus()== WareTaskStatusEnum.Locked.getCode()){
                        unlockStock(detailTo.getSkuId(), detailTo.getSkuNum(), detailTo.getWareId(), detailEntity.getId());
                    }
                }
            }else {
                throw new RuntimeException("远程调用订单服务失败");
            }
        }else {
            //无需解锁
        }
    }

业务逻辑:

库存锁定

  • 由于可能订单回滚的情况,所以为了能够得到库存锁定的信息,在锁定时需要记录库存工作单,其中包括订单信息和锁定库存时的信息(仓库id,商品id,锁了几件…), 在锁定成功后,向延迟队列发消息,带上库存锁定的相关信息。

库存的解锁

  • 收到死信队列发来的消息后,如果工作单详情不为空,说明该库存锁定成功 查询最新的订单状态,如果订单不存在,说明订单提交出现异常回滚,需要解锁
  • 如果订单存在(但订单处于已取消的状态),需要解锁
  • 如果工作单详情为空,说明库存未锁定,自然无需解锁

7.2.2、取消订单,库存锁定后延迟检查是否需要解锁库存

订单的
本地事务、分布式事务以及解决方案_第49张图片

  1. 订单的pom、yml、启动注解同上

  2. 配置

package com.song.gulimall.order.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;

@Configuration
public class MyRabbitmqConfig {
    @Bean
    public Exchange orderEventExchange() {
        /**
         *   String name,
         *   boolean durable,
         *   boolean autoDelete,
         *   Map arguments
         */
        return new TopicExchange("order-event-exchange", true, false);
    }

    /**
     * 延迟队列
     * @return
     */
    @Bean
    public Queue orderDelayQueue() {
       /**
            Queue(String name,  队列名字
            boolean durable,  是否持久化
            boolean exclusive,  是否排他
            boolean autoDelete, 是否自动删除
            Map arguments) 属性
         */
        HashMap<String, Object> arguments = new HashMap<>();
        //死信交换机
        arguments.put("x-dead-letter-exchange", "order-event-exchange");
        //死信路由键
        arguments.put("x-dead-letter-routing-key", "order.release.order");
        arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
        return new Queue("order.delay.queue",true,false,false,arguments);
    }

    /**
     * 普通队列
     *
     * @return
     */
    @Bean
    public Queue orderReleaseQueue() {

        Queue queue = new Queue("order.release.order.queue", true, false, false);

        return queue;
    }

    /**
     * 创建订单的binding
     * @return
     */
    @Bean
    public Binding orderCreateBinding() {
        /**
         * String destination, 目的地(队列名或者交换机名字)
         * DestinationType destinationType, 目的地类型(Queue、Exhcange)
         * String exchange,
         * String routingKey,
         * Map arguments
         * */
        return new Binding("order.delay.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.create.order", null);
    }

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

    @Bean
    public Binding orderReleaseOrderBinding() {
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.other.#",
                null);
    }
package com.song.gulimall.order.config;

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyAmqpConfig {
    @Bean
    public MessageConverter messageConverter() {
        //在容器中导入Json的消息转换器
        return new Jackson2JsonMessageConverter();
    }
}

  1. 创建订单,向死信队列中发送消息
@Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo) {
        SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
        responseVo.setCode(0);
        //1. 验证防重令牌
        MemberResponseVo memberResponseVo = LoginInterceptor.loginUser.get();
        String script= "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Long execute = redisTemplate.execute(new DefaultRedisScript<>(script,Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()), submitVo.getOrderToken());
        if (execute == 0L) {
            //1.1 防重令牌验证失败
            responseVo.setCode(1);
            return responseVo;
        }else {
            //2. 创建订单、订单项
            OrderCreateTo order =createOrderTo(memberResponseVo,submitVo);

            //3. 验价
            BigDecimal payAmount = order.getOrder().getPayAmount();
            BigDecimal payPrice = submitVo.getPayPrice();
            if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
                //4. 保存订单
                saveOrder(order);
                //5. 锁定库存
                List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setCount(item.getSkuQuantity());
                    return orderItemVo;
                }).collect(Collectors.toList());
                WareSkuLockVo lockVo = new WareSkuLockVo();
                lockVo.setOrderSn(order.getOrder().getOrderSn());
                lockVo.setLocks(orderItemVos);
                R r = wareFeignService.orderLockStock(lockVo);
                //5.1 锁定库存成功
                if (r.getCode()==0){
//                    int i = 10 / 0;
                    responseVo.setOrder(order.getOrder());
                    responseVo.setCode(0);

                    //发送消息到订单延迟队列,判断过期订单
                    rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());

                    //清除购物车记录
                    BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(CartConstant.CART_PREFIX + memberResponseVo.getId());
                    for (OrderItemEntity orderItem : order.getOrderItems()) {
                        ops.delete(orderItem.getSkuId().toString());
                    }
                    return responseVo;
                }else {
                    //5.1 锁定库存失败
                    String msg = (String) r.get("msg");
                    throw new NoStockException(msg);
                }

            }else {
                //验价失败
                responseVo.setCode(2);
                return responseVo;
            }
        }
    }
  1. 取消订单的监听
package com.song.gulimall.order.listener;

import com.rabbitmq.client.Channel;
import com.song.gulimall.order.entity.OrderEntity;
import com.song.gulimall.order.service.OrderService;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@RabbitListener(queues = {"order.release.order.queue"})
public class OrderCloseListener {

    @Autowired
    private OrderService orderService;

    @RabbitHandler
    public void listener(OrderEntity orderEntity, Message message, Channel channel) throws IOException {
        System.out.println("收到过期的订单信息,准备关闭订单" + orderEntity.getOrderSn());
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            orderService.closeOrder(orderEntity);
            channel.basicAck(deliveryTag,false);
        } catch (Exception e){
            channel.basicReject(deliveryTag,true);
        }

    }
}

关闭过期的的订单,因为取消库存锁定、取消订单 都是通过消息发送,消息可能出现延迟的现象,导致取消订单的消耗的时长要比取消库存锁定的大,造成库存锁死得不到释放的现象,所以在订单关闭的时候,需要向库存发送一条消息,在库存服务判断库存是否释放。双重保障防止库存锁死

/**
     * 关闭过期的的订单
     * @param orderEntity
     */
    @Override
    public void closeOrder(OrderEntity orderEntity) {
        //因为消息发送过来的订单已经是很久前的了,中间可能被改动,因此要查询最新的订单
        OrderEntity newOrderEntity = this.getById(orderEntity.getId());
        //如果订单还处于新创建的状态,说明超时未支付,进行关单
        if (newOrderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()) {
            OrderEntity updateOrder = new OrderEntity();
            updateOrder.setId(newOrderEntity.getId());
            updateOrder.setStatus(OrderStatusEnum.CANCLED.getCode());
            this.updateById(updateOrder);

            //关单后发送消息通知其他服务进行关单相关的操作,如解锁库存
            OrderTo orderTo = new OrderTo();
            BeanUtils.copyProperties(newOrderEntity,orderTo);
            rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other",orderTo);
        }
    }
package com.song.gulimall.ware.listener;

import com.rabbitmq.client.Channel;
import com.song.common.to.mq.OrderTo;
import com.song.common.to.mq.StockLockedTo;
import com.song.gulimall.ware.service.WareSkuService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Slf4j
@Component
@RabbitListener(queues = {"stock.release.stock.queue"})
public class StockReleaseListener {

    @Autowired
    private WareSkuService wareSkuService;
    @RabbitHandler
    public void handleStockLockedRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
        log.info("************************从订单模块收到库存解锁的消息********************************");
        try {
            wareSkuService.unlock(orderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

你可能感兴趣的:(安装教程,项目问题记录,分布式事务,2PC,3PC,TCC提交,Seata,raft协议,Paxos协议)