Spring笔记(4) - Spring的编程式事务和声明式事务详解

一.背景

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

二.事务详情

  1. 事务特性

    • 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用;
    • 一致性(Consistency):一旦事务完成(不管是成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏;
    • 隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏;
    • 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中;
  2. 事务类型

    1. 数据库分为本地事务和全局事务
      1. 本地事务:普通事务,独立一个数据库,能保证在该数据库上操作的ACID;
      2. 分布式事务:涉及两个或多个数据库源的事务,即跨越多台同类或异类数据库的事务(由每台数据库的本地事务组成),分布式事务旨在保证这些本地事务的所有操作的ACID,使事务可以跨越多台数据库;
    2. Java事务类型分为JDBC事务和JTA事务
      1. JDBC事务:即为上面说的数据库事务中的本地事务,通过connection对象控制管理;
      2. JTA事务:指Java事务API(Java Transaction API),是Java EE数据库事务规范,JTA只提供了事务管理接口,由应用程序服务器厂商(如WebSphere Application Server)提供实现,JTA事务比JDBC更强大,支持分布式事务;
    3. 按是否通过编程分为声明式事务和编程式事务
      1. 编程式事务:通过编程代码在业务逻辑时需要时自行实现,粒度更小;
      2. 声明式事务:通过注解或XML配置实现;
  3. Spring事务管理的两种方式

    • 编程式事务

        • 是侵入性事务管理,直接使用底层的PlatformTransactionManager、使用TransactionTemplate(Spring推荐使用);

        • 编程式事务管理对基于 POJO 的应用来说是唯一选择。我们需要在代码中调用beginTransaction()、commit()、rollback()等事务管理相关的方法;

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

      Spring配置文件中关于事务配置总是由三个组成部分,分别是DataSource、TransactionManager和代理机制这三部分,无论哪种配置方式,一般变化的只是代理机制这部分。

      DataSource、TransactionManager这两部分只是会根据数据访问方式有所变化,比如使用Hibernate进行数据访问时,DataSource实际为SessionFactory,TransactionManager的实现为HibernateTransactionManager。

      根据代理机制的不同,总结了五种Spring事务的配置方式,如下图:

      • 优点:

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

        • 非侵入式的开发方式,声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持;

      • 缺点:最细粒度只能是作用到方法级别,无法做到像编程事务那样可以作用到代码块级别;

      • 实现方式:

        1. 使用拦截器:基于TransactionInterceptor 类来实施声明式事务管理功能(Spring最初提供的实现方式);

        2. Bean和代理:基于 TransactionProxyFactoryBean的声明式事务管理

        3. 使用tx标签配置的拦截器:基于tx和aop名字空间的xml配置文件(基于Aspectj AOP配置事务);
        4. 全注解:基于@Transactional注解;

      • 声明式事务的约定流程:

          首先Spring通过事务管理器(PlatformTransactionManager的子类)创建事务,与此同时会把事务定义中的隔离级别、超时时间等属性根据配置内容往事务上设置。而根据传播行为配置采取一种特定的策略,后面会谈到传播行为的使用问题,这是Spring根据配置完成的内容,你只需要配置,无须编码。然后,启动开发者提供的业务代码,我们知道Spring会通过反射的方式调度开发者的业务代码,但是反射的结果可能是正常返回或者产生异常返回,那么它给的约定是只要发生异常,并且符合事务定义类回滚条件的,Spring就会将数据库事务回滚,否则将数据库事务提交,这也是Spring自己完成的。


  4. Spring事务特性

    • Spring 框架中,涉及到事务管理的 API 大约有100个左右,其中最重要的有三个:TransactionDefinition、PlatformTransactionManager、TransactionStatus。所谓事务管理,其实就是”按照给定的事务规则来执行提交或者回滚操作”。”给定的事务规则”就是用 TransactionDefinition 表示的,”按照……来执行提交或者回滚操作”便是用 PlatformTransactionManager 来表示,而 TransactionStatus 用于表示一个运行着的事务的状态。打一个不恰当的比喻,TransactionDefinition 与 TransactionStatus 的关系就像程序和进程的关系。
      • Spring所有的事务管理策略类都继承自org.springframework.transaction.PlatformTransactionManager接口,用于执行具体的事务操作。PlatformTransactionManager 接口中定义的主要方法如下:

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

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

        • DataSourceTransactionManager:适用于使用JDBC和iBatis进行数据持久化操作的情况。
        • HibernateTransactionManager:适用于使用Hibernate进行数据持久化操作的情况。
        • JpaTransactionManager:适用于使用JPA进行数据持久化操作的情况。
        • 另外还有JtaTransactionManager 、JdoTransactionManager、JmsTransactionManager等等。

                如果我们使用JTA进行事务管理,我们可以通过 JNDI 和 Spring 的 JtaTransactionManager 来获取一个容器管理的 DataSource。JtaTransactionManager 不需要知道 DataSource 和其他特定的资源,因为它将使用容器提供的全局事务管理。而对于其他事务管理器,比如DataSourceTransactionManager,在定义时需要提供底层的数据源作为其属性,也就是 DataSource。与 HibernateTransactionManager 对应的是 SessionFactory,与 JpaTransactionManager 对应的是 EntityManagerFactory 等等。

      • TransactionDefinition接口的主要方法如下:

        public interface TransactionDefinition{
            int getIsolationLevel();//返回事务的隔离级别,事务管理器依据它来控制另外一个事务能够看到本事务内的哪些数据。
            int getPropagationBehavior();//返回事务的传播行为,由是否有一个活动的事务来决定一个事务调用。
            int getTimeout();//它返回事务必须在多少秒内完毕。
            boolean isReadOnly();//事务是否仅仅读,事务管理器可以依据这个返回值进行优化。确保事务是仅仅读的。
        }

          也许你会奇怪,为什么接口只提供了获取属性的方法,而没有提供相关设置属性的方法。其实道理很简单,事务属性的设置完全是程序员控制的,因此程序员可以自定义任何设置属性的方法,而且保存属性的字段也没有任何要求。唯一的要求的是,Spring 进行事务操作的时候,通过调用以上接口提供的方法必须能够返回事务相关的属性取值。

      • TransactionStatus:PlatformTransactionManager.getTransaction(…) 方法返回一个 TransactionStatus 对象,表示一个事务的状态。返回的TransactionStatus 对象可能代表一个新的或已经存在的事务(如果在当前调用堆栈有一个符合条件的事务)。TransactionStatus 接口提供了一个简单的控制事务执行和查询事务状态的方法。比如当前调用栈中之前已经存在了一个事务,那么就是通过该接口来判断的,TransactionStatus接口可以让事务管理器控制事务的执行,比如检查事务是否为一个新事务,或者是否只读,TransactionStatus还可以初始化回滚操作。

        TransactionStatus 接口中定义的主要方法如下:

        public interface TransactionStatus extends SavepointManager, Flushable {
        
            //是否是一个新的事务
            boolean isNewTransaction();
        
            //判断是否有回滚点
            boolean hasSavepoint();
        
            //将一个事务标识为不可提交的。在调用完setRollbackOnly()后只能被回滚
            //在大多数情况下,事务管理器会检测到这一点,在它发现事务要提交时会立刻结束事务。
            //调用完setRollbackOnly()后,数数据库可以继续执行select,但不允许执行update语句,因为事务只可以进行读取操作,任何修改都不会被提交。
            void setRollbackOnly();
            boolean isRollbackOnly();
        
            @Override
            void flush();
            //判断事务是否已经完成
            boolean isCompleted();
        }
    • TransactionDefinition接口定义以下特性:

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

      1. TransactionDefinition.ISOLATION_DEFAULT:默认值-1,表示使用底层数据库的默认隔离级别,对大部分数据库而言,通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED;

      2. TransactionDefinition.ISOLATION_READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据,该级别可能导致脏读、不可重复读和幻读,因此很少使用该隔离级别,比如PostgreSQL实际上并没有此级别;

      3. TransactionDefinition.ISOLATION_READ_COMMITTED:(Oracle默认级别)该隔离级别表示一个事务只能读取另一个事务已经提交的数据,即允许从已经提交的并发事务读取,该级别可以防止脏读,但幻读和不可重复读仍可能会发生;

      4. TransactionDefinition.ISOLATION_REPEATABLE_READ:(MySQL默认级别)该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同,即对相同字段的多次读取的结果是一致的,除非数据被当前事务本事改变。该级别可以防止脏读和不可重复读,但幻读仍可能发生;

      5. TransactionDefinition.ISOLATION_SERIALIZABLE:(完全服从ACID的隔离级别)所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读和幻读,但严重影响程序的性能,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的;

      • 脏读(Dirty read):发生在一个事务读取了被另一个事务改写但尚未提交的数据时。如果这些改变在稍后被回滚了,那么第一个事务读取的数据就会是无效的;

        不可重复读(Nonrepeatable read):发生在一个事务执行相同的查询两次或两次以上,但每次查询结果都不相同时。这通常是由于另一个并发事务在两次查询之间更新了数据。(不可重复读重点在修改)

        幻读(Phantom reads):幻读和不可重复读相似。当一个事务(T1)读取几行记录后,另一个并发事务(T2)插入了一些记录时,幻读就发生了。在后来的查询中,第一个事务(T1)就会发现一些原来没有的额外记录。(幻读重点在新增或删除)

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

      1. TransactionDefinition.PROPAGATION_REQUIRED:默认值,能满足绝大部分业务需求,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行;

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

      3. TransactionDefinition.PROPAGATION_SUPPORTS:如果外层有事务,则加入外层事务;如果外层没有事务,则直接以非事务的方式继续运行。完全依赖外层的事务;

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

      5. TransactionDefinition.PROPAGATION_NEVER:该传播机制不支持外层事务,即如果外层有事务就抛出异常;

      6. TransactionDefinition.PROPAGATION_MANDATORY:与NEVER相反,如果外层有事务,则加入外层事务,如果外层没有事务,则抛出异常;

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

      • 传播机制回答了这样一个问题:一个新的事务应该被启动还是被挂起,或者是一个方法是否应该在事务性上下文中运行。
      • 这里需要指出的是,前面的六种事务传播行为是 Spring 从 EJB 中引入的,他们共享相同的概念。而 PROPAGATION_NESTED是 Spring 所特有的。以 PROPAGATION_NESTED 启动的事务内嵌于外部事务中(如果存在外部事务的话),此时,内嵌事务并不是一个独立的事务,它依赖于外部事务的存在,只有通过外部的事务提交,才能引起内部事务的提交,嵌套的子事务不能单独提交。如果熟悉 JDBC 中的保存点(SavePoint)的概念,那嵌套事务就很容易理解了,其实嵌套的子事务就是保存点的一个应用,一个事务中可以包括多个保存点,每一个嵌套子事务。另外,外部事务的回滚也会导致嵌套子事务的回滚。
      • 挂起事务,指的是将当前事务的属性如事务名称,隔离级别等属性保存在一个变量中,同时将当前线程中所有和事务相关的ThreadLocal变量设置为从未开启过线程一样。Spring维护着一个当前线程的事务状态,用来判断当前线程是否在一个事务中以及在一个什么样的事务中,挂起事务后,当前线程的事务状态就好像没有事务。
    3. 只读:如果一个事务只对数据库执行读操作,那么该数据库就可能利用那个事务的只读特性,采取某些优化措施。通过把一个事务声明为只读,可以给后端数据库一个机会来应用那些它认为合适的优化措施。由于只读的优化措施是在一个事务启动时由后端数据库实施的,因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、 ROPAGATION_NESTED)的方法来说,将事务声明为只读才有意义,在 TransactionDefinition 中以 boolean 类型来表示该事务是否只读。;
    4. 事务超时:指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务,在TransactionDefinition中以int的值来表示超时时间,其单位是秒;默认设置为底层事务系统的超时值,如果底层数据库事务系统没有设置超时值,那么就是none,没有超时限制;

    5. Spring事务回滚规则:默认配置下,Spring只有在抛出的异常为运行时异常(runtime exception)时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Error也会导致事务回滚),而抛出受检查异常(checked exception)则不会导致事务回滚,不过可以声明在抛出哪些异常时回滚事务,包括checked异常,也可以声明哪些异常抛出时不回滚事务,即使异常是运行时异常,还可以编程性的通过setRollbackOnly()方法来指示一个事务必须回滚,在调用完setRollbackOnly()后你所能执行的唯一操作就是回滚;

      • 事务回滚异常只能为RuntimeException异常,而Checked Exception异常不回滚,捕获异常不抛出也不会回滚,但可以强制事务回滚:TransactionAspectSupport.currentTransactionStatus().isRollbackOnly();
      • 解决“自我调用”而导致的不能设置正确的事务属性问题,可参考http://www.iteye.com/topic/1122740

