【Spring学习笔记 九】Spring声明式事务管理实现机制

什么是事务?事务就是把一系列的动作当成一个独立的工作单元,这些动作要么全部完成,要么全部不起作用,关乎数据准确性的地方我们一定要用到事务,防止业务逻辑出错。

什么是事务管理,事务管理对于企业应用而言至关重要。它保证了用户的每一次操作都是可靠的,即便出现了异常的访问情况,也不至于破坏后台数据的完整性。就像银行的自助取款机,通常都能正常为客户服务,但是也难免遇到操作过程中机器突然出故障的情况,此时,事务就必须确保出故障前对账户的操作不生效,就像用户刚才完全没有使用过取款机一样,以保证用户和银行的利益都不受损失

关于事务及事务管理其实我们在MySQL以及Redis部分学习的时候已经深入了解过了,在我的这篇博客:【MySQL数据库原理 七】MySQL数据库事务及锁机制里详细介绍了MySql的事务实现方式,并且在【Java Web编程 十三】深入理解JDBC规范中提及了JDBC是如何使用事务的,在【MyBatis学习笔记 二】MyBatis基本操作CRUD及配置解析中提及了MyBatis事务提交的方法,在另一篇博客:【Redis核心知识 三】Redis的事务机制里详细介绍了Redis的事务实现机制,今天这篇Blog从Spring的角度出发和实现方式去讨论下Spring是如何进行事务管理的,又是通过什么机制来实现事务管理的。

事务特性

在学习MySql中就学习过事务的四大特性,也可以理解为事务的四大原则,也就是我们常说的ACID:

  • 原子性(Atomicity),事务是最⼩的执⾏单位,不允许分割。事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响
  • 一致性(Consistency),是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
  • 隔离性(Isolation),是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离,要串行执行。即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。
  • 持久性(Durability),是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,导致数据库因为故障而没有执行事务的重大错误。

满足这四个基本原则或者特性,我们就说这个事务是有效的。

事务分类

事务的分类方式有好几种,分别安装事务生效范围、实现方式以及编程方式进行划分。

本地事务全局(分布式)事务

按照事务的生效范围,事务可以分类为:本地事务全局(分布式)事务

  • 本地事务:普通事务,独立一个数据库,能保证在该数据库上操作的ACID;
  • 全局(分布式)事务:涉及两个或多个数据库源的事务,即跨越多台同类或异类数据库的事务(由每台数据库的本地事务组成),分布式事务旨在保证这些本地事务的所有操作的ACID,使事务可以跨越多台数据库;

JDBC事务JTA事务

按照Java事务类型实现方式,事务可以分类为:JDBC事务JTA事务

  • JDBC事务:即为上面说的数据库事务中的本地事务,通过connection对象控制管理
  • JTA事务:指Java事务API(Java Transaction API),是Java EE数据库事务规范,JTA只提供了事务管理接口,由应用程序服务器厂商(如WebSphere Application Server)提供实现,JTA事务比JDBC更强大,支持分布式事务

声明式事务和编程式事务

按照是否通过编程或是否对业务代码有侵入性分为声明式事务编程式事务

  • 编程式事务:通过编程代码在业务逻辑时需要时自行实现,粒度更小,编程式事务是侵入性事务管理,在代码中直接使用底层的PlatformTransactionManager、使用TransactionTemplate。我们需要在代码中调用beginTransaction()、commit()、rollback()等事务管理相关的方法
  • 声明式事务:通过注解或XML配置实现,粒度大一些,但对业务无侵入性

我们通过Spring可以实现全局的JTA类型的声明式事务,这也是我们最常用的一种事务类型组合

Spring事务实现原理

Spring 框架中最重要的事务相关API有三个:TransactionDefinition、PlatformTransactionManager、TransactionStatus。所谓事务管理,其实就是按照给定的事务规则来执行提交或者回滚操作

  • 给定的事务规则就是用 TransactionDefinition 表示的
  • 按照规则执行提交或者回滚操作则是用 PlatformTransactionManager 来表示

TransactionStatus 的作用则是用于表示一个运行着的事务的状态,也就是事务操作的结果或进度。
在这里插入图片描述

TransactionDefinition接口功能

