一、本地事务
本地事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器位于同一节点相同数据库上。
1.ACID理论(强一致性)
原子性(Atomicity)、一致性(Consistency )隔离性或独立性( Isolation)和持久性(Durabilily),简称就是ACID;
- 原子性:一系列的操作整体不可拆分,要么同时成功,要么同时失败
- 一致性:数据在事务的前后,不能破坏数据的完整性以及业务逻辑上的一致性。
- 隔离性:多个事务并发访问时,事务之间互相隔离。(脏读、不可重复读、幻读)
- 持久性:事务成功后,数据会保存在数据库并不再回滚
2.事务间的影响
三者可能同时出现,都会导致同一事务中前后两次读取结果不一致
描述 | |
---|---|
脏读 | 在一个事务中读取了另一个事务未提交的脏数据(前后两次结果读取不一致) |
不可重复读 | 在一个事务中读取了另一个事务dml操作并提交的数据(前后两次结果读取不一致) |
幻读 | 在一个事务中读取了另一个事务ddl操作并提交的数据(前后两次结果读取不一致) |
3.解决方法
开启事务,设置事务隔离级别
在数据库管理系统(DBMS)中,默认情况下一条SQL就是一个单独事务,事务是自动提交的。 只有显式的使用start transaction开启一个事务,才能将一个代码块放在事务中执行。 保障事务的原子性是数据库管理系统的责任,为此许多数据源采用日志机制。 例如,SQL Server使用一个预写事务日志,在将数据提交到实际数据页面前,先写在事务日志上。
4.事务隔离级别
概述: 事务之间会存在互相影响的情况,事务隔离级别不同影响的范围也不同
谁实现的? 由数据库实现 在JAVA中只是设定事务隔离级别,而不是实现它
脏读 | 不可重复读 | 幻读 | 默认级别 | 实现方法 | |
---|---|---|---|---|---|
Read uncommitted | √ | √ | √ | ||
Read committed | × | √ | √ | SQLServer/Oracle | |
Repeatable read | × | × | √ | Mysql | 行锁 |
Serializable | × | × | × | 表锁 |
4.1.Spring设置事务隔离级别
@Transactional(isolation=Isolation.REPEATABLE_READ)
- DEFAULT (默认)
- READ_UNCOMMITTED (读未提交)该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。
- READ_COMMITTED (读已提交)一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读问题,Oracle 和 SQL Server 的默认隔离级别。
- REPEATABLE_READ (可重复读)该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间点的状态,因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。MySQL的 InnoDB 引擎可以通过 next-key locks 机制来避免幻读。
- SERIALIZABLE (串行化)在该隔离级别下事务都是串行顺序执行的,MySQL 数据库的 InnoDB 引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题
5.Spring的传播行为
- PROPAGATION_REQUIRED:(要求一个事务)当前没有事务,创建事务;如果存在事务,就加入该事务【常用】
- PROPAGATION_SUPPORTS:(支持当前事务)当前存在事务,就加入该事务;如果当不存在事务,以非事务执行
- PROPAGATION_MANDATORY:(强制使用当前事务)当前存在事务,就加入该事务;如果不存在事务,抛出异常
- PROPAGATION_REQUIRES_NEW:(要求一个新事务)创建新事务执行
- PROPAGATION_NOT_SUPPORTED:(不使用事务)以非事务方式执行,如果存在事务,就把当前事务挂起
- PROPAGATION_NEVER:(强制不使用事务)如果当前存在事务,则抛出异常
- PROPAGATION_NESTED:(嵌套事务)如果存在事务,则在嵌套事务内执行;如果没有事务,创建事务
5.1.案例
@Transactional(timeout=30)
public void a() {
b();// a事务传播给了b事务,并且b事务的设置失效
c();// c单独创建一个新事务
}
@Transactional(propagation = Propagation.REQUIRED, timeout=2)
public void b() {
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c() {
}
5.2.本地事务失效问题
案例
// 事务方法调用本方法内的其他事务方法,出现本地事务失效的问题
@Transactional(timeout=30)
public void a() {
b();// 绕过了代理对象,异常不会回滚
c();// 绕过了代理对象,异常不会回滚
}
@Transactional(propagation = Propagation.REQUIRED, timeout=2)
public void b() {
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c() {
}
原因
Spring事务的原理是使用了代理对象,如果两个事务方法在同一个Service类内,事务A方法直接调用事务B方法,即绕过了代理对象,事务未生效
解决
使用代理对象来调用事务方法,不能使用this.b(),也不能注入自己
具体步骤:
- 引入aop依赖
<!-- 引入aop,解决本地事务失效问题 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 在启动类添加注解,开启动态代理 // 加该注解后使用aspectj作动态代理【即使没有接口也能代理,使用cglib继承的方式完成动态代理】
// exposeProxy = true 对外暴露代理对象
@EnableAspectJAutoProxy(exposeProxy = true) - 本类互相调用使用代理对象 获取当前类的代理对象
OrderServiceImpl orderService = (OrderServiceImpl)AopContext.currentProxy();
orderService.b();
orderService.c();
二、CAP定理和BASE理论
1.CAP定理
1.1.概述
CAP定理,指的是在一个分布式系统中:
- 一致性(Consistency): 在分布式系统中的所有数据备份,在同一时刻是一致的。(3个数据库,同一份数据值一致)
- 可用性(Availability): 在集群中一部分节点故障后,集群整体仍能响应客户端的请求。(同一时刻数据可允许出现不一致)
- 分区容错性(Partition tolerance): 分布式系统之间允许通信失败。(分布式网络必须保证分区容错性,因为网络通信一定会出现问题) 大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition) 分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。
CAP原则指的是,这三个要素最多只能同时实现两点,不可能三者兼蹊。
- CA:互斥
- AP:可用性+分区容错性,允许出现子网络通信失败,并保证可用性(出现数据未同步,不能保证一致性)
- CP:一致性+分区容错性,允许出现子网络通信失败,并保证数据一致性(网络通信故障的节点无法继续提供服务,牺牲可用性)
1.2.实现一致性的算法
raft、paxos
raft算法演示:
1.3.raft算法(CP)
1.3.1.概述
1.raft算法通过领导选举、日志复制实现一致性+分区容错性 2.无法实现可用性,例如出现两个分区的时候,出现了两个领导并且两个分区节点数相等时,两个分区都无法工作(无法选出领导,因为无法获得大多数选举人的投票)
三种状态
Follower:随从(集群所有节点启动默认都是随从状态) Candidate:候选者(没有在集群中监听到领导者,变成候选者) Leader:领导者(得票多者被选举为领导者,所有的修改都必须经过领导)
三个超时时间
election timeout:选举超时(随从者成为候选者的自旋时间) 150ms~300ms之间 heartbeat timeout:心跳超时(领导者发送心跳给跟踪者的间隔时间) 最小选举超时: 如果集群中存在Leader时,并且接收到心跳信息之后在最小选举超时时间内接受到请求投票消息,那么将会忽略掉该投票消息。 在分布式系统中,有时候需要对集群中的成员数量进行更新的操作。对于被删除的服务器而言,如果它们没有及时关闭,那么它们将不会接收到心跳信息和日志信息,从而不断发生超时,最后导致任期不断增加(高于集群中所有成员的任期),然后不断向集群中发送请求投票消息。集群中的Leader将变为Follower,集群中将不断开始新的选举,从而扰乱集群的正常运行。
1.3.2.领导选举
选举步骤: 1.集群启动各节点进入随从态,若未监听到领导者,各自进行选举倒计时,倒计时结束成为候选者,并发起第一轮选举,向其他节点发起投票请求(自己会给自己投一票) 2.其他节点如果当前未投过票,就会投票给自己(可能投给了其他候选者) 3.随从节点投票完后立即进入下一选举时间(重置选举时间进入自旋态) 4.领导会发送追加日志消息给随从节点,并且不断给随从节点发送心跳 5.随从者接收心跳后重新进入下一轮选举自旋 6.领导者宕机后,随从者选举超时成为候选者进入第二轮选举,并向其他节点发起投票请求 7.随从节点投票后也进入第二轮选举 注意: 1.领导者也会回复投票请求 2.领导者接收到其他领导者的心跳检测后会让出领导者 3.同一轮投票时,每个节点只能给一个候选节点投票
选举超时的随从者成为候选者:
发起第一轮选举,向其他节点发起投票请求:
其他节点如果当前未投过票,就会投票给自己:
随从节点投票完后立即进入下一选举时间:
候选节点获得大多数投票后成为领导者:
领导会发送追加日志消息给随从节点:
领导者发送心跳消息:
随从者接收心跳后重新进入下一轮选举自旋:
第二轮选举结束,A成为新领导:
投票分离
当出现两个候选者的时候,会出现投票分离,结果会一直自旋抢票,直到产生一个领导者 C成为领导者的同时,B也成为了候选者并发出了投票请求
两个候选者:
D投给C,B投给A:
重新自旋,可能所有节点成为候选者:
C成为领导者的同时,B也成为了候选者并发出了投票请求:
1.3.3.日志复制
日志复制: 指集群使用raft算法,以日志复制的方式实现一致性 步骤: 1.所有修改数据都必须经过领导者 2.领导者创建节点日志,此时日志是未提交状态,且数据未修改 3.日志不会马上发出,会伴随心跳发送给每一个节点,节点收到心跳后回复 4.当大多数节点回复后领导者提交,数据更新 5.领导者更新成功响应客户端更新成功,并在下一次心跳通知其他节点也提交更新 7.所有节点修改成功后,集群实现一致性 注意: 1.修改数据的请求到达leader后创建日志,但是日志发出是随下一次心跳发出的 2.领导者提交后就会响应客户端修改成功,并在下一个心跳时间告诉其他节点提交 3.如果领导者没有接收到大多数节点的回复,日志不会提交
所有修改数据都必须经过领导者:
每一个修改操作都会被添加为节点日志:
此时日志是未提交状态,领导者还未修改数据:
领导者将日志复制发送给每一个随从节点:
大多数节点收到日志并告知领导者:
领导节点提交,数据更新:
领导者更新成功后通知其他节点也提交更新:
所有节点修改成功后,集群实现一致性:
1.3.4.网络分区一致性
1.网络出现分区时,原先领导延任,其他区重新选取领导 2.如果领导没有接收到大多数节点的回应,不会提交日志(只有领导E的提交成功) 3.恢复网络通信后,低轮次领导者会退位,并且A和B将未提交的数据全部回滚,并且同步新领导的数据
各分区出现各自的领导: 如果领导没有接收到大多数节点的回应,不会提交日志: B退位,并且A和B回滚未提交的数据,并同步新领导的数据:
1.4.paxos算法
1.5.集群面临的问题(AP)
对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到 99.99999%(N 个 9),即保证P 和 A,舍弃 C。
2.BASE理论(最终一致性)
2.1.案例
创建订单 1)远程锁定库存 2)创建订单 3)扣减积分 当扣减积分异常时,订单可以回滚,但是库存已经锁定无法回滚,所以在最后将锁定的库存释放,达到最终一致性。
2.2.概述
是对CAP理论的延伸,思想是即使无法做到强一致性(CAP的一致性就是强一致性),但可以采用适当的采取弱一致性,即最终一致性。【保证AP时,无法保证C,但是可以最终一致性】
BASE是指
- 基本可用(Basically Available)
- 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用。
- 响应时间上的损失:正常情况下搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。
- 功能上的损失:购物网站在购物高峰(如双十一)时,为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面。
- 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用。
- 软状态(Soft State)
- 软状态是指允许系统存在中间状态,中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体现。mysal replication的异步复制也是一种体现。通俗解释就是一致性只有两个状态,成功、失败,软状态是二者之间的状态
- 最终一致性(Eventual Consistency)
- 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
2.3.强一致性、弱一致性、最终一致性
强一致性:更新后的数据后续访问能看到(数据强一致,及时更新到所有节点) 弱一致性:容忍部分或全部访问不到(数据不一致,数据未及时同步到所有节点)【软状态,存在不一致的数据】 最终一致性:弱一致性经过一段时间后更新到最新数据【订单创建失败,经过一段时间后释放库存】
三、分布式事务
1.分布式事务概述
分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。 分布式事务的方案其实就是根据不同一致性设计的几种不同方案
分布式系统出现的异常
异常: 机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失
案例: 1.远程服务假失败: 远程服务其实成功了,由于网络故障没有返回 导致:订单回滚,库存成功扣减
2.远程服务执行完成,用户服务扣减积分异常 导致:订单回滚,库存成功扣减
2.刚性事务XA(ACID)
刚性事务遵循ACID理论,强一致性,基于AT模式(Auto Transaction),根据日志自动提交回滚
XA协议是一个基于数据库的分布式事务协议,其分为两部分: 事务管理器、本地资源管理器。
事务管理器:作为一个全局的调度者,负责对各个本地资源管理器统一号令提交或者回滚。二阶提交协议(2PC)和三阶提交协议(3PC)就是根据此协议衍生出来而来。如今Oracle、Mysql等数据库均已实现了XA接口。
2.1.2PC协议
两个阶段: 第一阶段提交: 协调者收到客户端请求,协调者给每一个参与者发送prepare指令,参与者接收指令执行本地数据但不提交事务,并返回ready
第二阶段提交: 1)当第一阶段所有参与者返回接收prepare指令并返回ready时,协调者发送commit指令给参与者,所有参与者提交事务 2)当第一阶段返回超时或者参与者执行失败,协调者在第二阶段告诉所有参与者回滚
案例
一个下单流程会用到多个服务,各个服务都无法保证调用的其他服务的成功与否,这个时候就需要一个全局的角色(协调者)对各个服务(参与者)进行协调。
一个下单请求过来通过协调者,给每一个参与者发送Prepare消息,执行本地数据脚本但不提交事务。 如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中被占用的资源,显然2PC做到了所有操作要么全部成功、要么全部失败。
缺点
二阶段提交看似能够提供原子性的操作,但它存在着严重的缺陷
- 网络抖动导致的数据不一致: 第二阶段中
协调者
向参与者
发送commit
命令之后,一旦此时发生网络抖动,导致一部分参与者
接收到了commit
请求并执行,可其他未接到commit
请求的参与者
无法执行事务提交。进而导致整个分布式系统出现了数据不一致。 - 超时导致的同步阻塞问题:
2PC
中的所有的参与者节点都为事务阻塞型
,当某一个参与者
节点出现通信超时,其余参与者
都会被动阻塞占用资源不能释放。 - 单点故障的风险: 由于严重的依赖
协调者
,一旦协调者
发生故障,而此时参与者
还都处于锁定资源的状态,无法完成事务commit
操作。虽然协调者出现故障后,会重新选举一个协调者,可无法解决因前一个协调者
宕机导致的参与者
处于阻塞状态的问题。
2.2.3PC协议
三个阶段:
- CanCommit: 协调者向所有参与者发送CanCommit命令,询问是否可以执行事务提交操作。如果全部响应YES则进入下一个阶段。
- PreCommit: 协调者向所有参与者发送PreCommit命令,询问是否可以进行事务的预提交操作,参与者接收到PreCommit请求后,如参与者成功的执行了事务操作,则返回Yes响应,进入最终commit阶段。一旦参与者中有向协调者发送了No响应,或因网络造成超时,协调者没有接到参与者的响应,协调者向所有参与者发送abort请求,参与者接受abort命令执行事务的中断。
- DoCommit: 在前两个阶段中所有参与者的响应反馈均是YES后,协调者向参与者发送DoCommit命令正式提交事务,如协调者没有接收到参与者发送的ACK响应,会向所有参与者发送abort请求命令,执行事务的中断。
缺点
三段提交(3PC)是对两段提交(2PC)的一种升级优化,3PC在2PC的第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前,各参与者节点的状态都一致。同时在协调者和参与者中都引入超时机制,当参与者各种原因未收到协调者的commit请求后,会对本地事务进行commit,不会一直阻塞等待,解决了2PC的单点故障问题,但3PC还是没能从根本上解决数据一致性的问题。 主要原因:网络中断,自动提交
3.柔性事务(BASE)
柔性事务是基于BASE理论,最终一致性
3.1.TCC模式
TCC与XA协议不同,不是数据库原生实现
概述
TCC(Try-Confirm-Cancel)分布式事务模型通过对业务逻辑进行分解来实现分布式事务。顾名思义,TCC事务模型需要业务系统提供以下三种业务逻辑。
- Try:完成业务检查,预留业务所需的资源。Try操作是整个TCC的精髓,可以灵活选择业务资源锁的粒度。
- Confirm:执行业务逻辑,直接使用Try阶段预留的业务资源,无须再次进行业务检查。
- Cancel:释放Try阶段预留的业务资源。
TCC模型仅提供两阶段原子提交协议,保证分布式事务的原子性。事务的隔离交给业务逻辑来实现。TCC 模型的隔离性思想是,通过对业务的改造将对数据库资源层面加锁上移至对业务层面加锁,从而释放底层数据库锁资源,拓宽分布式事务锁协议,提高系统的并发性。
案例
以A账户向B账户汇款100元为例。汇款服务和收款服务需要分别实现Try、Confirm、Cancel这三个接口,并在业务初始化阶段将这三个接口的实现注入TCC事务管理器。
- 汇款服务
— Try:检查A账户的有效性;检查A账户的余额是否充足;从A账户中扣减100元,并将状态置为“转账中”;预留扣减资源,将“从A账户向B账户转账100元”这个事件存入消息或日志。
— Confirm:不做任何操作。
— Cancel:A账户增加100元;从日志或消息中释放扣减资源。
- 收款服务
— Try:检查B账户的有效性。
— Confirm:读取日志或者消息,B账户增加100元;从日志或消息中释放扣减资源。
— Cancel:不做任何操作。
由此可以看出,TCC模型对业务的侵入性较强,改造的难度较大。
缺点
虽然在柔性事务中,TCC事务模型的功能最强,但需要应用方负责提供实现Try、Confirm和Cancel操作的三个接口,供事务管理器调用,因此业务方改造的成本较高。
3.2.最大努力通知型
按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合MQ进行实现,例如:通过MQ发送http请求,设置最大通知次数。达到通知次数后即不再通知。 案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对账文件),支付宝的支付成功异步回调
3.3. 可靠消息+最终一致性
概述
消息一致性方案是通过消息中间件保证上下游应用数据操作一致性的。基本思路是,将本地操作和发送消息放在同一个本地事务中,下游应用从消息系统订阅该消息,收到消息后执行相应的操作,本质上是依靠消息的重试机制达到最终一致性的。
缺点
消息驱动的缺点是,耦合度高,需要在业务系统中引入消息中间件,将导致系统复杂度增加。
基于ACID的强一致性事务和基于BASE的最终一致性事务都不是“银弹”,只有在最适合的场景中才能发挥它们的最大长处。
3.4.Sega
4.AT、TCC、XA
- XA协议
- 基于数据库的分布式事务协议,由数据库原生支持,例如2PC、3PC,是自动提交的
- AT模式
- 是基于XA协议实现的,将一段执行的sql记录在undo_log表中,然后通过undo_log自动完成回滚,不需要程序员手动写补偿代码(一个注解即可搞定分布式事务)
- AT模式下必须要依赖于数据库事务,并且每个资源管理器都要创建一个回滚日志表,需要回滚的时候根据日志魔改数据库
- Seata的AT模式与2PC的区别,第一段提交已经提交了事务,当出现异常的时候是根据undo_log回滚。Seata的AT模式本质其实就是2PC模式,只不过是变种
- 不使用与高并发场景
- TCC模式
- 未基于XA协议实现,由应用端实现,程序员在Cancel阶段实现业务逻辑完成数据回滚
- 可以不依赖于关系型数据库,比如我们的远程操作可能是操作Redis、MongoDB等。因为TCC中的各阶段的逻辑都是我们手动来实现的
5.总结:外柔内刚
- 强一致性的事务与柔性事务的API和功能并不完全相同,因此不能在它们之间自由地透明切换。在开发决策阶段,必须要在强一致的事务和柔性事务之间抉择,因此设计和开发成本大幅增加。
- 基于XA协议的强一致事务使用起来相对简单,但是无法很好地应对互联网的短事务和高并发场景;柔性事务则需要开发者对应用进行改造,接入成本非常高,并且需要开发者自行实现资源锁定和反向补偿。
- 对于分布式系统来说,建议使用“外柔内刚”的设计方案。外柔指的是在跨数据分片的情况下使用柔性事务,保证数据最终一致,并且换取最佳性能;内刚则是指在同一数据分片内使用本地事务,以满足ACID特性。
6.分布式事务对比表
XA协议: 适用场景:后台管理接口,例如新增商品(无高并发)
TCC: 适用场景:高并发接口,例如创建订单(高并发)
四、Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
概述: Seata的AT模式是基于XA协议实现的,本质上是2PC协议的变种,在第一段提交时提交事务,第二段回滚时是依照undo_log表进行回滚
1.术语表
- Seata术语
- TC (Transaction Coordinator) - 事务协调者 维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器 定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
2.流程
- 事务管理器告诉事务协调器开启全局事务
- 事务管理器调用各资源管理器,并且各资源服务器在事务协调者上注册分支事务
- 事务管理器调用各分支事务,之后分支事务向事务协调器汇报分支状态
- 如果所有事务分支成功则全局提交,否则全局回滚
3.整合Seata实现AT模式
3.1.创建UNDO_LOG表
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;
3.2.添加依赖
<!--seata 分布式事务 seata-all使用0.9【所以启动 事务协调者0.9版本的】--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>0.9.0</version> </dependency>
3.3.下载安装seata服务器
- 下载seata服务器并解压 seata服务器
- 修改配置文件registry.xml
registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 注册中心 type = "nacos" nacos { application = "seata-server" serverAddr = "localhost:8848" namespace = "" cluster = "default" username = "" password = "" } eureka { serviceUrl = "http://localhost:8761/eureka" application = "default" weight = "1" } redis { serverAddr = "localhost:6379" db = 0 password = "" cluster = "default" timeout = 0 } zk { cluster = "default" serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } consul { cluster = "default" serverAddr = "127.0.0.1:8500" } etcd3 { cluster = "default" serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" application = "default" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" cluster = "default" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3 seata配置路径 type = "file" nacos { serverAddr = "localhost" namespace = "" group = "SEATA_GROUP" username = "" password = "" } consul { serverAddr = "127.0.0.1:8500" } apollo { appId = "seata-server" apolloMeta = "http://192.168.1.204:8801" namespace = "application" } zk { serverAddr = "127.0.0.1:2181" sessionTimeout = 6000 connectTimeout = 2000 username = "" password = "" } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } }
- 运行 windows:seata-server.bat Linux: sh seata-server.sh
3.4.代理数据源
所有需要使用分布式事务的微服务使用Seata DataSourceProxy代理数据源
/** * seata分布式事务 * 配置代理数据源 * @author: wanzenghui **/ @Configuration public class MySeataConfig { @Autowired DataSourceProperties dataSourceProperties; /** * 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚 */ @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); } }
3.5.每个微服务导入配置
- 将registry.xml、file.xml复制到每个需要使用分布式事务的微服务里
- 修改file.xml,改为每个微服务应用的名字
service { #vgroup->rgroup vgroup_mapping.gulimall-order-fescar-service-group = "default" #only support single node default.grouplist = "localhost: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" }
3.4.添加注解
主业务方法上添加@GlobalTransactional
(TM) 各远程接口添加@Transactional
(RM)
/** * 创建订单 * * @param vo 收货地址、发票信息、使用的优惠券、备注、应付总额、令牌 */ @GlobalTransactional @Transactional @Override public SubmitOrderResponseVO submitOrder(OrderSubmitVO orderSubmitVO) throws Exception { SubmitOrderResponseVO result = new SubmitOrderResponseVO();// 返回值 // 创建订单线程共享提交数据 confirmVoThreadLocal.set(orderSubmitVO); // 1.生成订单实体对象(订单 + 订单项) OrderCreateTO order = createOrder(); // 2.验价应付金额(允许0.01误差,前后端计算不一致) if (Math.abs(orderSubmitVO.getPayPrice().subtract(order.getPayPrice()).doubleValue()) >= 0.01) { // 验价不通过 throw new VerifyPriceException(); } // 验价成功 // 3.保存订单 saveOrder(order); // 4.库存锁定(wms_ware_sku) // 封装待锁定商品项TO WareSkuLockTO lockTO = new WareSkuLockTO(); lockTO.setOrderSn(order.getOrder().getOrderSn()); List<OrderItemVO> locks = order.getOrderItems().stream().map((item) -> { OrderItemVO lock = new OrderItemVO(); lock.setSkuId(item.getSkuId()); lock.setCount(item.getSkuQuantity()); lock.setTitle(item.getSkuName()); return lock; }).collect(Collectors.toList()); lockTO.setLocks(locks);// 待锁定订单项 R response = wmsFeignService.orderLockStock(lockTO); if (response.getCode() == 0) { // 锁定成功 // TODO 5.远程扣减积分 // 封装响应数据返回 result.setOrder(order.getOrder()); return result; } else { // 锁定失败 throw new NoStockException(""); } }
五、可靠消息+最终一致性
1.实现方案
方案一:订单创建失败,在抛出异常的地方发送消息到MQ,告诉库存系统解锁库存
方案二:锁定库存的时候,同时保存库存工作单(wms_ware_order_task)+库存工作单详情(wms_ware_order_task_detail),然后使用定时器扫描工作单中创建失败的订单,进行库存解锁 【使用定时器比较麻烦,采用延迟队列】
方案三:锁库存时,往延时队列发送一条库存解锁消息,30分钟后消费消息,如果订单失败则释放库存
Comments NOTHING