Spring事务

3/25/2022 事务Spring

# 事务的分类?

# 编程式事务管理

编程式事务管理是侵入性事务管理,使用TransactionTemplate或者直接使用PlatformTransactionManager,对于编程式事务管理,Spring推荐使用TransactionTemplate。

# 声明式事务管理

声明式事务管理建立在AOP之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。

img

声明式事务执行流程:

  1. @Transactional注解可以使用在方法或者类上面,在Spring IoC 容器初始化时, Spring 会读入这个注解或者则XML配置的事务信息,并且保存到一个事务定义类里面( TransactionDefinition 接口的子类),以备将来使用。

  2. 当运行时会让Spring 拦截注解标注的某一个方法或者类的所有方法。谈到了拦截,可能会想到AOP, Spring 也是如此。有了AOP 的概念,那么它就会把你编写的代码织入到AOP 的流程中,然后给出它的约定。

  3. 首先Spring 通过事务管理器( PlatformTransactionManager 的子类)创建事务,与此同时会把事务定义中的隔离级别、超时时间等属性根据配置内容往事务上设置。而根据传播行为配置采取一种特定的策略,后面会谈到传播行为的使用问题,这是Spring 根据配置完成的内容,你只需要配置,无须编码。

  4. 然后,启动开发者提供的业务代码,我们知道Spring会通过反射的方式调度开发者的业务代码,但是反射的结果可能是正常返回或者产生异常返回,那么它给的约定是只要发生异常,井且符合事务定义类回滚条件的, Spring 就会将数据库事务回滚,否则将数据库事务提交,这也是Spring 自己完成的。

  5. 你会惊奇地发现,在整个开发过程中,只需要编写业务代码和对事务属性进行配置就可以了,井不需要使用代码干预,工作量比较少,代码逻辑也更为清晰,更有利于维护。

# 比较

  1. 编程式事务每次实现都要单独实现,但业务量大功能复杂时,使用编程式事务无疑是痛苦的,而声明式事务不同,声明式事务属于无侵入式,不会影响业务逻辑的实现,只需要在配置文件中做相关的事务规则声明或者通过注解的方式,便可以将事务规则应用到业务逻辑中

  2. 显然声明式事务管理要优于编程式事务管理,这正是Spring倡导的非侵入式的编程方式。唯一不足的地方就是声明式事务管理的粒度是方法级别,而编程式事务管理是可以到代码块的,但是可以通过提取方法的方式完成声明式事务管理的配置

# 事务的传播机制

支持当前事务的情况:

  • **TransactionDefinition.PROPAGATION_REQUIRED:**如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。

  • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行

  • **TransactionDefinition.PROPAGATION_MANDATORY:**如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)

不支持当前事务的情况:

  • **TransactionDefinition.PROPAGATION_REQUIRES_NEW:**创建一个新的事务,如果当前存在事务,则把当前事务挂起。

  • **TransactionDefinition.PROPAGATION_NOT_SUPPORTED:**以非事务方式运行,如果当前存在事务,则把当前事务挂起。

  • **TransactionDefinition.PROPAGATION_NEVER:**以非事务方式运行,如果当前存在事务,则抛出异常。

其他情况:

  • TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

事务的传播性一般用在事务嵌套的场景,比如一个事务方法里面调用了另外一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,这就是需要事务传播机制的配置来确定怎么样执行。

常用的事务传播机制如下:

  • PROPAGATION_REQUIRED **Spring默认的传播机制,**能满足绝大部分业务需求,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行.

  • PROPAGATION_REQUES_NEW 该事务传播机制是每次都会新开启一个事务,同时把外层事务挂起,当当前事务执行完毕,恢复上层事务的执行。如果外层没有事务,执行当前新开启的事务即可

  • PROPAGATION_SUPPORT 如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行。完全依赖外层的事务

  • PROPAGATION_NOT_SUPPORT 该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码

  • PROPAGATION_NEVER 该传播机制不支持外层事务,即如果外层有事务就抛出异常

  • PROPAGATION_MANDATORY 与NEVER相反,如果外层没有事务,则抛出异常

  • PROPAGATION_NESTED 该传播机制的特点是可以保存状态保存点,当前事务回滚到某一个点,从而避免所有的嵌套事务都回滚,即各自回滚各自的,如果子事务没有把异常吃掉,基本还是会引起全部回滚的

传播规则回答了这样一个问题:一个新的事务应该被启动还是被挂起,或者是一个方法是否应该在事务性上下文中运行。

# 事务的隔离级别?

隔离级别 含义
ISOLATION_DEFAULT 使用后端数据库默认的隔离级别
ISOLATION_READ_UNCOMMITTED 允许读取尚未提交的更改。可能导致脏读、幻读或不可重复读。
ISOLATION_READ_COMMITTED (Oracle 默认级别)允许从已经提交的并发事务读取。可防止脏读,但幻读和不可重复读仍可能会发生。
ISOLATION_REPEATABLE_READ (MYSQL默认级别)对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻读仍可能发生。
ISOLATION_SERIALIZABLE 完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。

# 回滚规则

在默认设置下,事务只在出现运行时异常(runtime exception)时回滚,而在出现受检查异常(checked exception)时不回滚(这一行为和EJB中的回滚行为是一致的)。

不过,可以声明在出现特定受检查异常时像运行时异常一样回滚。同样,也可以声明一个事务在出现特定的异常时不回滚,即使特定的异常是运行时异常。

# @Transactional的自调用失败

注解@ Trans action 的底层实现是SpringAOP 技术,而SpringAOP 技术使用的是动态代理。这就意味着对于静态( static )方法和非public 方法, 注解@Transactional 是失效的。还有一个更为隐秘的,而且在使用过程中极其容易犯错误的一一自调用。