TransactionDefinition接口管理了一个事务具体的定义和全部属性,的主要方法如下

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface TransactionDefinition {
    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;
    int ISOLATION_DEFAULT = -1;
    int ISOLATION_READ_UNCOMMITTED = 1;
    int ISOLATION_READ_COMMITTED = 2;
    int ISOLATION_REPEATABLE_READ = 4;
    int ISOLATION_SERIALIZABLE = 8;
    int TIMEOUT_DEFAULT = -1;
    
    //返回事务的传播行为,由是否有一个活动的事务来决定一个事务调用
    default int getPropagationBehavior() {
        return 0;
    }
    //返回事务的隔离级别,事务管理器依据它来控制另外一个事务能够看到本事务内的哪些数据
    default int getIsolationLevel() {
        return -1;
    }
    //返回事务必须在多少秒内完毕
    default int getTimeout() {
        return -1;
    }
    //事务是否仅仅读,事务管理器可以依据这个返回值进行优化。确保事务是仅仅读的
    default boolean isReadOnly() {
        return false;
    }

    @Nullable
    default String getName() {
        return null;
    }

    static TransactionDefinition withDefaults() {
        return StaticTransactionDefinition.INSTANCE;
    }
    
}

1 事务隔离级别

指若干个并发的事务之间的隔离程度,TransactionDefinition接口中定义了5个表示隔离级别的常量

    int ISOLATION_DEFAULT = -1;
    int ISOLATION_READ_UNCOMMITTED = 1;
    int ISOLATION_READ_COMMITTED = 2;
    int ISOLATION_REPEATABLE_READ = 4;
    int ISOLATION_SERIALIZABLE = 8;

各个隔离级别解释如下:

  • ISOLATION_DEFAULT :默认值-1,表示使用底层数据库的默认隔离级别,对大部分数据库而言,通常这值就是ISOLATION_READ_COMMITTED,也就是读已提交
  • ISOLATION_READ_UNCOMMITTED读未提交,该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据,该级别可能导致脏读、不可重复读和幻读,因此很少使用该隔离级别,比如PostgreSQL实际上并没有此级别
  • ISOLATION_READ_COMMITTED
  • ISOLATION_READ_COMMITTED读已提交,(Oracle默认级别)该隔离级别表示一个事务只能读取另一个事务已经提交的数据,即允许从已经提交的并发事务读取,该级别可以防止脏读,但幻读和不可重复读仍可能会发生;
  • ISOLATION_REPEATABLE_READ可重复读,(MySQL默认级别)该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同,即对相同字段的多次读取的结果是一致的,除非数据被当前事务本事改变。该级别可以防止脏读和不可重复读,但幻读仍可能发生;
  • ISOLATION_SERIALIZABLE串行化,(完全服从ACID的隔离级别)所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读和幻读,但严重影响程序的性能,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的

关于脏读、幻读和不可重复读的一些概念,可以参照我这篇Blog:【MySQL数据库原理 七】MySQL数据库事务及锁机制这里不再赘述。

2 事务传播机制

事务的传播性一般用在事务嵌套的场景,比如一个事务方法里面调用了另外一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,这就需要事务传播机制的配置来确定怎么样执行;在TransactionDefinition接口中定义了以下几个表示传播机制的常量,值为0~6:

    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;

各个传播性配置解释如下:

  • PROPAGATION_REQUIRED默认值,能满足绝大部分业务需求如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行,假设 ServiceX#methodX() 都工作在事务环境下(即都被 Spring 事务增强了),假设程序中存在如下的调用链:Service1#method1()->Service2#method2()->Service3#method3(),那么这 3 个服务类的 3 个方法通过 Spring 的事务传播机制都工作在同一个事务中,也就是我们刚才的几个方法存在调用,所以会被放在一组事务当中!
  • PROPAGATION_SUPPORTS :如果外层有事务,则加入外层事务;如果外层没有事务,则直接以非事务的方式继续运行。完全依赖外层的事务
  • PROPAGATION_MANDATORY:如果外层有事务,则加入外层事务,如果外层没有事务,则抛出异常
  • PROPAGATION_REQUIRES_NEW :该事务传播机制是每次都会新开启一个事务,同时把外层事务挂起,当当前事务执行完毕,恢复外层事务的执行。如果外层没有事务,执行当前新开启的事务即可,挂起事务指的是将当前事务的属性如事务名称,隔离级别等属性保存在一个变量中,同时将当前线程中所有和事务相关的ThreadLocal变量设置为从未开启过线程一样。Spring维护着一个当前线程的事务状态,用来判断当前线程是否在一个事务中以及在一个什么样的事务中,挂起事务后,当前线程的事务状态就好像没有事务
  • PROPAGATION_NOT_SUPPORTED:该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码
  • PROPAGATION_NEVER,该传播机制不支持外层事务,即如果外层有事务就抛出异常
  • PROPAGATION_NESTED:该传播机制的特点是可以保存状态保存点,当前事务回滚到某一个点,从而避免所有的嵌套事务都回滚,即各自回滚各自的,如果子事务没有把异常吃掉,基本还是会引起全部回滚的,前面的六种事务传播行为是 Spring 从 EJB 中引入的,他们共享相同的概念。而 PROPAGATION_NESTED是 Spring 所特有的。以 PROPAGATION_NESTED 启动的事务内嵌于外部事务中(如果存在外部事务的话),此时,内嵌事务并不是一个独立的事务,它依赖于外部事务的存在,只有通过外部的事务提交,才能引起内部事务的提交,嵌套的子事务不能单独提交。如果熟悉 JDBC 中的保存点(SavePoint)的概念,那嵌套事务就很容易理解了,其实嵌套的子事务就是保存点的一个应用,一个事务中可以包括多个保存点,每一个嵌套子事务。另外,外部事务的回滚也会导致嵌套子事务的回滚

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

