美文网首页
PlatformTransactionManager

PlatformTransactionManager

作者: 学编程的小屁孩 | 来源:发表于2020-02-23 19:29 被阅读0次

    Spring Boot 使用事务非常简单,首先使用注解 @EnableTransactionManagement 开启事务支持后,然后在访问数据库的Service方法上添加注解 @Transactional 便可。
    关于事务管理器,不管是JPA还是JDBC等都实现自接口 PlatformTransactionManager

    • 添加spring-boot-starter-jdbc依赖默认注入 DataSourceTransactionManager 实例。
    • 添加spring-boot-starter-data-jpa依赖默认注入 JpaTransactionManager 实例。

    你可以在启动类中添加如下方法,Debug测试,就能知道自动注入的是 PlatformTransactionManager 接口的哪个实现类。

    // 启注解事务管理,等同于xml配置方式的 <tx:annotation-driven />
    @EnableTransactionManagement 
    @SpringBootApplication public class ProfiledemoApplication {
    
      @Bean 
      public Object testBean(PlatformTransactionManager platformTransactionManager) {
        System.out.println(">>>" + platformTransactionManager.getClass().getName()); 
        return new Object();
      } 
    
      public static void main(String[] args) {
        SpringApplication.run(ProfiledemoApplication.class, args);
      }
    }
    

    这些SpringBoot为我们自动做了,这些对我们并不透明,如果你项目做的比较大,添加的持久化依赖比较多,我们还是会选择人为的指定使用哪个事务管理器。
    代码如下:

    @EnableTransactionManagement
    @SpringBootApplication public class ProfiledemoApplication { 
    
      // 其中 dataSource 框架会自动为我们注入
      @Bean 
      public PlatformTransactionManager txManager(DataSource dataSource) { 
        return new DataSourceTransactionManager(dataSource);
      }
    
      @Bean 
      public Object testBean(PlatformTransactionManager platformTransactionManager) {
        System.out.println(">>>" + platformTransactionManager.getClass().getName()); 
        return new Object();
      } 
    
      public static void main(String[] args) {
            SpringApplication.run(ProfiledemoApplication.class, args);
      }
    }
    

    在Spring容器中,我们手工注解@Bean 将被优先加载,框架不会重新实例化其他的 PlatformTransactionManager 实现类。
    然后在Service中,被 @Transactional 注解的方法,将支持事务。如果注解在类上,则整个类的所有方法都默认支持事务。
    对于同一个工程中存在多个事务管理器要怎么处理,请看下面的实例,具体说明请看代码中的注释。

    // 开启注解事务管理,等同于xml配置文件中的 <tx:annotation-driven />
    @EnableTransactionManagement 
    @SpringBootApplication 
    public class ProfiledemoApplication implements TransactionManagementConfigurer {
    
      @Resource(name="txManager2") 
      private PlatformTransactionManager txManager2; // 创建事务管理器1
    
      @Bean(name = "txManager1") 
      public PlatformTransactionManager txManager(DataSource dataSource) {   
        return new DataSourceTransactionManager(dataSource);
      }
    
       // 创建事务管理器2
      @Bean(name = "txManager2") 
      public PlatformTransactionManager txManager2(EntityManagerFactory factory) { 
        return new JpaTransactionManager(factory);
      } 
    
      // 实现接口 TransactionManagementConfigurer 方法,
      // 其返回值代表在拥有多个事务管理器的情况下默认使用的事务管理器
      @Override 
      public PlatformTransactionManager annotationDrivenTransactionManager() { 
        return txManager2;
      } 
    
      public static void main(String[] args) {
        SpringApplication.run(ProfiledemoApplication.class, args);
      }
    }
    
    @Component 
    public class DevSendMessage implements SendMessage { 
      // 使用value具体指定使用哪个事务管理器
      @Transactional(value="txManager1")
      @Override 
      public void send() {
        System.out.println(">>>>>>>>Dev Send()<<<<<<<<");
        send2();
      } 
    
      // 在存在多个事务管理器的情况下,如果使用value具体指定
      // 则默认使用方法 annotationDrivenTransactionManager() 返回的事务管理器
     @Transactional 
      public void send2() {
        System.out.println(">>>>>>>>Dev Send2()<<<<<<<<");
      }
    }
    

    注:
    如果Spring容器中存在多个 PlatformTransactionManager 实例,并且没有实现接口 TransactionManagementConfigurer 指定默认值,在我们在方法上使用注解 @Transactional 的时候,就必须要用value指定,如果不指定,则会抛出异常。

    对于系统需要提供默认事务管理的情况下,实现接口 TransactionManagementConfigurer 指定。

    对有的系统,为了避免不必要的问题,在业务中必须要明确指定 @Transactional 的 value 值的情况下。不建议实现接口 TransactionManagementConfigurer,这样控制台会明确抛出异常,开发人员就不会忘记主动指定。

    注意的几点:
    1 @Transactional 只能被应用到public方法上, 对于其它非public的方法,如果标记了@Transactional也不会报错,但方法没有事务功能.

    2 默认情况下,一个有事务方法, 遇到RuntiomeException会回滚 . 遇到 受检查的异常不会回滚 的. 要想所有异常都回滚,要加上 @Transactional( rollbackFor={Exception.class,其它异常}) .

    @Transactional 的所有可选属性如下:

    属性 类型 默认值 说明
    propagation Propagation枚举 REQUIRED 事务传播属性 (下有说明)
    isolation isolation枚举 DEFAULT 事务隔离级别 (另有说明)
    readOnly boolean false 是否只读
    timeout int -1 超时(秒)
    rollbackFor Class[] {} 需要回滚的异常类
    rollbackForClassName String[] {} 需要回滚的异常类名
    noRollbackFor Class[] {} 不需要回滚的异常类
    noRollbackForClassName String[] {} 不需要回滚的异常类名

    ** 事务的隔离级别 有如下可选:**

    用时,导入org.springframework.transaction.annotation.Isolation,再在Transactional括号里用如isolation = Isolation.DEFAULT)

    DEFAULT 采用数据库默认隔离级别
    READ_UNCOMMITTED 读未提交数据( 会出现脏读,不可重复读,幻读 ,避免了 第一类丢失 更新 )
    READ_COMMITTED 读已提交的数据(会出现不可重复读,幻读)
    REPEATABLE_READ 可重复读(会出现幻读)
    SERIALIZABLE 串行化

    数据库提供了四种事务隔离级别, 不同的隔离级别采用不同的锁类开来实现.
    在四种隔离级别中, Serializable的级别最高, Read Uncommited级别最低.
    大多数数据库的默认隔离级别为: Read Commited,如Sql Server , Oracle.
    少数数据库默认的隔离级别为Repeatable Read, 如MySQL InnoDB存储引擎
    即使是最低的级别,也不会出现第一类丢失更新问题。

    丢失更新:
    当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,会发生丢失更新问题。每个事务都不知道其它事务的存在。最后的更新将重写由其它事务所做的更新,这将导致数据丢失。

    例:
    事务A和事务B同时修改某行的值,
    1.事务A将数值改为1并提交
    2.事务B将数值改为2并提交。
    这时数据的值为2,事务A所做的更新将会丢失。
    解决办法:对行加锁,只允许并发一个更新事务。

    脏读: 一个事务读到另一个事务未提交的更新数据

    例:
    1.Mary的原工资为1000, 财务人员将Mary的工资改为了8000(但未提交事务)
    2.Mary读取自己的工资 ,发现自己的工资变为了8000,欢天喜地!
    3.而财务发现操作有误,回滚了事务,Mary的工资又变为了1000, 像这样,Mary记取的工资数8000是一个脏数据。

    不可重复读: 在同一个事务中,多次读取同一数据,返回的结果有所不同. 换句话说就是,后续读取可以读到另一个事务已提交的更新数据. 相反"可重复读"在同一事务多次读取数据时,能够保证所读数据一样,也就是后续读取不能读到另一事务已提交的更新数据.

    例:
    1.在事务1中,Mary 读取了自己的工资为1000,操作并没有完成
    2.在事务2中,这时财务人员修改了Mary的工资为2000,并提交了事务.
    3.在事务1中,Mary 再次读取自己的工资时,工资变为了2000
    解决办法:如果只有在修改事务完全提交之后才可以读取数据,则可以避免该问题。

    幻读: 一个事务读取到另一个事务已提交的insert数据.

    例:
    第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时 (此时第一事务还未提交) ,第二个事务向表中插入一行新数据。这时第一个事务再去读取表时,发现表中还有没有修改的数据行,就好象发生了幻觉一样。

    **事务的传播属性 **,有如下可选
    用时,导入org.springframework.transaction.annotation.Propagation,再在Transactional括号里用如propagation = Propagation.REQUIRED)

    传播属性 详细描述
    REQUIRED 业务方法需要在一个事务中运行,如果方法运行时,已处在一个事务中,那么就加入该事务,否则自己创建一个新的事务.这是spring默认的传播行为.
    SUPPORTS 如果业务方法在某个事务范围内被调用,则方法成为该事务的一部分,如果业务方法在事务范围外被调用,则方法在没有事务的环境下执行.
    MANDATORY 只能在一个已存在事务中执行,业务方法不能发起自己的事务,如果业务方法在没有事务的环境下调用,就抛异常
    REQUIRES_NEW 业务方法总是会为自己发起一个新的事务,如果方法已运行在一个事务中,则原有事务被挂起,新的事务被创建,直到方法结束,新事务才结束,原先的事务才会恢复执行.
    NOT_SUPPORTED 声明方法需要事务,如果方法没有关联到一个事务,容器不会为它开启事务.如果方法在一个事务中被调用,该事务会被挂起,在方法调用结束后,原先的事务便会恢复执行.
    NEVER 声明方法绝对不能在事务范围内执行,如果方法在某个事务范围内执行,容器就抛异常.只有没关联到事务,才正常执行.
    NESTED 如果一个活动的事务存在,则运行在一个嵌套的事务中.如果没有活动的事务,则按REQUIRED属性执行.它使用了一个单独的事务, 这个事务拥有多个可以回滚的保证点.内部事务回滚不会对外部事务造成影响, 它只对DataSourceTransactionManager 事务管理器起效.

    事务传播属性
    @Transactional(propagation=Propagation.REQUIRED) //如果有事务,那么加入事务,没有的话新建一个(不写的情况下)
    @Transactional(propagation=Propagation.NOT_SUPPORTED) //容器不为这个方法开启事务
    @Transactional(propagation=Propagation.REQUIRES_NEW) //不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务
    @Transactional(propagation=Propagation.MANDATORY) //必须在一个已有的事务中执行,否则抛出异常
    @Transactional(propagation=Propagation.NEVER) //必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)
    @Transactional(propagation=Propagation.SUPPORTS) //如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.
    @Transactional(propagation=Propagation.NESTED) //嵌套的事务
    @Transactional (propagation = Propagation.REQUIRED,readOnly=true) //readOnly=true只读,不能更新,删除
    @Transactional (propagation = Propagation.REQUIRED,timeout=30)//设置超时时间
    @Transactional (propagation = Propagation.REQUIRED,isolation=Isolation.DEFAULT)//设置数据库隔离级别

    用 spring 事务管理器,由spring来负责数据库的打开,提交,回滚.
    默认遇到运行期例外(throw new RuntimeException("注释");)会回滚,即遇到不受检查(unchecked)的例外时回滚;
    而遇到需要捕获的例外(throw new Exception("注释");)不会回滚,即遇到受检查的例外(就是非运行时抛出的异常,编译器会检查到的异常叫受检查例外或说受检查异常)时,需我们指定方式来让事务回滚 ,如下:

    @Transactional(rollbackFor=Exception.class) 
    //指定回滚,遇到异常Exception时回滚
    public void methodName() { 
      throw new Exception("注释");
    } 
    
    @Transactional(noRollbackFor=Exception.class)
    //指定不回滚,遇到运行期例外(throw new RuntimeException("注释");)会回滚
    public ItimDaoImpl getItemDaoImpl() { 
      throw new RuntimeException("注释");
      } 
    

    什么是事务?

    我们在开发企业应用时,对于业务人员的一个操作实际是对数据读写的多步操作的结合。由于数据操作在顺序执行的过程中,任何一步操作都有可能发生异常,异常会导致后续操作无法完成,此时由于业务逻辑并未正确的完成,之前成功操作数据的并不可靠,需要在这种情况下进行回退。

    事务的作用就是为了保证用户的每一个操作都是可靠的,事务中的每一步操作都必须成功执行,只要有发生异常就回退到事务开始未进行操作的状态。

    事务管理是Spring框架中最为常用的功能之一,我们在使用Spring Boot开发应用时,大部分情况下也都需要使用事务。

    快速入门

    在Spring Boot中,当我们使用了spring-boot-starter-jdbc或spring-boot-starter-data-jpa依赖的时候,框 架会自动默认分别注入DataSourceTransactionManager或JpaTransactionManager。所以我们不需要任何额外 配置就可以用@Transactional注解进行事务的使用。

    在该样例工程中(若对该数据访问方式不了解,可先阅读该文章),我们引入了spring-data-jpa,并创建了User实体以及对User的数据访 问对象UserRepository,在ApplicationTest类中实现了使用UserRepository进行数据读写的单元测试用例,如下:

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(Application.class) public class ApplicationTests {
    
      @Autowired 
      private UserRepository userRepository;
    
      @Test public void test() throws Exception { 
        // 创建10条记录
        userRepository.save(new User("AAA", 10));
        userRepository.save(new User("BBB", 20));
        userRepository.save(new User("CCC", 30));
        userRepository.save(new User("DDD", 40));
        userRepository.save(new User("EEE", 50));
        userRepository.save(new User("FFF", 60));
        userRepository.save(new User("GGG", 70));
        userRepository.save(new User("HHH", 80));
        userRepository.save(new User("III", 90));
        userRepository.save(new User("JJJ", 100)); 
        // 省略后续的一些验证操作
     }
    }
    

    可以看到,在这个单元测试用例中,使用UserRepository对象连续创建了10个User实体到数据库中,下面我们人为的来制造一些异常,看看会发生什么情况。

    通过定义User的name属性长度为5,这样通过创建时User实体的name属性超长就可以触发异常产生。

    @Entity public class User {
    
      @Id
      @GeneratedValue private Long id;
    
      @Column(nullable = false, length = 5) 
      private String name;
    
      @Column(nullable = false) 
      private Integer age; 
    
      // 省略构造函数、getter和setter
     }
    

    修改测试用例中创建记录的语句,将一条记录的name长度超过5,如下:name为HHHHHHHHH的User对象将会抛出异常。

    // 创建10条记录
    userRepository.save(new User("AAA", 10));  
    userRepository.save(new User("BBB", 20));  
    userRepository.save(new User("CCC", 30));  
    userRepository.save(new User("DDD", 40));  
    userRepository.save(new User("EEE", 50));  
    userRepository.save(new User("FFF", 60));  
    userRepository.save(new User("GGG", 70));  
    userRepository.save(new User("HHHHHHHHHH", 80));  
    userRepository.save(new User("III", 90));  
    userRepository.save(new User("JJJ", 100));
    

    执行测试用例,可以看到控制台中抛出了如下异常,name字段超长:

    2016-05-27 10:30:35.948 WARN 2660 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1406, SQLState: 22001
    2016-05-27 10:30:35.948 ERROR 2660 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Data truncation: Data too long for column 'name' at row 1
    2016-05-27 10:30:35.951 WARN 2660 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Warning Code: 1406, SQLState: HY000
    2016-05-27 10:30:35.951 WARN 2660 --- [ main] o.h.engine.jdbc.spi.SqlExceptionHelper : Data too long for column 'name' at row 1
    org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.DataException: could not execute statement

    此时查数据库中,创建了name从AAA到GGG的记录,没有HHHHHHHHHH、III、JJJ的记录。而若这是一个希望保证完整性操作的情况 下,AAA到GGG的记录希望能在发生异常的时候被回退,这时候就可以使用事务让它实现回退,做法非常简单,我们只需要在test函数上添加 @Transactional 注解即可。

    @Test
    @Transactional 
    public void test() throws Exception { 
      // 省略测试内容
     }
    

    这里主要通过单元测试演示了如何使用 @Transactional 注解来声明一个函数需要被事务管理,通常我们单元测试为了保证每个测试之间的数据独立,会使用 @Rollback 注解让每个单元测试都能在结束时回滚。而真正在开发业务逻辑时,我们通常在service层接口中使用 @Transactional 来对各个业务逻辑进行事务管理的配置,例如:

    public interface UserService {
      @Transactional
      User login(String name, String password);
    }
    

    事务详解

    上面的例子中我们使用了默认的事务配置,可以满足一些基本的事务需求,但是当我们项目较大较复杂时(比如,有多个数据源等),这时候需要在声明事务时,指定不同的事务管理器。对于不同数据源的事务管理配置可以见 《Spring Boot多数据源配置与使用》 中的设置。在声明事务时,只需要通过value属性指定配置的事务管理器名即可,例如: @Transactional(value="transactionManagerPrimary")

    除了指定不同的事务管理器之后,还能对事务进行隔离级别和传播行为的控制,下面分别详细解释:

    隔离级别

    隔离级别是指若干个并发的事务之间的隔离程度,与我们开发时候主要相关的场景包括:脏读取、重复读、幻读。

    我们可以看 org.springframework.transaction.annotation.Isolation 枚举类中定义了五个表示隔离级别的值:

    public enum Isolation {  
        DEFAULT(-1),
        READ_UNCOMMITTED(1),
        READ_COMMITTED(2),
        REPEATABLE_READ(4),
        SERIALIZABLE(8);
    }
    
    • DEFAULT :这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是: READ_COMMITTED
    • READ_UNCOMMITTED :该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。
    • READ_COMMITTED :该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
    • REPEATABLE_READ :该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。
    • SERIALIZABLE :所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

    指定方法:通过使用 isolation 属性设置,例如:

    @Transactional(isolation = Isolation.DEFAULT)
    
    

    传播行为

    所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。

    我们可以看 org.springframework.transaction.annotation.Propagation 枚举类中定义了6个表示传播行为的枚举值:

    public enum Propagation {  
        REQUIRED(0),
        SUPPORTS(1),
        MANDATORY(2),
        REQUIRES_NEW(3),
        NOT_SUPPORTED(4),
        NEVER(5),
        NESTED(6);
    }
    
    • REQUIRED :如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
    • SUPPORTS :如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
    • MANDATORY :如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
    • REQUIRES_NEW :创建一个新的事务,如果当前存在事务,则把当前事务挂起。
    • NOT_SUPPORTED :以非事务方式运行,如果当前存在事务,则把当前事务挂起。
    • NEVER :以非事务方式运行,如果当前存在事务,则抛出异常。
    • NESTED :如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 REQUIRED

    指定方法:通过使用 propagation 属性设置,例如:

    @Transactional(propagation = Propagation.REQUIRED)
    

    相关文章

      网友评论

          本文标题:PlatformTransactionManager

          本文链接:https://www.haomeiwen.com/subject/wsfsqhtx.html