所谓自调用, 就是一个类的一个方法去调用自身另外一个方法的过程。

在insertRoleList 方法的实现中,它调用了自身类实现insertRole 的方法,而insertRole 声明是阻QUIRES _NEW 的传播行为,也就是每次调用就会产生新的事务运行,那么它会成功吗?

从日志中可以看到角色插入两次都使用了同一事务,也就是说,在insertRole 上标注的@Transactional 失效了,这是一个很容易掉进去的陷阱。

出现这个的问题根本原因在于AOP 的实现原理。由于@Transactional 的实现原理是AOP ,而AOP 的实现原理是动态代理, 而在代码清单13-22 中使用的是自己调用自己的过程。换句话说, 并不存在代理对象的调用,这样就不会产生AOP 去为我们设置@Transactional配置的参数,这样就出现了自调用注解失效的问题

为了克服这个问题, 一方面可以使用两个服务类, SpringIoC 容器中为你生成了RoleService 的代理对象,这样就可以使用AOP, 且不会出现自调用的问题。另外一方面,你也可以直接从容器中获取RoleService 的代理对象。

# 事务相关错误使用案例

# 错误使用Service

假设我们想在一个Controller 中插入两个角色,并且两个角色需要在同一个事务中处理。但是我们在Controller中先后调用Service的插入角色方法,导致错误的产生:

public void errUseServeice(){
	Role role1 = new Role();
    role1.setRoleName("");
    roleService.insertRole(role1);
    
    Role role2 = new Role();
    role2.setRoleName("");
    roleService.insertRole(role2);
}

这里存在的问题是两个insertRole 方法根本不在同一个事务里的问题。

当一个Controller 使用Service 方法时,如果这个Service 标注有@Transactional ,那么它就会启用一个事务,而一个Service 方法完成后,它就会释放该事务,所以前后两个insertRole 的方法是在两个不同的事务中完成的。下面是笔者测试这段代码的日志,可以清晰地看出它们并不存在于同一个事务中。

# 过长时间占用事务

在企业的生产系统中,数据库事务资源是最宝贵的资源之一,使用了数据库事务之后,要及时释放数据库事务。换言之,我们应该尽可能地使用数据库事务资源去完成所需工作,但是在一些工作中需要使用到文件、对外连接等操作,而这些操作往往会占用较长时间。

@override
@Transactional(propagation = Propagation.REQUIRES_NEW, isolaion = Isolation.READ_COMMITTED)
public int insertRole(Role role){
	// 操作一些与数据库无关的操作
    doSomethingForFile();
    return result;
}

当insertRole 方法结束后Spring 才会释放数据库事务资源, 也就是说在运行doSomethingForFile 方法时, Spring 并没有释放数据库事务资源,而等到doSomethingForFile方法运行完成后,返回result 后才会关闭数据库资源。

解决方法是可以将这些与事务无关的操作放在Controller中完成。

# 错误捕捉异常

模拟一段购买商品的代码, 其中ProductService 是产品服务类, 而TransactionService是记录交易信息, 需求显然就是产品减库存和保存交易在同一个事务里面,要么同时成功,要么同时失败,并且假设减库存和保存交易的传播行为都为阻QUIRED。

@override
@Transactional(propagation = Propagation.REQUIRED, isolaion = Isolation.READ_COMMITTED)
public int doTransaction(TransactionBean trans){
	int result = 0;
    try{
    	// 减少库存
        int result = prodoctService.decreaseStock();
        // 如果减少库存成功则保存记录
        if(result > 0){
        	transactionService.save(trans);
        }
    }catch(Exception ex){
    	// 处理异常
        log.info(ex);
    }
}

这里的问题是方法已经存在异常了,由于开发者不了解Spring 的事务约定, 在两个操作的方法里面加入了自己的try catch语旬, 就可能发生这样的结果。当减少库存成功了,但是保存交易信息时失败而发生了异常,此时由于开发者加入try.. . catch . ..语句, 所以Spring 在数据库事务所约定的流程中再也得不到任何异常信息了, 此时Spring 就会提交事务,这样就出现了库存减少,而交易记录却没有的糟糕情况。

解决方案:在catch代码块中加入抛出runtime运行时异常,这样在Spring 的事务流程中,就会捕捉到抛出的这个异常,进行事务回滚,从而保证了产品减库存和交易记录保存的一致性

@override
@Transactional(propagation = Propagation.REQUIRED, isolaion = Isolation.READ_COMMITTED)
public int doTransaction(TransactionBean trans){
	int result = 0;
    try{
    	// 减少库存
        int result = prodoctService.decreaseStock();
        // 如果减少库存成功则保存记录
        if(result > 0){
        	transactionService.save(trans);
        }
    }catch(Exception ex){
    	// 处理异常
        log.info(ex);
        throw new RuntimeException(ex);
    }
}

# Spring中使用到的设计模式?

  1. 单例模式

    • bean的作用范围:singleton
  2. 工厂方法模式

    • Spring使用工厂模式可以通过 BeanFactory 或 ApplicationContext 创建 bean 对象。
  3. 代理模式

    • Spring AOP代理
  4. 适配器模式

    • Spring AOP 的增强或通知(Advice)使用到了适配器模式
    • Spring MVC:controller
  5. 观察者模式:Spring 事件驱动模型就是观察者模式很经典的一个应用。

  6. 模板方法模式

    • Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。

    • 模板方法模式是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。

  7. 装饰者模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。

Last Updated: 3/28/2022, 10:41:34 PM