3 只读

如果一个事务只对数据库执行读操作,那么该数据库就可能利用事务的只读特性采取优化措施。通过把一个事务声明为只读,可以给数据库一个机会来应用那些它认为合适的优化措施。由于只读的优化措施是在一个事务启动时由后端数据库实施的,因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、 ROPAGATION_NESTED)的方法来说,将事务声明为只读才有意义,在 TransactionDefinition 中以 boolean 类型来表示该事务是否只读

   //事务是否仅仅读,事务管理器可以依据这个返回值进行优化。确保事务是仅仅读的
    default boolean isReadOnly() {
        return false;
    }

4 事务超时

指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务,在TransactionDefinition中以int的值来表示超时时间,其单位是秒;默认设置为底层事务系统的超时值,如果底层数据库事务系统没有设置超时值,那么就是none,没有超时限制

//返回事务必须在多少秒内完毕
    default int getTimeout() {
        return -1;
    }

5 Spring事务回滚规则

默认配置下,Spring只有在抛出的异常为运行时异常(runtime exception)时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Error也会导致事务回滚),而抛出受检查异常(checked exception)则不会导致事务回滚,除此之外:

  • 可以声明在抛出哪些异常时回滚事务,包括checked异常
  • 可以声明哪些异常抛出时不回滚事务,即使异常是运行时异常
  • 可以编程性的通过setRollbackOnly()方法来指示一个事务必须回滚,在调用完setRollbackOnly()后你所能执行的唯一操作就是回滚

也就是说比较灵活,我们一般使用默认的方式就可以了

PlatformTransactionManager接口功能

Spring所有的事务管理策略类都继承自org.springframework.transaction.PlatformTransactionManager接口,用于执行具体的事务操作。PlatformTransactionManager 接口中定义的主要方法如下

package org.springframework.transaction;

import org.springframework.lang.Nullable;

public interface PlatformTransactionManager extends TransactionManager {
    //获得当前事务状态
    TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
    //提交事务
    void commit(TransactionStatus var1) throws TransactionException;
    //回滚事务
    void rollback(TransactionStatus var1) throws TransactionException;
}

根据底层所使用的不同的持久化 API 或框架,PlatformTransactionManager 的主要实现类大致如下:

  • DataSourceTransactionManager:适用于使用JDBC和iBatis进行数据持久化操作的情况,因为我们的Spring框架整合的是MyBatis,所以我们重点讨论这种情况
  • HibernateTransactionManager:适用于使用Hibernate进行数据持久化操作的情况。
  • JpaTransactionManager:适用于使用JPA进行数据持久化操作的情况。

在这里插入图片描述

TransactionStatus接口功能

TransactionStatus 表示事务状态, TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;方法返回一个 TransactionStatus 对象,表示一个事务的状态,部分方法如下:

package org.springframework.transaction;

import java.io.Flushable;

public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
    boolean hasSavepoint();

    void flush();
}

