上一章的订单方法使用的本地事务的问题:
1. 库存服务锁定成功了,但是网络原因返回数据途中有问题怎么办---》远程服务假失败:远程服务其实成功了,由于网络故障等没有返回导致订单回滚但是库存却扣减了
2.库存服务锁定成功了,库存服务下面的逻辑发生故障,然后订单回滚了,库存是远程调用无法回滚,怎么处理--》远程服务执行完成,下面的其他方法抛出异常导致订单回滚但是库存却扣减了
本地事务:分布式系统,只能控制住自己的回滚,控制不了其他服务的回滚
分布式事务:最大原因:网络问题+分布式机器
本地事务回顾
数据库事务的基本特性:
- 原子性质:一系列操作整体不可拆分要么同时成功要么同时失败
- 一致性:数据在事务的前后业务整体一致 A有100,B有20,A给B转10,那么A就是90,B就是30
- 隔离性:事务之间隔离
- 持久性:事务一旦成功,数据一定会落盘在数据库
事务的传播行为:
如果方法a,b,c都是一个事务,其中a调用了b和c,如果b的事务传播行为是propagation=Propogation.REQUIRED 那么a和b就共用一个事务,如果c的事务传播行为是propagation=Propogation.REQUIRES_NEW那么a和c就不共用一个事务,现在在a的方法最底下出现了一个异常,b会跟着a一起回滚,但是c就不会回滚,而且b会继承a的设置,自己的设置会无效
ps:直接调用同一个类里的事务方法会导致事务失效(因为aop的原因)
声明式事务基于Spring AOP实现,将具体业务逻辑与事务处理解耦,在 Spring 的 AOP 代理下,只有目标方法由外部调用,目标方法才由 Spring 生成的代理对象来管理,这会造成自调用问,spring的@Transactional事务生效的一个前提是进行方法调用前经过拦截器TransactionInterceptor,也就是说只有通过TransactionInterceptor拦截器的方法才会被加入到spring事务管理中,如果是在同一个类中的方法调用,则不会被方法拦截器拦截到,因此事务不会起作用,必须将方法放入另一个类,并且该类通过spring注入。
解决:
- 引入aop模块依赖spring-boot-starter-aop,使用aspectJ动态代理,开启主类注解@EnableAspectJAutoProxy(exposeProxy=true)(即使没有接口也可以创建动态代理)
- 使用代理对象的本类互相调用,AoPContext.currentProxy();转为当前类类型然后使用这个对象调用类里的b,c事务方法
事务的隔离级别:
- READUNCOMMITTED:未提交的都可以读到,导致脏读问题
- READCOMMITTED:读已提交,sql server和oracle的默认隔离级别
- REPEATABLEREAD:可重复读,MySql默认隔离级别,在同一个事务里,select的结果是事务开始时间点的状态,因此,相同的select操作读到的结果会是一样的,但是会有幻读现象(InnoDB引擎可以通过next-key locks机制来避免幻读)幻读(指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的数据行)
- SERIALIZABLE:序列化,在该隔离级别下事务都是串行顺序执行的,从而避免了脏读,不可重复读和幻读问题
分布式事务
CAP定理:三个要素最多实现两点,不可能三者兼顾
- 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否是同样的值(等同于所有节点访问同一一份最新的数据副本)
- 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求(对数据更新具备高可用性)
- 分区容错性(P):大多数分布式系统都分布在多个子网络,每个子网络叫做一个区。分区容错的意思是,区间通信可能失败,比如一台服务器放在中国,另一台放在美国,这就是两个区,他们直接可能无法通行
分区容错是一定要满足的,我们只能子啊可用性和一致性中二选一
实现一致性的Raft算法:
一个节点有以下三个状态
- Leader:领导者,一个集群里只能存在一个Leader。
- Follower:跟随者,follower是被动的,一个客户端的修改数据请求如果发送到Follower上面时,会首先由Follower重定向到Leader上,
- Candidate:参与者,一个节点切换到这个状态时,将开始进行一次新的选举。
一开始都是以随从状态启动,如果一个随从没有听到领导的命令,那么他就会变成一个候选者,候选者会发起投票,只要大多数节点投票通过,他就会成为一个领导者,这个过程叫领导选举过程。领导选举过程中有两个超时时间,第一个叫选举超时时间(也叫节点自旋时间),150ms-300ms,指的是一个节点在没有听到领导之后等到成为候选者的时间。第二个叫心跳时间,在心跳的时候会同步日志信息,领导和跟随者维持联系的时间,少于节点自旋时间,这个心跳会一直维持直到另一个节点停止接收心跳,成为候选者。
如果出现了两个候选者,两个领导者会重新开始自旋,直到选举只有一个领导。
客户端给领导发送一个数据,领导在日志里保存这个数据,但是没有提交,别的节点拿不到数据,接着领导提交日志数据,随从接受到数据后设置数据进日志里后会响应领导,领导等待大多数节点的响应完成之后,领导正式提交数据,领导提交成功后告诉其他节点现在可以提交了,其他节点提交节点,到这领导才会给客户端响应保存成功,这个过程叫日志复制,如果在数据传输过程中,由于网络中断,产生了两个领导A,B,而后网络恢复,产生最后的领导A,那么有B领导的子节点的没有提交的数据会全部回滚
BASE理论
是对CAP理论的延伸,思想是即使无法保证强一致性质(CAP的C表示的是强一致),但是可以采用适当的方法实现弱一致性
- 基本可用(Basically Available): 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。
- 软状态(Soft State): 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。MySQL Replication 的异步复制也是一种体现。
- 最终一致性(Eventual Consistency): 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
强一致要求更新的数据马上就能看到,弱一致能容忍后续的部分或者全部访问不到,经过一段时间后要求访问到更新后的数据就是最终一致性
分布式事务的几种方案
2PC模式:也叫XA协议,刚性事务遵循ACID原则,性能不理想不能放在高并发的情况下使用
- 第一阶段:事务协调器要求每个涉及到事务的数据库预提交此操纵,并且反应是否可以提交
- 第二阶段:事务协调器要求每个数据库提交数据,其中如果有任何一个数据库否决此提交,那么所有数据库都会被要求回滚他们在此事务中的那部分信息
TCC事务补偿型方案:柔性事务,遵循BASE理论
把自定义的分支事务纳入到全局事务的管理中,三个阶段行为分别是:prepare行为,commit行为,rollback行为
最大努力通知型方案-柔性事务
按规律进行通知,不保证数据一定能通知成功,但是会提供可查询操作接口进行核对,这种方案常用于与第三方系统通讯时。
可靠消息+最终一致性方案-柔性事务
业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,并不是真正的发送,业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正的发送
seata环境准备
RM:资源管理器,管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或者回滚
TC:事务协调者,维护全局和分支事务的状态,驱动全局事务提交或者回滚
TM:事务管理器,定义全局事务的范围,开始全局事务,提交或者回滚全局事务
使用逻辑:
1.每个微服务要使用seata都需要一个undo_log表,并且数据库需要使用innodb引擎
-- 注意此处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;
2.在common模块里导入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
注意到seata-all的版本
3.下载seata-server的版本必须和seata-all指定的版本一致
4.更改配置文件:conf/registry.conf为nacos注册中心
5.在nacos注册中心看到seata服务器打开了就行
6.添加一个全局事务注解,@Transactional标在每一个分支事务
7.每一个想要使用分布式事务的微服务都要使用seataSourceProxy代理自己的数据源 ,该配置放置在所有想参与到分布式事务的服务里,这里我放在了ware和order里
package com.wuyimin.gulimall.order.config;
/**
* @ Author wuyimin
* @ Date 2021/8/29-11:55
* @ Description Seata代理数据源配置
*/
@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);
}
}
8.每一个微服务还需要file.config和registry.config两个文件,需要在file.config里手动添加server配置,主要 vgroupMapping.gulimall-ware-fescar-service-group = "default"需要更改成每一个模块名字,1.3以后这里vgroupMapping改成了驼峰命名要注意
service {
#vgroup->rgroup
vgroupMapping.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"
}
此外还需要配置yml文件
9.模拟事务失败
10.测试成功结果,仓库锁定操作也跟着回滚
最终一致性库存解锁逻辑
以上操作默认使用的是AT模式(基于ACID),是基于2PC的二阶提交的演变,并不适合高并发场景,同时Seata支持的TCC模式也不适合,Seata比较适合于非高并发的场景比如后台管理系统里的保存操作。为了保证高并发,库存服务自己回滚,可以发消息给库存服务。库存服务本身也可以使用自动解锁模式,这些都可以使用消息队列来实现