三.事务实现

  1. 编程式事务的实现

    1. PlatformTransactionManager代码实现步骤:获取事务管理器;创建事务属性对象;获取事务状态对象;创建JDBC模板对象;业务数据操作处理;
      public class test {
          @Resource
          private PlatformTransactionManager txManager;
          @Resource
          private  DataSource dataSource;
          private static JdbcTemplate jdbcTemplate;
          @Test
          public void testdelivery(){
              //定义事务隔离级别,传播行为,
              DefaultTransactionDefinition def = new DefaultTransactionDefinition();  
              def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);  
              def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);  
              //事务状态类,通过PlatformTransactionManager的getTransaction方法根据事务定义获取;获取事务状态后,Spring根据传播行为来决定如何开启事务
              TransactionStatus status = txManager.getTransaction(def);  
              jdbcTemplate = new JdbcTemplate(dataSource);
              try {  
                  jdbcTemplate.update("insert into testtranstation(sd) values(?)", "1");  
                   //提交status中绑定的事务
                  txManager.commit(status); 
              } catch (RuntimeException e) {  
                  //回滚
                  txManager.rollback(status);  
              } 
          }
          
      }

      如上所示,我们在类中增加了两个属性:一个是 TransactionDefinition 类型的属性,它用于定义一个事务;另一个是 PlatformTransactionManager 类型的属性,用于执行事务管理操作。

      如果方法需要实施事务管理,我们首先需要在方法开始执行前启动一个事务,调用PlatformTransactionManager.getTransaction(…) 方法便可启动一个事务。创建并启动了事务之后,便可以开始编写业务逻辑代码,然后在适当的地方执行事务的提交或者回滚。

    2. 使用TransactionTemplate,该类继承了DefaultTransactionDefinition,用于简化事务管理,事务管理由模板定义,主要是通过TransactionCallback回调接口或TransactionCallbackWithoutResult回调接口指定,通过调用模板类的参数类型为TransactionCallback或TransactionCallbackWithoutResult的execute方法来自动享受事务管理。
      • TransactionCallback:通过实现该接口的“T doInTransaction(TransactionStatus status)”方法来定义需要事务管理的操作代码;
      • TransactionCallbackWithoutResult:继承TransactionCallback接口,提供“void doInTransactionWithoutResult(TransactionStatus status)”便利接口用于方便哪些不需要返回值的事务操作代码;
      1. TransactionCallback的配置、代码实现步骤:获取模板对象;选择事务结果类型;业务数据操作处理;
        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans" 
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:tx="http://www.springframework.org/schema/tx"
            xsi:schemaLocation="
                http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
         
            <description>数据源及事务配置</description>
         
            <!-- 数据源配置 -->
            <!-- 代理datasource,使其能够显式获取preparedStatement的参数值 -->
            <bean id="proxyDataSource" class="org.jdbcdslog.ConnectionPoolDataSourceProxy">
                <property name="targetDSDirect" ref="dataSource"/>
            </bean>
         
            <!-- 配置事务管理器 -->
            <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
                <property name="dataSource" ref="proxyDataSource" />
            </bean>
         
            <!--事务模板 -->  
            <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">  
                <property name="transactionManager" ref="transactionManager"/>  
                <!--ISOLATION_DEFAULT 表示由使用的数据库决定  -->  
                <property name="isolationLevelName" value="ISOLATION_DEFAULT"/>  
                <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED" />  
                <!-- <property name="timeout" value="30"/> -->  
            </bean> 
         
            <!-- 注解方式配置事物 -->
            <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
        </beans>
        package com.hrh.initialize;
         
        import org.springframework.beans.factory.InitializingBean;
        import org.springframework.jdbc.core.JdbcTemplate;
        import org.springframework.transaction.TransactionStatus;
        import org.springframework.transaction.support.TransactionCallback;
        import org.springframework.transaction.support.TransactionTemplate;
         
        public class DataInitializer implements InitializingBean{
         
            private TransactionTemplate transactionTemplate;
         
            private JdbcTemplate jdbcTemplate;
            @Override
            public void afterPropertiesSet() throws Exception {
         
                transactionTemplate.execute(new TransactionCallback<Object>() {
                    @Override
                    public Object doInTransaction(TransactionStatus status) {
                        //创建保存点
                        Object savepoint = status.createSavepoint();
                        // DML执行
                        try {
                            jdbcTemplate.execute("truncate table SET_RESOURCE");
                            jdbcTemplate.execute(String.format("INSERT INTO SET_RESOURCE VALUES ('%s','%s','%s','%s',%s,%s)", 
                                    "100", "cAuthc", "/sample/component.html", "1", "null", "null"));
                            jdbcTemplate.execute(String.format("INSERT INTO SET_RESOURCE VALUES ('%s','%s','%s','%s',%s,%s)", 
                                    "992", "cAuthc", "/mt/rgroup/rgroup_read.html", "1", "null", "null"));
                        } catch (Throwable e) {
                            LOG.error("Error occured, cause by: {}", e.getMessage());
                            //通过TransactionStatus的setRollbackOnly()或rollbackToSavepoint(savepoint) 控制事务
                            status.setRollbackOnly();
                            // status.rollbackToSavepoint(savepoint);
                        }
                        return null;
                    }
                });
            }
         
            public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
                this.transactionTemplate = transactionTemplate;
            }
         
            public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
                this.jdbcTemplate = jdbcTemplate;
            }  
         
        }
      2. TransactionCallbackWithoutResult代码实现:
            @Override
            public void afterPropertiesSet() throws Exception {
                transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                    @Override
                    protected void doInTransactionWithoutResult(TransactionStatus status) {
                        //字段sd为int型,所以插入肯定失败报异常,自动回滚,代表TransactionTemplate自动管理事务
                        jdbcTemplate.update("insert into testtranstation(sd) values(?)", "饿死");   
                    }}
                );
            }
      • 总结

        TransactionTemplate 的 execute() 方法有一个 TransactionCallback 类型的参数,该接口中定义了一个 doInTransaction() 方法,通常我们以匿名内部类的方式实现 TransactionCallback 接口,并在其 doInTransaction() 方法中书写业务逻辑代码。这里可以使用默认的事务提交和回滚规则,这样在业务代码中就不需要显式调用任何事务管理的 API。doInTransaction() 方法有一个TransactionStatus 类型的参数,我们可以在方法的任何位置调用该参数的 setRollbackOnly() 方法将事务标识为回滚的,以执行事务回滚。

        根据默认规则,如果在执行回调方法的过程中抛出了未检查异常,或者显式调用了TransacationStatus.setRollbackOnly() 方法,则回滚事务;如果事务执行完成或者抛出了 checked 类型的异常,则提交事务。

        TransactionCallback 接口有一个子接口 TransactionCallbackWithoutResult,该接口中定义了一个 doInTransactionWithoutResult() 方法,TransactionCallbackWithoutResult 接口主要用于事务过程中不需要返回值的情况。当然,对于不需要返回值的情况,我们仍然可以使用 TransactionCallback 接口,并在方法中返回任意值即可。

  2. 声明式事务的实现

    1. 基于TransactionInterceptor 类来实施声明式事务管理功能:Spring最初提供的实现方式
      • 配置文件:
        <beans...>
        ......
            <bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
                <property name="transactionManager" ref="transactionManager"/>
                <property name="transactionAttributes">
                    <props>
                        <prop key="add*">PROPAGATION_REQUIRED</prop>
                    </props>
                </property>
            </bean>
            <bean id="buyStockServiceTarget" class="footmark.spring.core.tx.declare.origin.BuyStockServiceImpl">
                <property name="stockDao" ref="stockDao"/>
            </bean>

        首先,我们配置了一个 TransactionInterceptor 来定义相关的事务规则,它有两个主要的属性:一个是 transactionManager,用来指定一个事务管理器,并将具体事务相关的操作委托给它;另一个是 Properties 类型的 transactionAttributes 属性,它主要用来定义事务规则,该属性的每一个键值对中,键指定的是方法名,方法名可以使用通配符,而值就表示相应方法的所应用的事务属性。

        指定事务属性的取值有较复杂的规则,这在 Spring 中算得上是一件让人头疼的事。具体的书写规则如下:

        传播行为 [,隔离级别] [,只读属性] [,超时属性] [不影响提交的异常] [,导致回滚的异常]

        • 超时属性的取值必须以”TIMEOUT_”开头,后面跟一个int类型的值,表示超时时间,单位是秒。
        • 不影响提交的异常是指,即使事务中抛出了这些类型的异常,事务任然正常提交。必须在每一个异常的名字前面加上”+”。异常的名字可以是类名的一部分。比如”+RuntimeException”、”+tion”等等。
        • 导致回滚的异常是指,当事务中抛出这些类型的异常时,事务将回滚。必须在每一个异常的名字前面加上”-”。异常的名字可以是类名的全部或者部分,比如”-RuntimeException”、”-tion”等等。

         实例:

        <property name="*Service">
        PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED,TIMEOUT_20,
        +AbcException,+DefException,-HijException
        </property>
        以上表达式表示,针对所有方法名以 Service 结尾的方法,使用 PROPAGATION_REQUIRED 事务传播行为,事务的隔离级别是 ISOLATION_READ_COMMITTED,超时时间为20秒,当事务抛出 AbcException 或者 DefException 类型的异常,则仍然提交,当抛出 HijException 类型的异常时必须回滚事务。这里没有指定”readOnly”,表示事务不是只读的。

        1)配置好了 TransactionInterceptor,我们还需要配置一个 ProxyFactoryBean 来组装 target 和advice。这也是典型的 Spring AOP 的做法。通过 ProxyFactoryBean 生成的代理类就是织入了事务管理逻辑后的目标类。

        <bean id="buyStockService" class="org.springframework.aop.framework.ProxyFactoryBean">
            <property name="target" ref="buyStockServiceTarget"/>
            <property name="interceptorNames">
                <list>
                    <idref bean="transactionInterceptor"/>
                </list>
            </property>
        </bean>

        2)除了使用上面的ProxyFactoryBean来组装代理类表示哪些类需要使用到事务拦截器外,还可以使用BeanNameAutoProxyCreator告诉Spring哪些类要使用事务拦截器进行拦截:

        <!--指明事务拦截器拦截哪些类-->
        <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
            <property name="beanNames">
                <list>
                    <value>*ServiceImpl</value>
                </list>
            </property>
            <property name="interceptorNames">
                <list>
                    <value>transactionInterceptor</value>
                </list>
            </property>
        </bean>

        BeanName属性告诉Spring如何拦截类。由于声明为*ServiceImpl,所有关于Service是现实类都会被其拦截,然后interceptorNames则是定义事务拦截器,这样对应的类和方法就会被事务管理器所拦截了。

        至此,声明式事务管理就算是实现了。我们没有对业务代码进行任何操作,所有设置均在配置文件中完成,这就是声明式事务的最大优点。

    2. 基于 TransactionProxyFactoryBean的声明式事务管理:

      前面的声明式事务虽然好,但是却存在一个非常恼人的问题:配置文件太多。我们必须针对每一个目标对象配置一个 ProxyFactoryBean;另外,虽然可以通过父子 Bean 的方式来复用 TransactionInterceptor 的配置,但是实际的复用几率也不高;这样,加上目标对象本身,每一个业务类可能需要对应三个 <bean/> 配置,随着业务类的增多,配置文件将会变得越来越庞大,管理配置文件又成了问题。

      为了缓解这个问题,Spring 为我们提供了 TransactionProxyFactoryBean,用于将TransactionInterceptor 和 ProxyFactoryBean 的配置合二为一。

      • 下面是每个Bean都有一个代理的实现:

        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:context="http://www.springframework.org/schema/context"
            xmlns:mvc="http://www.springframework.org/schema/mvc"
            xmlns:aop="http://www.springframework.org/schema/aop"
            xmlns:tx="http://www.springframework.org/schema/tx"
            xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
                http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
                http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
                http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-aop-4.2.xsd
                ">
            
            <context:property-placeholder location="classpath:jdbc.properties"/>
            
            <!-- 注册数据源 C3P0 -->
            <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"  >
                 <property name="driverClass" value="${jdbc.driverClass}"></property>
                 <property name="jdbcUrl"  value="${jdbc.url}"></property>
                 <property name="user"  value="${jdbc.username}"></property>
                 <property name="password" value="${jdbc.password}"></property>
            </bean>
            
            <bean id="accountDao" class="com.hrh.dao.impl.AccountDaoImpl">
                <property name="dataSource" ref="dataSource"/>
            </bean>
            
            <bean id="stockDao" class="com.hrh.dao.impl.StockDaoImpl">
                <property name="dataSource" ref="dataSource"/>
            </bean>
            
            <bean id="buyStockService" class="com.hrh.service.impl.BuyStockServiceImpl">
                <property name="accountDao" ref="accountDao"></property>
                <property name="stockDao" ref="stockDao"></property>
            </bean>
            
            
            <!-- 事务管理器 -->
            <bean id="myTracnsactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
                <property name="dataSource" ref="dataSource"></property>
            </bean>
            
            <!-- 事务代理工厂 -->
            <!-- 生成事务代理对象 -->
            <bean id="serviceProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
                <property name="transactionManager" ref="myTracnsactionManager"></property>
                <property name="target" ref="buyStockService"></property>
                <property name="transactionAttributes">
                    <props>
                        <!-- 主要 key 是方法   
                            ISOLATION_DEFAULT  事务的隔离级别
                            PROPAGATION_REQUIRED  传播行为
                        -->
                        <prop key="add*">ISOLATION_DEFAULT,PROPAGATION_REQUIRED</prop>
                        <!-- -Exception 表示发生指定异常回滚,+Exception 表示发生指定异常提交 -->
                        <prop key="buyStock">ISOLATION_DEFAULT,PROPAGATION_REQUIRED,-BuyStockException</prop>
                    </props>
                </property>
                
            </bean>
            
            
        </beans>

        如此一来,配置文件与先前相比简化了很多。我们把这种配置方式称为 Spring 经典的声明式事务管理。

        但是,显式为每一个业务类配置一个 TransactionProxyFactoryBean 的做法将使得代码显得过于刻板,为此我们可以使用自动创建代理的方式来将其简化(使用自动创建代理是纯 AOP 知识)。

      • 所有Bean共享一个代理基类的实现:
        <?xml version="1.0" encoding="UTF-8"?>
        <beans xmlns="http://www.springframework.org/schema/beans"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:context="http://www.springframework.org/schema/context"
            xmlns:mvc="http://www.springframework.org/schema/mvc"
            xmlns:aop="http://www.springframework.org/schema/aop"
            xmlns:tx="http://www.springframework.org/schema/tx"
            xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
                http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
                http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
                http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-aop-4.2.xsd
                ">
            
            <context:property-placeholder location="classpath:jdbc.properties"/>
            
            <!-- 注册数据源 C3P0 -->
            <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"  >
                 <property name="driverClass" value="${jdbc.driverClass}"></property>
                 <property name="jdbcUrl"  value="${jdbc.url}"></property>
                 <property name="user"  value="${jdbc.username}"></property>
                 <property name="password" value="${jdbc.password}"></property>
            </bean>
            
            <bean id="accountDao" class="com.hrh.dao.impl.AccountDaoImpl">
                <property name="dataSource" ref="dataSource"/>
            </bean>
            
            <bean id="stockDao" class="com.hrh.dao.impl.StockDaoImpl">
                <property name="dataSource" ref="dataSource"/>
            </bean>
            
            <bean id="buyStockService" class="com.hrh.service.impl.BuyStockServiceImpl">
                <property name="accountDao" ref="accountDao"></property>
                <property name="stockDao" ref="stockDao"></property>
            </bean>
            
            
            <!-- 事务管理器 -->
            <bean id="myTracnsactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
                <property name="dataSource" ref="dataSource"></property>
            </bean>
            
            <bean id="transactionBase"  
                    class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"  
                    lazy-init="true" abstract="true">  
                <!-- 配置事务管理器 -->  
                <property name="transactionManager" ref="myTracnsactionManager" />  
                <!-- 配置事务属性 -->  
                <property name="transactionAttributes">  
                    <props>  
                        <prop key="*">PROPAGATION_REQUIRED</prop>  
                    </props>  
                </property>  
            </bean>    
           <!-- 共享基类 -->
            <bean id="serviceProxy" parent="transactionBase" >  
                <property name="target" ref="buyStockService" />   
            </bean>
        </beans>
    3. 以MyBatis为例,基于.xml文件的声明式事务配置(基于Aspectj AOP配置事务),通过使用Spring的<tx:advice>定义事务通知与AOP相关配置实现

      前面两种声明式事务配置方式奠定了 Spring 声明式事务管理的基石。在此基础上,Spring 2.x 引入了 <tx> 命名空间,结合使用 <aop> 命名空间,带给开发人员配置声明式事务的全新体验,配置变得更加简单和灵活。另外,得益于 <aop> 命名空间的切点表达式支持,声明式事务也变得更加强大。

      <!-- 
      <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="advice" transaction-manager="transactionManager">
          <tx:attributes>
              <!-- 拦截save开头的方法,事务传播行为为:REQUIRED:必须要有事务, 如果没有就在上下文创建一个 -->
              <tx:method name="save*" propagation="REQUIRED" isolation="READ_COMMITTED" timeout="" read-only="false" no-rollback-for="" rollback-for="java.lang.Exception"/>
              <!-- 支持,如果有就有,没有就没有 -->
              <tx:method name="*" propagation="SUPPORTS"/>
          </tx:attributes>
      </tx:advice>
      <!-- 定义切入点,expression为切人点表达式,如下是指定impl包下的所有方法,具体以自身实际要求自定义  -->
      <aop:config>
          <aop:pointcut expression="execution(* com.hrh.*.service.impl.*.*(..))" id="pointcut"/>
          <!--<aop:advisor>定义切入点,与通知,把tx与aop的配置关联,才是完整的声明事务配置 -->
          <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
      </aop:config>

      由于使用了切点表达式,我们就不需要针对每一个业务类创建一个代理对象了。另外,如果配置的事务管理器 Bean 的名字取值为 “transactionManager”,则我们可以省略 <tx:advice> 的 transaction-manager 属性,因为该属性的默认值即为 “transactionManager”。

    4. 以MyBatis为例,基于注解的声明式事务配置,通过@Transactional实现事务管理

      除了基于命名空间的事务配置方式,Spring 2.x 还引入了基于 Annotation 的方式,具体主要涉及@Transactional 标注。

      1. 添加tx名字空间
        xmlns:tx="http://www.springframework.org/schema/tx"
      2. 开启事务的注解支持:Spring 使用 BeanPostProcessor 来处理 Bean 中的标注,因此我们需要在配置文件中作如下声明来激活该后处理 Bean,
        <!-- 开启事务控制的注解支持 -->  
        <tx:annotation-driven transaction-manager="transactionManager"/>
      3. MyBatis自动参与到Spring事务管理中,无需额外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的数据源与DataSourceTransactionManager引用的数据源一致即可
        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> 
            <property name="dataSource" ref="dataSource" />  
            <property name="configLocation">  
                <value>classpath:mybatis-config.xml</value>  
            </property>  
        </bean> 
        
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
            <property name="dataSource" ref="dataSource" />  
        </bean>
      4. 使用@Transactional注解                              

        @Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法都将具有该类型的事务属性,同时,我们也可以在方法级别使用该注解来覆盖类级别的定义。

        虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外,@Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果在 protected、private 或者默认可见性的方法上使用@Transactional注解,这将被忽略,也不会抛出任何异常。

        • 注意:
          • 如果在接口、实现类或方法上都指定了@Transactional 注解,则优先级顺序为方法>实现类>接口;
          • 建议只在实现类或实现类的方法上使用@Transactional,而不要在接口上使用,这是因为如果使用JDK代理机制(基于接口的代理)是没问题;而使用使用CGLIB代理(继承)机制时就会遇到问题,因为其使用基于类的代理而不是接口,这是因为接口上的@Transactional注解是“不能继承的”;
          • 默认情况下,只有来自外部的方法调用才会被AOP代理捕获,也就是,类内部方法调用本类内部的其他方法并不会引起事务行为,即使被调用方法使用@Transactional注解进行修饰。
        • @Transactional(timeout = 60)

            如果用这个注解描述一个方法的话,线程已经跑到方法里面,如果已经过去60秒了还没跑完这个方法并且线程在这个方法中的后面还有涉及到对数据库的增删改查操作时会报事务超时错误(会回滚)。
            如果已经过去60秒了还没跑完但是后面已经没有涉及到对数据库的增删改查操作,那么这时不会报事务超时错误(不会回滚)。
           
        • Spring管理事务默认回滚的异常是什么? 
            答案是: RuntimeException或者Error。 
            注意:如果事务在try{}catch(Exception e){e.printStackTrace();}中跑,并且catch中只是打印e的话,那么事务不会rollback。因为异常被catch掉了,框架不知道发生了异常。
            如果想要rollback,可以加上rollbackFor=Exception.class,然后:
              ①在方法上添加 throws  Exception,将方法中出现的异常抛出给spring事务,
              ②去掉方法体中的try catch
              ③catch (Exception e) {  throw e;}继续向上抛,目的是让spring事务捕获这个异常。
                rollbackFor=Exception.class,catch(){
                     throw new RunTimeException();
                }
            如果不加rollbackFor=Exception.class,抛出new Exception() 是不会回滚的。Spring源码如下:
              public boolean rollbackOn(Throwable ex) { 
                   return (ex instanceof RuntimeException || ex instanceof Error);
              } 
            如果是RuntimeException或Error的话,就返回True,表示要回滚,否则返回False,表示不回滚。
            只有spring事务捕获到Exception异常后,@Transactional(rollbackFor=Exception.class),才会起到应有的作用;catch (Exception e) {            e.printStackTrace();        }这句是捕获try中出现的Exception然后将异常信息打印出来,仅仅是打印出来,然后什么也没干。

            @Transactional(timeout = 60,rollbackFor=Exception.class)与rollbackFor=Exception.class的作用是
              1. 让checked例外也回滚:在整个方法前加上 @Transactional(rollbackFor=Exception.class)
              2. 让unchecked例外不回滚: @Transactional(notRollbackFor=RunTimeException.class)
            checked Unchecked exception是运行时错误。
    5. 基于 <tx> 命名空间和基于 @Transactional 的事务声明方式各有优缺点。基于 <tx> 的方式,其优点是与切点表达式结合,功能强大。利用切点表达式,一个配置可以匹配多个方法,而基于 @Transactional 的方式必须在每一个需要使用事务的方法或者类上用 @Transactional 标注,尽管可能大多数事务的规则是一致的,但是对 @Transactional 而言,也无法重用,必须逐个指定。另一方面,基于 @Transactional 的方式使用起来非常简单明了,没有学习成本。开发人员可以根据需要,任选其中一种使用,甚至也可以根据需要混合使用这两种方式。

      如果不是对遗留代码进行维护,则不建议再使用基于 TransactionInterceptor 以及基于TransactionProxyFactoryBean 的声明式事务管理方式。