其实现接口实现的一些判断方法如下:

    //判断事务是否只读
    boolean isReadOnly() ;
 
    //是否是一个新的事务
    boolean isNewTransaction();

    //判断是否有回滚点
    boolean hasSavepoint();

    //将一个事务标识为不可提交的。在调用完setRollbackOnly()后只能被回滚
    //在大多数情况下,事务管理器会检测到这一点,在它发现事务要提交时会立刻结束事务。
    //调用完setRollbackOnly()后,数数据库可以继续执行select,但不允许执行update语句,因为事务只可以进行读取操作,任何修改都不会被提交。
    void setRollbackOnly();
    
    boolean isRollbackOnly();

    void flush();
    
    //判断事务是否已经完成
    boolean isCompleted();
  
    Object createSavepoint() throws TransactionException;

    void rollbackToSavepoint(Object var1) throws TransactionException;

    void releaseSavepoint(Object var1) throws TransactionException;

Spring声明式事务实现原理

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

声明式事务组成部分

Spring配置文件中关于事务配置总是由三个组成部分,分别是DataSource、TransactionManager代理机制这三部分,无论哪种配置方式,一般变化的只是代理机制这部分,DataSource、TransactionManager这两部分只是会根据数据访问方式有所变化,比如使用Hibernate进行数据访问时,DataSource实际为SessionFactory,TransactionManager的实现为HibernateTransactionManager
在这里插入图片描述
声明式事务的实现方式随着时间的演化过程分为四个阶段:

  1. 使用拦截器:基于TransactionInterceptor 类来实施声明式事务管理功能(Spring最初提供的实现方式);
  2. 使用Bean和代理:基于 TransactionProxyFactoryBean的声明式事务管理
  3. 使用tx标签配置的拦截器:基于tx和aop名字空间的xml配置文件(基于Aspectj AOP配置事务)
  4. 使用全注解实现:基于@Transactional注解

编程式事务每次实现都要单独实现,但业务量大且功能复杂时,使用编程性事务无疑是痛苦的;而声明式事务不同,声明式事务属于非侵入性,不会影响业务逻辑的实现,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中,这种非侵入式的开发方式使得声明式事务管理下的业务代码不受污染,当然缺点也比较明显就是:最细粒度只能是作用到方法级别,无法做到像编程事务那样可以作用到代码块级别

声明式事务执行流程

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

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

整体的执行流程如下图所示:
在这里插入图片描述

Spring声明式事务实现方式

大多数情况下我们使用的是声明式事务,因为声明式事务可以解决80%以上的场景,而且对现有逻辑没有侵入性,所以这里就不再做编程式事务的相关实践,而声明式事务的演化过程也分为如下4种:
在这里插入图片描述

我们使用事务时充分利用AOP来实现,所以只介绍最后也是最新最常用的两种用法,项目也是基于之前的spring-myBatis的整合项目,所以这里用的事务管理器也是DataSourceTransactionManager

不使用事务会怎样

还是那句话,使用一个技术前一定是有需求催生的,所以不使用事务会发生什么具体的问题呢?数据准备还是使用我们之前的整合项目【Spring学习笔记 八】Spring整合MyBatis实现方式,我们要实现如下一个需求,新增一个人员到系统中后要把这个人员同步到别的系统中,也就是保证两个系统中的数据一致,我们来准备下代码环境:

数据库表

**加粗样式**

PersonDao

package com.example.spring_mybatis.dao;

import com.example.spring_mybatis.model.Person;

import java.util.List;

public interface PersonDao {
    List<Person> getPersonList();
    int addAndSendPerson(Person person);
}

PersonServiceImpl

package com.example.spring_mybatis.serviceImpl;

import com.example.spring_mybatis.daoImpl.PersonDaoImpl;
import com.example.spring_mybatis.model.Person;
import com.example.spring_mybatis.service.PersonService;
import lombok.Data;

import java.util.List;
@Data
public class PersonServiceImpl implements PersonService {

    private PersonDaoImpl personDaoImpl;

    @Override
    public List<Person> getPersonList() {
        List<Person> personList=personDaoImpl.getPersonList();
        for (Person person : personList) {
            System.out.println(person);
        }
        return personList;
    }

    @Override
    public int addAndSendPerson(Person person) {
        int result=personDaoImpl.addPerson(person);//本地新增人员
        this.sendMessage(person); //发送人员同步到下游系统
        return result;
    }

    public void sendMessage(Person person){
        System.out.println("人员新增到下游系统失败"+person);
        throw new RuntimeException();

    }
}

PersonServiceTest

 @Test
    public void addAndSendPersonTest(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        //落地还是落地到某个具体的接口上了,所以我们一般是一个接口对应一个实现类
        PersonService personService = (PersonService) applicationContext.getBean("personServiceImpl");
        Person person=new Person();
        person.setId(4);
        person.setUsername("wcong");
        person.setAge(30);
        person.setEmail("111111@qq.com");
        person.setPassword("111111");
        person.setPhone(11111111);
        person.setHobby("跳远");
        personService.addAndSendPerson(person);
    }

我们跑一下单元测试,我们的目的是保证人员数据一致,如果下游同步失败抛异常,我们本地也不应该新增数据成功,然而运行单元测试:
在这里插入图片描述
异常抛出了,下游系统数据未同步,但是人员本地落库却成功了:
在这里插入图片描述
这就是不增加事务会产生的问题,接下来我们用声明式事务解决下该问题。

基于Aspectj AOP配置实现

通过Aspectj AOP配置实现需要经过如下步骤

1 添加tx名字空间

基于AOP的配置首先需要引入相关约束,支持使用tx标签来进行事务处理:

xmlns:tx="http://www.springframework.org/schema/tx"

2 applicationContext.xml配置文件配置

然后我们在applicationContext.xml配置文件中进行相关配置:

    <!-- 加载数据库配置信息 -->
    <context:property-placeholder location="properties/db.properties" system-properties-mode="NEVER"/>
    <!-- 连接池对象 -->
    <bean id="myDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" >
        <property name="driverClassName" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
    </bean>
    <!-- 加载事务管理器 -->
   <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="myDataSource" />
    </bean>
    <!--
      <tx:advice>定义事务通知,用于指定事务属性,其中“transaction-manager”属性指定事务管理器,并通过<tx:attributes>指定具体需要拦截的方法
      <tx:method>拦截方法,其中参数有:
      name:方法名称,将匹配的方法注入事务管理,可用通配符
      propagation:事务传播行为,
      isolation:事务隔离级别定义;默认为“DEFAULT”
      timeout:事务超时时间设置,单位为秒,默认-1,表示事务超时将依赖于底层事务系统;
      read-only:事务只读设置,默认为false,表示不是只读;
      rollback-for:需要触发回滚的异常定义,可定义多个,以“,”分割,默认任何RuntimeException都将导致事务回滚,而任何Checked Exception将不导致事务回滚;
      no-rollback-for:不被触发进行回滚的 Exception(s);可定义多个,以“,”分割;
 -->
    <!--配置事务通知-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!--配置哪些方法使用什么样的事务,配置事务的传播特性-->
            <!-- 拦截addAndSendPerson方法,事务传播行为为:REQUIRED:必须要有事务, 如果没有就在上下文创建一个 -->
            <tx:method name="addAndSendPerson" propagation="REQUIRED" isolation="READ_COMMITTED" timeout="20" read-only="false" no-rollback-for="" rollback-for="java.lang.Exception"/>
            <!-- 支持,如果有就有,没有就没有 -->
            <tx:method name="*" propagation="SUPPORTS"/>
        </tx:attributes>
    </tx:advice>
   <!--配置AOP-->
    <aop:config proxy-target-class="true">
        <aop:pointcut id="txPointcut" expression="execution(* com.example.spring_mybatis.service..*.*(..))"/>
        <!--<aop:advisor>定义切入点,与通知,把tx与aop的配置关联,才是完整的声明事务配置 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
    </aop:config>

3 测试事务实现

然后我们调整测试方法新插入一条数据看是否能插入成功:

  @Test
    public void addAndSendPersonTest(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        //落地还是落地到某个具体的接口上了,所以我们一般是一个接口对应一个实现类
        PersonService personService = (PersonService) applicationContext.getBean("personServiceImpl");
        Person person=new Person();
        person.setId(5);
        person.setUsername("lisi");
        person.setAge(30);
        person.setEmail("111111@qq.com");
        person.setPassword("111111");
        person.setPhone(11111111);
        person.setHobby("跳远");
        personService.addAndSendPerson(person);
    }

单元测试结果如下:
在这里插入图片描述
然后我们再看数据库中,该数据并没有被add成功:
在这里插入图片描述

基于@Transactional注解实现

接下来我们看看通过注解如何实现:

@Transactional注解特性

关于@Transactional注解我们还需要了解一些配置属性:
在这里插入图片描述

@Transactional注解使用规范

