1. 本地事务

商品新增功能非常复杂,商品管理微服务在service层中调用保存spu和sku相关的方法,为了保证数据的一致性,必然会使用事务。

在JavaEE企业级开发的应用领域,为了保证数据的完整性和一致性,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的技术。

咱们之前玩的事务都是本地事务。所谓本地事务,是指该事务仅在当前项目内有效。

1.1. 基本概念

事务的概念:事务是逻辑上一组操作,组成这组操作各个逻辑单元,要么一起成功,要么一起失败。

事务的四个特性(ACID):

  1. 原子性(atomicity):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。

  2. 一致性(consistency):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。

  3. 隔离性(isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰

  4. 持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。

1.2. 隔离级别

事务并发引起一些读的问题:

  • 脏读:一个事务可以读取另一个事务未提交的数据

  • 不可重复读: 一个事务可以读取另一个事务已提交的数据 单条记录前后不匹配

  • 虚读(幻读: 一个事务可以读取另一个事务已提交的数据 读取的数据前后多了点或者少了点

并发写:使用mysql默认的锁机制(独占锁)

解决读问题:设置事务隔离级别

  • read uncommitted(0)

  • read committed(2)

  • repeatable read(4)

  • Serializable(8)

隔离级别越高,性能越低。

一般情况下:脏读是不可允许的,不可重复读和幻读是可以被适当允许的。

1.3. 相关命令

查看全局事务隔离级别: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

1.4. 传播行为

事务的传播行为不是jdbc规范中的定义。传播行为主要针对实际开发中的问题

1567660421239

七种传播行为:

REQUIRED 支持当前事务,如果不存在,就新建一个

SUPPORTS 支持当前事务,如果不存在,就不使用事务

MANDATORY 支持当前事务,如果不存在,抛出异常

REQUIRES_NEW 如果有事务存在,挂起当前事务,创建一个新的事务

NOT_SUPPORTED 以非事务方式运行,如果有事务存在,挂起当前事务

NEVER 以非事务方式运行,如果有事务存在,抛出异常

NESTED 如果当前事务存在,则嵌套事务执行(嵌套式事务)

  • 依赖于JDBC3.0提供的SavePoint技术

  • 删除用户 删除订单。在删除订单后,设置savePoint,执行删除用户。删除订单和删除用户在同一事务中,删除用户失败,事务回滚savePoint,由用户控制视图提交还是回滚

这七种事务传播机制最常用的就两种:

REQUIRED:一个事务,要么成功,要么失败

REQUIRES_NEW:两个不同事务,彼此之间没有关系。一个事务失败了不影响另一个事务

1.4.1. 伪代码练习

传播行为伪代码模拟:有a,b,c,d,e等5个方法,a中调用b,c,d,e方法的传播行为在小括号中标出

a(required){
	b(required);
	c(requires_new);
	d(required);
	e(requires_new);
	// a方法的业务
}

问题:

  1. a方法的业务出现异常,会怎样?a,b,d回滚 c,e不回滚
  2. d方法出现异常,会怎样?a,b,d回滚;c不回滚;e未执行
  3. e方法出现异常,会怎样?a,b,d,e回滚 c不回滚,e方法出异常会上抛影响到上级方法
  4. b方法出现异常,会怎样?a,b回滚 c,d,e未执行

加点难度:

a(required){
	b(required){
		f(requires_new);
		g(required)
	}
	c(requires_new){
		h(requires_new)
		i(required)
	}
	d(required);
	e(requires_new);
	// a方法的业务
}

问题:

  1. a方法业务出异常?a,b,g,d回滚;f,c,h,i,e不回滚
  2. e方法出异常?e,a,b,g,d回滚;f,c,h,i不回滚
  3. d方法出异常?a,b,g,d回滚;f,c,h,i不回滚;e为执行
  4. h,i方法分别出异常?h,i,c,a,b,g回滚;f不回滚;d,e未执行
  5. i方法出异常?i,c,a,b,g回滚;f,h不回滚;d,e未执行
  6. f,g方法分别出异常?f,g,b,a回滚;c,h,i,d,e未执行

1.4.2. 改造商品新增代码

现在商品保存的方法结构如下:

    @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:

1584780507279

1.4.3. 测试1:同一service + requires_new

springboot 1.x使用事务需要在引导类上添加**@EnableTransactionManagement注解开启事务支持**

springboot 2.x可直接使用**@Transactional**玩事务,传播行为默认是REQUIRED

添加事务:

1584784895102

这时,在保存商品的主方法中制造异常:

1584784987731

由于保存商品描述方法使用的是requires_new,spu应该会回滚,spu_desc应该保存成功。

清空pms_spu_desc表,再添加一个spu保存。

结果pms_spu_desc表中依然没有数据。

但是控制台打印了新增pms_spu_desc表的sql语句:

1584791120439

说明saveSpuDesc方法的事务回滚了,也就是说该方法配置的事务传播机制没有生效。

解决方案:

  1. 把service方法放到不同的service中
  2. 使用动态代理对象调用该方法

1.4.4. 测试2:不同service + requires_new

把saveSpuDesc方法放到SpuDescService中:

1584791341509

在实现类中实现该方法,可以把之前的实现copy过来:

1584791517742

改造SpuServiceImpl中保存商品的方法,调用SpuDescServiceImpl的saveSpuDesc方法:

1584791613579

再次重启gmall-pms,虽然控制台依然报错,但是数据可以保存成功,说明没有在一个事务中。

1567687774435

为什么测试1的事务传播行为没有生效,而测试2的事务传播行为生效了?

spring的事务是声明式事务,而声明式事务的本质是Spring AOP,SpringAOP的本质是动态代理。

事务要生效必须是代理对象在调用。

测试1:通过this调用同一个service中的方法,this是指service实现类对象本身,不是代理对象,就相当于方法中的代码粘到了大方法里面,相当于还是一个方法。

测试2:通过其他service对象(spuDescService)调用,这个service对象本质是动态代理对象

接下来debug,打个断点看看:

  1. spuDescService:

    1567689094127

  2. this:

    1567689136840

1.4.5. 在同一个service中使用传播行为

只需要把测试1中的this.方法名()替换成this代理对象.方法名()即可。

问题是怎么在service中获取当前类的代理对象?

在类中获取代理对象分三个步骤:

  1. 导入aop的场景依赖:spring-boot-starter-aop
  2. 开启AspectJ的自动代理,同时要暴露代理对象:@EnableAspectJAutoProxy(exposeProxy=true)
  3. 获取代理对象:SpuInfoService proxy = (SpuInfoService) AopContext.currentProxy();

具体如下:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

1567690207315

1567690368936

重启后测试:先清空pms_spu_info_desc表中数据

1567690577522

表中数据新增成功,说明saveSpuDesc方法走的是自己的事务,传播行为生效了。

debug可以看到,spuInfoService是一个代理对象。

1567690539055

1.5. 回滚策略

事务很重要的另一个特征是程序出异常时,会回滚。但并不是所有的异常都会回滚。

默认情况下的回滚策略:

  • 运行时异常:不受检异常,没有强制要求try-catch,都会回滚。例如:ArrayOutOfIndex,OutofMemory,NullPointException
  • 编译时异常:受检异常,必须处理,要么try-catch要么throws,都不回滚。例如:FileNotFoundException

可以通过@Transactional注解的下面几个属性改变回滚策略:

1567670630801

rollbackFor:指定的异常必须回滚

noRollbackFor:发生指定的异常不用回滚

1.5.1. 测试编译时异常不回滚

在商品保存方法中制造一个编译时异常:

1584791955821
重启测试,注意pms_spu表中数据:

控制台报异常:

1567691494892

pms_spu表中的数据新增成功了。

1567691611593

也就证明了编译时异常不回滚。

1.5.2. 定制回滚策略

经过刚才的测试,我们知道:

  1. ArithmeticException异常(int i = 1/0)会回滚
  2. FileNotFoundException异常(new FileInputStream(“xxxx”))不回滚

接下来我们来改变一下这个策略:

1584792095693
测试:

  1. FileNotFoundException:在程序中添加new FileInputStream(“xxxx”),然后测试。

    1567692233983

    还是id还是17,说明回滚了(回滚也会占用id=18)

  2. ArithmeticException:在程序中添加int i = 1/0; 然后测试。

    1567692364759

    id是19,说明没有回滚。

1.6. 超时事务

@Transactional注解,还有一个属性是timeout超时时间,单位是秒。

1567692523018

timeout=3:是指第一个sql开始执行到最后一个sql结束执行之间的间隔时间。

即:超时时间(timeout)是指数据库超时,不是业务超时。

改造之前商品保存方法:SpuInfoServiceImpl类中

1584792229794

重启测试:控制台出现事务超时异常

1567693383569

1.7. 只读事务

@Transactional注解最后一个属性是只读事务属性

1567693468270

如果一个方法标记为readOnly=true事务,则代表该方法只能查询,不能增删改。readOnly默认为false

给商品新增的事务标记为只读事务:

1584792311622
测试:
1567693694019

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