商品新增功能非常复杂,商品管理微服务在service层中调用保存spu和sku相关的方法,为了保证数据的一致性,必然会使用事务。
在JavaEE企业级开发的应用领域,为了保证数据的完整性和一致性,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的技术。
咱们之前玩的事务都是本地事务。所谓本地事务,是指该事务仅在当前工程内有效。
事务的概念:事务是逻辑上一组操作,组成这组操作各个逻辑单元,要么一起成功,要么一起失败。
事务的四个特性(ACID):
事务并发引起一些读的问题:
并发写:使用mysql默认的锁机制(独占锁)
解决读问题:设置事务隔离级别
隔离级别越高,性能越低。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eV6A9wL4-1576151315959)(assets/af5b9c1e-4517-3df2-ad62-af25d1672d12.jpg)]
一般情况下:脏读是不可允许的,不可重复读和幻读是可以被适当允许的。
查看全局事务隔离级别:SELECT @@global.tx_isolation
设置全局事务隔离级别:set global transaction isolation level read committed;
查看当前会话事务隔离级别:SELECT @@tx_isolation
设置当前会话事务隔离级别:set session transaction isolation level read committed;
查看mysql默认自动提交状态:select @@autocommit
设置mysql默认自动提交状态:set autocommit = 0;【不自动提交】
开启一个事务:start transaction;
提交事务:commit
回滚事务: rollback
在事务中创建一个保存点:savepoint tx1
回滚到保存点:rollback to tx1
事务的传播行为不是jdbc规范中的定义。传播行为主要针对实际开发中的问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WJqMJBQ5-1576151316049)(assets/1567660421239.png)]
七种传播行为:
REQUIRED 支持当前事务,如果不存在,就新建一个
SUPPORTS 支持当前事务,如果不存在,就不使用事务
MANDATORY 支持当前事务,如果不存在,抛出异常
REQUIRES_NEW 如果有事务存在,挂起当前事务,创建一个新的事务
NOT_SUPPORTED 以非事务方式运行,如果有事务存在,挂起当前事务
NEVER 以非事务方式运行,如果有事务存在,抛出异常
NESTED 如果当前事务存在,则嵌套事务执行(嵌套式事务)
这七种事务传播机制最常用的就两种:
REQUIRED:一个事务,要么成功,要么失败
REQUIRES_NEW:两个不同事务,彼此之间没有关系。一个事务失败了不影响另一个事务
传播行为伪代码模拟:有a,b,c,d,e等5个方法,a中调用b,c,d,e方法的传播行为在小括号中标出
a(required){
b(required);
c(requires_new);
d(required);
e(requires_new);
// a方法的业务
}
问题:
加点难度:
a(required){
b(required){
f(requires_new);
g(required)
}
c(requires_new){
h(requires_new)
i(required)
}
d(required);
e(requires_new);
// a方法的业务
}
问题:
现在商品保存的方法结构如下:
/**
* 保存商品信息
* @param spuInfoVO
*/
@Override
public void saveSpuInfoVO(SpuInfoVO spuInfoVO) {
/// 1.保存spu相关
// 1.1. 保存spu基本信息 spu_info
this.saveSpuInfo(spuInfoVO);
// 1.2. 保存spu的描述信息 spu_info_desc
this.saveSpuDesc(spuInfoVO);
// 1.3. 保存spu的规格参数信息
this.saveBaseAttrs(spuInfoVO);
/// 2. 保存sku相关信息
this.saveSkuInfoWithSaleInfo(spuInfoVO);
}
/**
* 保存sku相关信息及营销信息
* @param spuInfoVO
*/
private void saveSkuInfoWithSaleInfo(SpuInfoVO spuInfoVO) { 。。。 }
/**
* 保存spu基本属性信息
* @param spuInfoVO
*/
private void saveBaseAttrs(SpuInfoVO spuInfoVO) { 。。。 }
/**
* 保存spu描述信息(图片)
* @param spuInfoVO
*/
private void saveSpuDesc(SpuInfoVO spuInfoVO) { 。。。 }
/**
* 保存spu基本信息
* @param spuInfoVO
*/
private void saveSpuInfo(SpuInfoVO spuInfoVO) { 。。。 }
为了测试事务传播行为,我们在SpuInfoService接口中把saveSkuInfoWithSaleInfo、saveBaseAttrs、saveSpuDesc、saveSpuInfo声明为service接口方法。
public interface SpuInfoService extends IService<SpuInfoEntity> {
PageVo queryPage(QueryCondition params);
PageVo querySpuInfo(QueryCondition condition, Long catId);
void saveSpuInfoVO(SpuInfoVO spuInfoVO);
void saveSkuInfoWithSaleInfo(SpuInfoVO spuInfoVO);
void saveBaseAttrs(SpuInfoVO spuInfoVO);
void saveSpuDesc(SpuInfoVO spuInfoVO);
void saveSpuInfo(SpuInfoVO spuInfoVO);
}
再把SpuInfoServiceImpl实现类的对应方法改成public:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qIGhsq3W-1576151316060)(assets/1567682780936.png)]
springboot 1.x使用事务需要在引导类上添加@EnableTransactionManagement注解开启事务支持
springboot 2.x可直接使用@Transactional玩事务,传播行为默认是REQUIRED
添加事务:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0nSmoslA-1576151316065)(assets/1567683889606.png)]
这时,在保存商品的主方法中制造异常:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DVCa7iYJ-1576151316070)(assets/1567684193546.png)]
商品描述信息是否可以保存成功?
清空pms_spu_info_desc表,再添加一个spu保存。
结果pms_spu_info_desc表中依然没有数据。
控制台打印了新增pms_spu_info_desc表的sql语句:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0bbyv3SS-1576151316075)(assets/1567684800757.png)]
说明saveSpuDesc方法的事务回滚了,也就是说该方法配置的事务传播机制没有生效。
解决方案:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-foTRPcQF-1576151316080)(assets/1567688566455.png)]
把saveSpuDesc方法放到SpuInfoDescService中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MJqW6EFd-1576151316086)(assets/1567687357987.png)]
在实现类中实现该方法,可以把之前的实现copy过来:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IuzzwWoi-1576151316091)(assets/1567687448560.png)]
改造SpuInfoServiceImpl中保存商品的方法,调用SpuInfoDescServiceImpl的saveSpuDesc方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u2nUG0Ge-1576151316094)(assets/1567687644137.png)]
再次重启gmall-pms,虽然控制台依然报错,但是数据可以保存成功,说明没有在一个事务中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HHp46xWF-1576151316098)(assets/1567687774435.png)]
为什么测试1的事务传播行为没有生效,而测试2的事务传播行为生效了?
spring的事务是声明式事务,而声明式事务的本质是Spring AOP,SpringAOP的本质是动态代理。
事务要生效必须是代理对象在调用。
测试1:通过this调用同一个service中的方法,this是指service实现类对象本身,不是代理对象,就相当于方法中的代码粘到了大方法里面,相当于还是一个方法。
测试2:通过其他service对象(spuInfoDescService)调用,这个service对象本质是动态代理对象
接下来debug,打个断点看看:
spuInfoDescService:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V1Xi9hvb-1576151316105)(assets/1567689094127.png)]
this:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oGO3l7l9-1576151316113)(assets/1567689136840.png)]
只需要把测试1中的this.方法名()
替换成this代理对象.方法名()
即可。
问题是怎么在service中获取当前类的代理对象?
在类中获取代理对象分三个步骤:
具体如下:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jnPmIZRo-1576151316122)(assets/1567690207315.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oygEEv7J-1576151316138)(assets/1567690368936.png)]
重启后测试:先清空pms_spu_info_desc表中数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4PzabOPA-1576151316141)(assets/1567690577522.png)]
表中数据新增成功,说明saveSpuDesc方法走的是自己的事务,传播行为生效了。
debug可以看到,spuInfoService是一个代理对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LPk54PSy-1576151316146)(assets/1567690539055.png)]
事务很重要的另一个特征是程序出异常时,会回滚。但并不是所有的异常都会回滚。
默认情况下的回滚策略:
可以通过@Transactional注解的下面几个属性改变回滚策略:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YpVQsAYk-1576151316151)(assets/1567670630801.png)]
rollbackFor:指定的异常必须回滚
noRollbackFor:发生指定的异常不用回滚
在商品保存方法中制造一个编译时异常:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NDwU8WYO-1576151316155)(assets/1567691455176.png)]
重启测试,注意pms_spu_info表中数据:
控制台报异常:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kt9DYkES-1576151316163)(assets/1567691494892.png)]
pms_spu_info表中的数据新增成功了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HcokEGHz-1576151316168)(assets/1567691611593.png)]
也就证明了编译时异常不回滚。
经过刚才的测试,我们知道:
接下来我们来改变一下这个策略:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2PTwV9hB-1576151316174)(assets/1567692066724.png)]
测试:
FileNotFoundException:在程序中添加new FileInputStream(“xxxx”),然后测试。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6VcQZkOR-1576151316179)(assets/1567692233983.png)]
还是id还是17,说明回滚了(回滚也会占用id=18)
ArithmeticException:在程序中添加int i = 1/0; 然后测试。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3e1X9Fae-1576151316183)(assets/1567692364759.png)]
id是19,说明没有回滚。
@Transactional注解,还有一个属性是timeout超时时间,单位是秒。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jHD5ETsM-1576151316185)(assets/1567692523018.png)]
timeout=3:是指第一个sql开始执行到最后一个sql结束执行之间的间隔时间。
即:超时时间(timeout)是指数据库超时,不是业务超时。
改造之前商品保存方法:SpuInfoServiceImpl类中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y1rZC64J-1576151316205)(assets/1567692831955.png)]
重启测试:控制台出现事务超时异常
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MWhI2JMn-1576151316209)(assets/1567693383569.png)]
@Transactional注解最后一个属性是只读事务属性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EzlZS7WP-1576151316212)(assets/1567693468270.png)]
如果一个方法标记为readOnly=true事务,则代表该方法只能查询,不能增删改。readOnly默认为false
给商品新增的事务标记为只读事务:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v1yW3fcL-1576151316214)(assets/1567693732724.png)]
测试:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GG8GhzGC-1576151316215)(assets/1567693694019.png)]
商品管理微服务在service层中除了保存spu和sku相关的方法,又远程调用gmall-sms的保存接口,并在gmall-sms的service层中调用了保存营销信息的相关方法。如果保存营销信息的方法出现异常,会怎样?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V27WaPIZ-1576151316218)(/assets/1567647212521.png)]
传统的一个工程内为了保证数据的一致性,使用本地事务。本地事务只能解决同一工程中的事务问题,而现在的场景更加复杂,关系到两个工程模块,怎么保证要么都成功,要么都失败?
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。
本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v7pKP5dn-1576151316221)(assets/1567696219358.png)]
分布式事务产生的原因:
分布式系统异常除了本地事务那些异常之外,还有:机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失…
数据库的 ACID 四大特性,已经无法满足我们分布式事务,这个时候又有一些新的大佬提出一些新的理论。
分布式存储系统的CAP原理(分布式系统的三个指标):
Consistency(一致性):在分布式系统中的所有数据备份,在同一时刻是否同样的值。
对于数据分布在不同节点上的数据来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。
Availability(可用性):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(要求数据需要备份)
Partition tolerance(分区容错):大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。
CAP理论就是说在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们无法避免的。所以我们只能在一致性和可用性之间进行权衡,没有系统能同时保证这三点。要么选择CP、要么选择AP。
问题:zookeeper分布式协调组件 是 CP 还是 AP
动画:http://thesecretlivesofdata.com/raft/
我们的妥协:BASE
BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。接下来看看BASE中的三要素:
Basically Available(基本可用)
基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。
Soft state(软状态)
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。
Eventually consistent(最终一致性)
最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
BASE模型是传统ACID模型的反面,不同于ACID,BASE强调牺牲高一致性,从而获得可用性,数据允许在一段时间内的不一致,只要保证最终一致就可以了。
分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免。
主流的解决方案如下:
数据库支持的2PC【2 Phase Commit】,又叫做 XA Transactions。
MySQL从5.5版本开始支持,SQL Server 2005 开始支持,Oracle 7 开始支持。
其中,XA 是一个两阶段提交协议,该协议分为以下两个阶段:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-szalJ1ZI-1576151316224)(assets/913887-20160328134232723-1604465391.png)]
第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交.
第二阶段:事务协调器要求每个数据库提交数据。
其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。
可用率 a%*b% = 99%*99%=98%
总的来说,XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。但是,XA也有致命的缺点,那就是性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景。
另外:XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录prepare阶段日志,主备切换会导致主库与备库数据不一致。许多nosql也没有支持XA,这让XA的应用场景变得非常狭隘。
最后:存在事务管理器的单点故障问题
对应的开源框架:atomikos
是一种编程式分布式事务解决方案。
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。TCC模式要求从服务提供三个接口:Try、Confirm、Cancel。
整个TCC业务分成两个阶段完成:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VeHEASVB-1576151316225)(assets/434101-20180414152822741-1232436610.png)]
第一阶段:主业务服务分别调用所有从业务的try操作,并在活动管理器中登记所有从业务服务。当所有从业务服务的try操作都调用成功或者某个从业务服务的try操作失败,进入第二阶段。
第二阶段:活动管理器根据第一阶段的执行结果来执行confirm或cancel操作。如果第一阶段所有try操作都成功,则活动管理器调用所有从业务活动的confirm操作。否则调用所有从业务服务的cancel操作。
举个例子,假如 Bob 要向 Smith 转账100元,思路大概是:
我们有一个本地方法,里面依次调用
缺点:
不少大公司里,其实都是自己研发 TCC 分布式事务框架的,专门在公司内部使用。国内开源出去的:ByteTCC,TCC-transaction,Himly。
基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oioDb5t9-1576151316228)(assets/1567702580183.png)]
虽然上面的方案能够完成A和B的操作,但是A和B并不是严格一致的,而是最终一致的,我们在这里牺牲了一致性,换来了性能的大幅度提升。当然,这种玩法也是有风险的,如果B一直执行不成功,那么一致性会被破坏,具体要不要玩,还是得看业务能够承担多少风险。
适用于高并发最终一致
低并发基本一致:二阶段提交
高并发强一致:没有解决方案
seata:Simple Extensible Autonomous Transaction Architecture
官方:https://github.com/seata/seata
中文wiki:https://github.com/seata/seata
2019 年 1 月,阿里巴巴中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback),和社区一起共建开源分布式事务解决方案。Fescar 的愿景是让分布式事务的使用像本地事务的使用一样,简单和高效,并逐步解决开发者们遇到的分布式事务方面的所有难题。
Fescar 开源后,蚂蚁金服加入 Fescar 社区参与共建,并在 Fescar 0.4.0 版本中贡献了 TCC 模式。
为了打造更中立、更开放、生态更加丰富的分布式事务开源社区,经过社区核心成员的投票,大家决定对 Fescar 进行品牌升级,并更名为 Seata,意为:Simple Extensible Autonomous Transaction Architecture,是一套一站式分布式事务解决方案。
Seata 融合了阿里巴巴和蚂蚁金服在分布式事务技术上的积累,并沉淀了新零售、云计算和新金融等场景下丰富的实践经验,但要实现适用于所有的分布式事务场景的愿景,仍有很长的路要走。因此,我们决定建立一个完全中立的分布式事务组织,希望更多的企业、开发者能够加入我们,一起打造 Seata。
历史:
Ant Financial
XTS:Extended Transaction Service,可扩展事务服务。蚂蚁金服中间件团队自2007年以来开发了分布式事务中间件,广泛应用于Ant Financial,解决了跨数据库和服务的数据一致性问题。
DTX:Distributed Transaction Extended。自2013年以来,XTS已在Ant Financial Cloud上发布,名称为DTX。
阿里巴巴
TXC:Taobao Transaction Constructor。阿里巴巴中间件团队自2014年起启动该项目,以解决因应用程序架构从单片机改为微服务而导致的分布式事务问题。
GTS:Global Transaction Service。 TXC作为Aliyun中间件产品,新名称GTS自2016年起发布。
Fescar:我们从2019年开始基于TXC / GTS开源开源项目Fescar,以便在未来与社区密切合作。
Seata社区
Seata:简单的可扩展自治交易架构。 Ant Financial加入Fescar,使其成为一个更加中立和开放的分布式服务社区,并将Fescar更名为Seata。
Seata有3个基本组件:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yyn0IWGR-1576151316232)(assets/68747470733a2f2f63646e2e6e6c61726b2e636f6d2f6c61726b2f302f323031382f706e672f31383836322f313534353031333931353238362d34613930663064662d356664612d343165312d393165302d3261613364333331633033352e706e67.png)]
全局事务与分支事务:
a Distributed Transaction is a Global Transaction which is made up with a batch of Branch Transaction, and normally Branch Transaction is just Local Transaction.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rtw8Smwi-1576151316234)(assets/1567735232409.png)]
Seata管理分布式事务的典型生命周期:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hrhxe6bD-1576151316236)(assets/seata_procees.png)]
至此,seata的协议机制总体上看与 XA 是一致的。但是是有差别的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oEmibjRr-1576151316239)(assets/seata-xa.png)]
XA 方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身(通过提供支持 XA 的驱动程序来供应用使用)。
而 Fescar 的 RM 是以二方包的形式作为中间件层部署在应用程序这一侧的,不依赖于数据库本身对协议的支持,当然也不需要数据库支持 XA 协议。这点对于微服务化的架构来说是非常重要的:应用层不需要为本地事务和分布式事务两类不同场景来适配两套不同的数据库驱动。
这个设计,剥离了分布式事务方案对数据库在 协议支持 上的要求。
springCloud整合seata:https://github.com/seata/seata-samples/tree/master/springcloud-jpa-seata
官方提供了案例工程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vc6ovJ0p-1576151316242)(assets/1567742601372.png)]
案例工程结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PWRxdhDY-1576151316245)(assets/seata-springcloud-sample.png)]
接下来,把案例工程下载下来,根据管方案例学习seata该怎么使用。
或者使用课前资料中下载好的seata案例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tpM1m0m0-1576151316247)(assets/1567743001798.png)]
解压后把springcloud-jpa-seata工程,导入eclipse
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IYuSjtOb-1576151316250)(assets/1567743133707.png)]
导入后:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fRaMeeZf-1576151316254)(assets/1567743542465.png)]
怎么启动这些工程,官方也给出文档:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m4lcLsSs-1576151316255)(assets/1567743654040.png)]
把压缩包下springcloud-jpa-seata/sql目录下的sql文件导入数据库
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yjyhXb1V-1576151316257)(assets/1567744673843.png)]
导入后,有案例工程微服务对应的3个数据库:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rEpghtV8-1576151316260)(assets/1567744814798.png)]
下载地址:https://github.com/seata/seata/releases
自行下载,或者使用课前资料中提供:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ipIlMqTj-1576151316262)(assets/1567745017532.png)]
先修改conf下的配置文件(registry.conf):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pTt0Rj9J-1576151316266)(assets/1567745781488.png)]
修改内容如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y3BF2DYQ-1576151316269)(assets/1567745909207.png)]
切换到bin目录下,双击启动即可
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zQERZRvV-1576151316273)(assets/1567745207857.png)]
先修改各个微服务的application.properties中有关数据库连接信息的配置,例如:storage-service
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X4mMqUy4-1576151316276)(assets/1567751459520.png)]
spring.datasource.url=jdbc:mysql://172.16.116.100:3306/db_storage?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
其他微服务,请自行修改。注意:url中的数据库名要对应。
然后,依次启动四个微服务。
依据官方文档,进行测试:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F6KJYD8u-1576151316277)(assets/1567751856641.png)]
测试前,观察数据库:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bO9E6Ai0-1576151316278)(assets/1567752177296.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mn4lEd2T-1576151316281)(assets/1567752217418.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GuS8ZL7b-1576151316286)(assets/1567752259127.png)]
正常提交情况:
在浏览器中输入:http://127.0.0.1:8084/purchase/commit
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7E2RNu3B-1576151316287)(assets/1567752001898.png)]
测试结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zvPDJ6zU-1576151316290)(assets/1567752312314.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wf22ljTd-1576151316293)(assets/1567752668656.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mHIzocNY-1576151316296)(assets/1567752484335.png)]
business-service源码,其实就是1001账户花了5金钱购买了2001物品。前后数据一致
异常回滚情况:
在浏览器访问:http://127.0.0.1:8084/purchase/rollback
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wIdcnfLX-1576151316297)(assets/1567752825796.png)]
查看数据库,会发现各个表数据不变!!
查看控制台,account-service、order-service、business-service报错!这是因为business-service的rollback请求的业务(BusinessController)是这样的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qwSd1hBP-1576151316298)(assets/1567752967167.png)]
在account-service的AccountService中判断账号是否是1002,是就抛出运行时异常。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WpMCT1GA-1576151316300)(assets/1567753059865.png)]
总结流程:浏览器访问/rollback --> business-service的Controller让1002账户买东西 --> order-service创建订单,并远程调用1002的账户信息扣款 --> account-service扣款,并在最后制造一个运行时异常。
导致:account-service异常 --> 调用方order-service异常 --> 调用方business-service异常。
但是:storage-service执行正常。
由于business-service使用了分布式事务,所以storage-service数据回滚。最后都没有数据更新
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RIAcxee6-1576151316304)(assets/1567753455081.png)]
如果,把@GlobalTransactional注解去掉或者改成@Transactional注解会怎么样?
自行测试一下。肯定会出现分布式事务问题:即库存会被正常扣掉
启示玩任何技术,无非就是三个东西:
接下来以storage-service为例,总结一下seata的玩法。
每个数据库都要有一张undo_log日志表,sql如下:
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `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=2 DEFAULT CHARSET=utf8;
引入了两个和seata有关的依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-seataartifactId>
<version>2.0.0.RELEASEversion>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>0.8.0version>
dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-09Qz2D7M-1576151316306)(assets/1567754876355.png)]
application.properties:就是一些常规配置,可略过
还剩下3个配置文件:
该文件包含两部分配置:
注册中心:
registry { # 注册中心配置
# 可选项:file 、nacos 、eureka、redis、zk
type = "nacos" # 指定nacos注册中心,默认是file。由于项目整体使用nacos,所以后续选择nacos
nacos {
serverAddr = "127.0.0.1:8848"
namespace = "public"
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:1001/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6381"
db = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
file {
name = "file.conf"
}
}
配置中心
config { # 配置中心
# 可选项:file、nacos 、apollo、zk
type = "file" # 指向file配置中心,也可以指向nacos等其他注册中心
nacos {
serverAddr = "localhost"
namespace = "public"
cluster = "default"
}
apollo {
app.id = "fescar-server"
apollo.meta = "http://192.168.1.204:8801"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
file {
name = "file.conf" # 通过file.conf配置seata参数,指向第二个配置文件
}
}
该文件的命名取决于registry.conf配置中心的配置
由于registry.conf中配置的是
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0YmrWJxM-1576151316307)(assets/1567757677307.png)]
也就是说:file.conf文件名取决于registry的配置中心配置,如果registry配置的配置中心不是file,可以没有改文件。例如:如果配置中心是nacos,这是file.conf文件就不需要了,把file.conf文件内容交给nacos就可
网络传输配置:
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
#thread factory for netty
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-prefix = "NettyServerBizHandler"
share-boss-worker = false
client-selector-thread-prefix = "NettyClientSelector"
client-selector-thread-size = 1
client-worker-thread-prefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
boss-thread-size = 1
#auto default pin or 8
worker-thread-size = 8
}
}
*事务日志存储配置:该部分内容参照account-service中的file.conf配置
store {
## store mode: file、db
mode = "file" # 存储方式file、db
## file store
file {
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
max-branch-session-size = 16384
# globe session size , if exceeded throws exceptions
max-global-session-size = 512
# file buffer size , if exceeded allocate new buffer
file-write-buffer-cache-size = 16384
# when recover batch read size
session.reload.read_size = 100
# async, sync
flush-disk-mode = async
}
## database store
db {
driver_class = ""
url = ""
user = ""
password = ""
}
}
*当前微服务在seata服务器中注册的信息配置:
service {
#vgroup->rgroup
#必须和服务名一致:${spring.applicaiton.name}
vgroup_mapping.${spring.application.name}-fescar-service-group = "default"
#only support single node
default.grouplist = "127.0.0.1:8091" #seata-server服务器地址,默认是8091
#degrade current not support
enableDegrade = false
#disable
disable = false
}
客户端相关工作的机制
client {
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
}
}
每一个微服务原来自己的数据源都必须使用DataSourceProxy代理,这样seata才能掌控所有事务。
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
/**
* 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚
*
* @param druidDataSource The DruidDataSource
* @return The default datasource
*/
@Primary
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
主业务方法添加全局事务:@GlobalTransactional
分支业务方法添加本地事务注解:@Transactional
给每个数据库添加undo_log日志表:
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `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=2 DEFAULT CHARSET=utf8;
例如:guli_pms
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bweI80bY-1576151316311)(assets/1567759877276.png)]
在pom.xml中同一引入seata的依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-seataartifactId>
<version>2.0.0.RELEASEversion>
dependency>
<dependency>
<groupId>io.seatagroupId>
<artifactId>seata-allartifactId>
<version>0.8.0version>
dependency>
添加配置文件:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A0839hxN-1576151316314)(assets/1567773401161.png)]
registry.conf内容如下:
registry {
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = "public"
cluster = "default"
}
}
config {
type = "file"
file {
name = "file.conf"
}
}
file.conf内容如下:
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
#thread factory for netty
thread-factory {
boss-thread-prefix = "NettyBoss"
worker-thread-prefix = "NettyServerNIOWorker"
server-executor-thread-prefix = "NettyServerBizHandler"
share-boss-worker = false
client-selector-thread-prefix = "NettyClientSelector"
client-selector-thread-size = 1
client-worker-thread-prefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
boss-thread-size = 1
#auto default pin or 8
worker-thread-size = 8
}
}
## transaction log store
store {
## store mode: file、db
mode = "db" ## 改成db模式
## database store db相关参数根据个人环境设置
db {
driver_class = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://172.16.116.100:3306/guli-pms"
user = "root"
password = "root"
}
}
service {
#vgroup->rgroup
# 根据工程的服务名修改
vgroup_mapping.pms-service-fescar-service-group = "default"
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
}
client {
async.commit.buffer.limit = 10000
lock {
retry.internal = 10
retry.times = 30
}
}
DataSourceConfig配置:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iXYiOJh1-1576151316317)(assets/1567775140238.png)]
内容如下:
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource hikariDataSource(@Value("${spring.datasource.url}")String url) {
HikariDataSource hikariDataSource = new HikariDataSource();
hikariDataSource.setJdbcUrl(url);
return hikariDataSource;
}
/**
* 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚
*
* @param hikariDataSource The HikariDataSource
* @return The default datasource
*/
@Primary
@Bean("dataSource")
public DataSource dataSource(DataSource hikariDataSource) {
return new DataSourceProxy(hikariDataSource);
}
}
SpuInfoServiceImpl实现类保存商品信息的主方法:
/**
* 保存商品信息
* @param spuInfoVO
*/
@GlobalTransactional
@Override
public void saveSpuInfoVO(SpuInfoVO spuInfoVO) throws FileNotFoundException {
/// 1.保存spu相关
// 1.1. 保存spu基本信息 spu_info
this.saveSpuInfo(spuInfoVO);
// 1.2. 保存spu的描述信息 spu_info_desc
this.spuInfoDescService.saveSpuDesc(spuInfoVO);
// 1.3. 保存spu的规格参数信息
this.saveBaseAttrs(spuInfoVO);
/// 2. 保存sku相关信息
this.saveSkuInfoWithSaleInfo(spuInfoVO);
// 最后制造异常
int i = 1 / 0;
}
其他所有方法包括SpuInfoDescServiceImpl中的方法都只加普通注解:@Transactional
pom.xml的依赖、file.conf、registry.conf、DataSourceConfig等等直接从gmall-pms拷贝过来即可
只需要把registry.conf中的如下内容修改一下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RCA6Eczp-1576151316319)(assets/1567775543491.png)]
然后在业务方法上添加@Transactional注解即可。
一旦我们使用注册中心,进行服务的发现,seata服务器也得配置放在注册中心。
修改registry.conf配置文件,把seata-server事务协调管理器也注册到nacos注册中心
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Aa1nxt3D-1576151316321)(assets/1567775739170.png)]
如下内容修改一下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9BcSYvyc-1576151316325)(assets/1567775909128.png)]
回到bin目录下,重新启动seata-server。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pANkBsh3-1576151316327)(assets/1567770750119.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MBhL2jZL-1576151316328)(assets/1567770815333.png)]