@Transactional 注解有如下几种使用原则和注意点,防止我们使用时配置错误

  • @Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法都将具有该类型的事务属性,同时,我们也可以在方法级别使用该注解来覆盖类级别的定义。
  • 虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外,@Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果在 protected、private 或者默认可见性的方法上使用@Transactional注解,这将被忽略,也不会抛出任何异常。
  • 如果在接口、实现类或方法上都指定了@Transactional 注解,则优先级顺序为方法>实现类>接口;

关于注解的实现方式我们知道这些就够了

@Transactional注解实现步骤

基于配置的方式当然略显繁琐,我们再来尝试下基于注解的实现方式吧。

1 添加tx名字空间

基于注解的配置首先也需要引入相关约束,支持使用tx标签来进行事务处理:

xmlns:tx="http://www.springframework.org/schema/tx"

2 applicationContext.xml配置文件配置

然后开启事务的注解支持:Spring 使用 BeanPostProcessor 来处理 Bean 中的标注,因此我们需要在配置文件中作如下声明来激活该后处理 Bean:

<!-- 开启事务控制的注解支持 -->  
<tx:annotation-driven transaction-manager="transactionManager"/>

MyBatis自动参与到Spring事务管理中,无需额外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的数据源与DataSourceTransactionManager引用的数据源一致即可,这样我们的applicationContext.xml配置文件相关配置如下:

     <!-- 开启事务控制的注解支持 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="myDataSource" />
    </bean>

4 PersonServiceImpl类的addAndSendPerson方法加注解

然后我们需要在PersonServiceImpl类上的方法加@Transactional 注解:

package com.example.spring_mybatis.serviceImpl;

import com.example.spring_mybatis.daoImpl.PersonDaoImpl;
import com.example.spring_mybatis.model.Person;
import com.example.spring_mybatis.service.PersonService;
import lombok.Data;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
@Data
public class PersonServiceImpl implements PersonService {

    private PersonDaoImpl personDaoImpl;

    @Override
    public List<Person> getPersonList() {
        List<Person> personList=personDaoImpl.getPersonList();
        for (Person person : personList) {
            System.out.println(person);
        }
        return personList;
    }

    @Override
    @Transactional
    public int addAndSendPerson(Person person) {
        int result=personDaoImpl.addPerson(person);
        this.sendMessage(person);
        return result;
    }

    public void sendMessage(Person person){
        System.out.println("人员新增到下游系统失败"+person);
        throw new RuntimeException();

    }
}

5 测试事务实现

然后我们再进行单元测试:
在这里插入图片描述
然后看数据库发现数据没有被插入进行:
在这里插入图片描述

基于配置和基于注解实现对比

基于 <tx> 命名空间和基于 @Transactional 的事务声明方式各有优缺点。基于 <tx> 的方式,其优点是与切点表达式结合,功能强大。利用切点表达式,一个配置可以匹配多个方法,而基于 @Transactional 的方式必须在每一个需要使用事务的方法或者类上用 @Transactional 标注,尽管可能大多数事务的规则是一致的,但是对 @Transactional 而言,也无法重用,必须逐个指定。另一方面,基于 @Transactional 的方式使用起来非常简单明了,没有学习成本。开发人员可以根据需要,任选其中一种使用,甚至也可以根据需要混合使用这两种方式

总结一下

其实事务的实现方式也存在类似Spring整合MyBatis或者Spring AOP的实现演进方式类似,最开始我们使用一组API去实现功能;后来有个了Spring把这组API交给配置文件管理,使用最原生的方式实现,例如AOP使用原生动态代理实现、事务最初用的是基于拦截器的实现方式;再后来Spring发现可以把这些经典实现规范化为一组标签,用于简化配置或者增强其通用能力,这样就为其规范了标准的命名空间,大大简化了配置也让我们更明白这组配置的作用,例如<aop>标签就是用来实现AOP的,<tx>标签就是用来实现事务的;最终因为有了注解所有Spring认为干脆连这个配置都不需要了,直接在XML中配置一个注解,用到哪些配置方式在对应的方法上直接配就行了。当然配置和注解目前我认为还是不相伯仲的,配置可以集中化管理,注解可以快速上手,混合使用更好吧,我认为复杂实现还是使用配置好些,简单实现可以使用注解。所以总结而言就是:代码实现,配置取代代码,简化配置,注解取代配置

评论 1 您还未登录,请先 登录 后发表或查看评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:Age of Ai 设计师:meimeiellie 返回首页

打赏作者

存在morning

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值