四.总结

    • 基于 TransactionDefinition、PlatformTransactionManager、TransactionStatus 编程式事务管理是 Spring 提供的最原始的方式,通常我们不会这么写,但是了解这种方式对理解 Spring 事务管理的本质有很大作用。
    • 基于 TransactionTemplate 的编程式事务管理是对上一种方式的封装,使得编码更简单、清晰。
    • 基于 TransactionInterceptor 的声明式事务是 Spring 声明式事务的基础,通常也不建议使用这种方式,但是与前面一样,了解这种方式对理解 Spring 声明式事务有很大作用。
    • 基于 TransactionProxyFactoryBean 的声明式事务是上中方式的改进版本,简化的配置文件的书写,这是 Spring 早期推荐的声明式事务管理方式,但是在 Spring 2.0 中已经不推荐了。
    • 基于 <tx> 和 <aop> 命名空间的声明式事务管理是目前推荐的方式,其最大特点是与 Spring AOP 结合紧密,可以充分利用切点表达式的强大支持,使得管理事务更加灵活。
    • 基于 @Transactional 的方式将声明式事务管理简化到了极致。开发人员只需在配置文件中加上一行启用相关后处理 Bean 的配置,然后在需要实施事务管理的方法或者类上使用 @Transactional 指定事务规则即可实现事务管理,而且功能也不必其他方式逊色。
posted @ 2020-10-20 22:05  码猿手  阅读(7326)  评论(0编辑  收藏  举报
Live2D