商品新增功能非常复杂,商品管理微服务在service层中调用保存spu和sku相关的方法,为了保证数据的一致性,必然会使用事务。
在JavaEE企业级开发的应用领域,为了保证数据的完整性和一致性,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的技术。
咱们之前玩的事务都是本地事务。所谓本地事务,是指该事务仅在当前项目内有效。
事务的概念:事务是逻辑上一组操作,组成这组操作各个逻辑单元,要么一起成功,要么一起失败。
事务的四个特性(ACID):
事务并发引起一些读的问题:
并发写:使用mysql默认的锁机制(独占锁)
解决读问题:设置事务隔离级别
隔离级别越高,性能越低。
一般情况下:脏读是不可允许的,不可重复读和幻读是可以被适当允许的。
查看全局事务隔离级别: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规范中的定义。传播行为主要针对实际开发中的问题
七种传播行为:
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方法的业务 }
问题:
现在商品保存的方法结构如下:
@Override public void bigSave(SpuVo spuVo) { /// 1.保存spu相关 // 1.1. 保存spu基本信息 spu_info Long spuId = saveSpu(spuVo); // 1.2. 保存spu的描述信息 spu_info_desc saveSpuDesc(spuVo, spuId); // 1.3. 保存spu的规格参数信息 saveBaseAttr(spuVo, spuId); /// 2. 保存sku相关信息 saveSku(spuVo, spuId); } /** * 保存sku相关信息及营销信息 * @param spuInfoVO */ private void saveSku(SpuVo spuVo, Long spuId) { 。。。 } /** * 保存spu基本属性信息 * @param spuInfoVO */ private void saveBaseAttr(SpuVo spuVo, Long spuId) { 。。。 } /** * 保存spu描述信息(图片) * @param spuInfoVO */ private void saveSpuDesc(SpuVo spuVo, Long spuId) { 。。。 } /** * 保存spu基本信息 * @param spuInfoVO */ private void saveSpu(SpuVo spuVo) { 。。。 }
为了测试事务传播行为,我们在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 saveSku(SpuVo spuVo, Long spuId); void saveBaseAttr(SpuVo spuVo, Long spuId); void saveSpuDesc(SpuVo spuVo, Long spuId); Long saveSpu(SpuVo spuVo); }
再把SpuInfoServiceImpl实现类的对应方法改成public:
springboot 1.x使用事务需要在引导类上添加**@EnableTransactionManagement注解开启事务支持**
springboot 2.x可直接使用**@Transactional**玩事务,传播行为默认是REQUIRED
添加事务:
这时,在保存商品的主方法中制造异常:
由于保存商品描述方法使用的是requires_new,spu应该会回滚,spu_desc应该保存成功。
清空pms_spu_desc表,再添加一个spu保存。
结果pms_spu_desc表中依然没有数据。
但是控制台打印了新增pms_spu_desc表的sql语句:
说明saveSpuDesc方法的事务回滚了,也就是说该方法配置的事务传播机制没有生效。
解决方案:
把service方法放到不同的service中使用动态代理对象调用该方法
把saveSpuDesc方法放到SpuDescService中:
在实现类中实现该方法,可以把之前的实现copy过来:
改造SpuServiceImpl中保存商品的方法,调用SpuDescServiceImpl的saveSpuDesc方法:
再次重启gmall-pms,虽然控制台依然报错,但是数据可以保存成功,说明没有在一个事务中。
为什么测试1的事务传播行为没有生效,而测试2的事务传播行为生效了?
spring的事务是声明式事务,而声明式事务的本质是Spring AOP,SpringAOP的本质是动态代理。
事务要生效必须是代理对象在调用。
测试1:通过this调用同一个service中的方法,this是指service实现类对象本身,不是代理对象,就相当于方法中的代码粘到了大方法里面,相当于还是一个方法。
测试2:通过其他service对象(spuDescService)调用,这个service对象本质是动态代理对象
接下来debug,打个断点看看:
spuDescService:
this:
只需要把测试1中的this.方法名()
替换成this代理对象.方法名()
即可。
问题是怎么在service中获取当前类的代理对象?
在类中获取代理对象分三个步骤:
具体如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
重启后测试:先清空pms_spu_info_desc表中数据
表中数据新增成功,说明saveSpuDesc方法走的是自己的事务,传播行为生效了。
debug可以看到,spuInfoService是一个代理对象。
事务很重要的另一个特征是程序出异常时,会回滚。但并不是所有的异常都会回滚。
默认情况下的回滚策略:
可以通过@Transactional注解的下面几个属性改变回滚策略:
在商品保存方法中制造一个编译时异常:
重启测试,注意pms_spu表中数据:
控制台报异常:
pms_spu表中的数据新增成功了。
也就证明了编译时异常不回滚。
经过刚才的测试,我们知道:
ArithmeticException异常(int i = 1/0)会回滚FileNotFoundException异常(new FileInputStream(“xxxx”))不回滚
接下来我们来改变一下这个策略:
测试:
FileNotFoundException:在程序中添加new FileInputStream(“xxxx”),然后测试。
还是id还是17,说明回滚了(回滚也会占用id=18)
ArithmeticException:在程序中添加int i = 1/0; 然后测试。
id是19,说明没有回滚。
@Transactional注解,还有一个属性是timeout超时时间,单位是秒。
timeout=3:是指第一个sql开始执行到最后一个sql结束执行之间的间隔时间。
即:超时时间(timeout)是指数据库超时,不是业务超时。
改造之前商品保存方法:SpuInfoServiceImpl类中
重启测试:控制台出现事务超时异常
@Transactional注解最后一个属性是只读事务属性
如果一个方法标记为readOnly=true事务,则代表该方法只能查询,不能增删改。readOnly默认为false
给商品新增的事务标记为只读事务:
测试: