zoukankan      html  css  js  c++  java
  • (转)使用Spring注解方式管理事务与传播行为详解

    http://blog.csdn.net/yerenyuan_pku/article/details/52885041

    使用Spring注解方式管理事务

    前面讲解了怎么使用@Transactional注解声明PersonServiceBean底下所有的业务方法需要事务管理,那么事务是如何来管理的呢? 
    我们知道当每个业务方法执行的时候,它都会打开事务,在业务方法执行结束之后,它就会结束事务。那么它什么时候决定这个事务提交,什么时候决定这个事务回滚呢?原先我们手工控制事务的时候,通常这个事务的提交或回滚是由我们来操纵的,那现在我们采用容器的声明式事务管理,那我们如何知道事务什么时候提交,什么时候回滚呢?答案是:Spring容器默认情况下对运行时异常,它会进行事务的回滚,如果它碰到的是用户异常,如检查时异常(checked exception),这时事务就不会回滚。 
    现在我们就来做一个实验。假设person表里面有如下记录: 
     
    如果现在我们要删除person表中id为5的记录,但是在PersonServiceBean类的delete()方法中,人为地抛出一个运行时异常,如下:

    public void delete(Integer personid) {
        jdbcTemplate.update("delete from person where id=?", new Object[]{personid}, 
                new int[]{java.sql.Types.INTEGER});
    
        throw new RuntimeException("运行期异常");
    }

    此时测试PersonServiceTest类中的delete()方法:

    @Test
    public void delete() {
        personService.delete(5);
    }

    会发现Eclipse控制台打印出一个异常,立马查看person表,发现id为5的记录没有删除掉,这就说明了Spring容器默认情况下对运行时异常,它会进行事务的回滚。 
    如果在PersonServiceBean类的delete()方法中,人为地抛出一个检查时异常,如下:

    public void delete(Integer personid) throws Exception {
        jdbcTemplate.update("delete from person where id=?", new Object[]{personid}, 
                new int[]{java.sql.Types.INTEGER});
    
        throw new Exception("检查时异常");
    }
    • 1

    为了不报错,我们还须将PersonService接口中的delete()方法签名修改为:

    /**
     * 删除指定id的person
     */
    public void delete(Integer personid) throws Exception;

    此时测试PersonServiceTest类中的delete()方法:

    @Test
    public void delete() {
        try {
            personService.delete(5);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    会发现Eclipse控制台打印出一个异常,立马查看person表,发现id为5的记录被删除掉,这就说明了如果Spring容器碰到的是用户异常,如检查时异常(checked exception),这时事务就不会回滚。 
    当然我们也可改变这种规则:

    • 当Spring容器碰到用户异常——如检查时异常(checked exception)时,让事务回滚。 
      那到底该怎么办呢?此时就需要用到事务的rollbackFor属性了。我们将PersonServiceBean类的delete()方法修改为:

      @Transactional(rollbackFor=Exception.class)
      public void delete(Integer personid) throws Exception {
          jdbcTemplate.update("delete from person where id=?", new Object[]{personid}, 
                  new int[]{java.sql.Types.INTEGER});
      
          throw new Exception("检查时异常");
      }
      • 1

      此时测试PersonServiceTest类中的delete()方法:

      @Test
      public void delete() {
          try {
              personService.delete(4);
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
      • 1

      会发现Eclipse控制台打印出一个异常,立马查看person表,发现id为4的记录没有被删除掉。

    • 当Spring容器碰到运行时异常时,不让它进行事务的回滚,而是提交事务。 
      那到底该怎么办呢?此时就需要用到事务的rollbackFor属性了。我们将PersonServiceBean类的delete()方法修改为:

      @Transactional(noRollbackFor=RuntimeException.class)
      public void delete(Integer personid) throws Exception {
          jdbcTemplate.update("delete from person where id=?", new Object[]{personid}, 
                  new int[]{java.sql.Types.INTEGER});
      
          throw new RuntimeException("运行期异常");
      }
      • 1

      此时测试PersonServiceTest类中的delete()方法, 会发现Eclipse控制台打印出一个异常,立马查看person表,发现id为4的记录已经被删除掉了。

    事务传播属性

    事务还有一些其他的特点,如在业务bean——PersonServiceBean中,有些业务方法是不需要进行事务管理的,比方说获取数据的方法,那么这个时候我们就需要用到事物的propagation属性了,该属性指定了事务的传播行为。所以,我们应将getPerson()方法的代码修改为:

    /**
     * 使用JdbcTemplate获取一条记录
     */
    @Transactional(propagation=Propagation.NOT_SUPPORTED)
    public Person getPerson(Integer personid) {
        return jdbcTemplate.queryForObject("select * from person where id=?", new Object[]{personid}, 
                new int[]{java.sql.Types.INTEGER}, new PeronRowMapper());
    }
    • 1

    还要将getPersons()方法的代码修改为:

    /**
     * 使用JdbcTemplate获取多条记录
     */
    @Transactional(propagation=Propagation.NOT_SUPPORTED)
    public List<Person> getPersons() {
        return jdbcTemplate.query("select * from person", new PeronRowMapper());
    }
    • 1

    这样,当这2个业务方法执行的时候,它都不会开启事务,能节约资源,提供效率。 
    下面,我们就来对事务传播属性做一个总结:

    • REQUIRED:业务方法需要在一个事务中运行。如果方法运行时,已经处在一个事务中,那么这个时候就会加入到该事务中,如果当前没有事务环境的话,就会为自己创建一个新的事务。默认情况下,业务方法的事务传播属性就是REQUIRED。在应用开发中,80%的情况下都会使用这种事务传播属性。
    • NOT_SUPPORTED:声明方法不需要事务。如果方法没有关联到一个事务,容器不会为它开启事务。如果方法在一个事务中被调用(在其他业务bean的方法中被调用了,而其他业务bean的方法是开启了事务的),该事务会被挂起,在方法调用结束后,原先的事务便会恢复执行。
    • REQUIRESNEW:该属性表明不管当前是否存在事务,业务方法总会为自己发起一个新的事务。如果方法已经运行在一个事务中,则原有事务会被挂起,新的事务会被创建,直到方法执行结束,新事务才算结束,原先的事务才会恢复执行。
    • MANDATORY:该属性指定业务方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务。如果业务方法在没有事务的环境下调用,容器就会抛出异常。
    • SUPPORTS:这一事务属性表明,如果业务方法在某个事务范围内被调用,则方法成为该事务的一部分。如果业务方法在事务范围外被调用,则方法在没有事务的环境下执行。即当标注了事务传播属性——SUPPORTS的业务方法在另一个bean的业务方法中执行时,如果另一个bean的业务方法开启了事务,那么执行到标注了事务传播属性——SUPPORTS的业务方法时,它就会处在事务中执行,如果另一个bean的业务方法也没开启事务,那么标注了事务传播属性——SUPPORTS的业务方法也在没有事务的环境中进行
    • Never:指定业务方法绝对不能在事务范围内执行。如果业务方法在某个事务中执行,容器会抛出异常,只有业务方法没有关联到任何事务,才能正常执行。
    • NESTED:如果一个活动的事务存在,则当前方法运行在一个嵌套的事务中。 如果没有活动事务,则按REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效果。

    接下来,我们着重介绍事务传播属性——NESTED。如有下面一段代码:

    @Resource OtherService otherService;
    
    public void xxx() {
        stmt.executeUpdate("update person set name='888' where id=1");
        otherService.update(); // OtherService的update()方法的事务传播属性为NESTED
        stmt.executeUpdate("delete from person where id=9");
    }
    • 1

    将以上代码展开,可能就变成了如下这样一段代码:

    Connection conn = null;
    try {
        conn.setAutoCommit(false);
        Statement stmt = conn.createStatement();
        stmt.executeUpdate("update person set name='888' where id=1");
    
        Savepoint savepoint = conn.setSavepoint(); // 保存点
        try{   
            conn.createStatement().executeUpdate("update person set name='222' where sid=2");
        }catch(Exception ex){
            conn.rollback(savepoint);    
        }
    
        stmt.executeUpdate("delete from person where id=9");
        conn.commit();
        stmt.close();
        } catch (Exception e) {
            conn.rollback();
        }finally{
            try {
                if(null!=conn && !conn.isClosed()) conn.close();
            } catch (SQLException e) { 
                e.printStackTrace(); 
            }
        }
    }
    • 1

    其中,OtherService中标注事务传播属性为NESTED的update()方法,就相当于这样一段代码:

    Savepoint savepoint = conn.setSavepoint(); // 保存点
    try{   
        conn.createStatement().executeUpdate("update person set name='222' where sid=2");
    }catch(Exception ex){
        conn.rollback(savepoint);    
    }
    • 1

    我们也就明白了内部事务的回滚不会对外部事务造成影响。

    事务的其他属性

    除了事务传播属性外,事务还有一些其他的属性:

    • readOnly属性:设置为只读事务,对于只读事务,它就不能进行更新操作,一般只存在数据读取的时候,可以将readOnly属性设置为true,可提供效率。
    • timeout属性:代表事务的超时时间,默认为30s,一般情况下都不需要设置超时时间。

    事务的isolation属性

    事务的isolation属性指定了事务的隔离级别,实际上事务的隔离级别并不是由Spring容器决定的,而是由底层数据库决定的。

    数据库系统提供了四种事务隔离级

    数据库系统提供了四种事务隔离级别供用户选择。不同的隔离级别采用不同的锁类型来实现,在四种隔离级别中,Serializable的隔离级别最高,但对并发访问数据库的性能影响最大。Read Uncommited的隔离级别最低。大多数据库默认的隔离级别为Read Commited,如SqlServer,当然也有少部分数据库默认的隔离级别为Repeatable Read,如MySQL

    • Read Uncommited:读未提交数据(会出现脏读,不可重复读和幻读)
    • Read Commited:读已提交数据(会出现不可重复读和幻读)
    • Repeatable Read:可重复读(会出现幻读)
    • Serializable:串行化

    脏读:一个事务读取到另一事务未提交的更新新据。前提是并发的两个或多个事务。 
    不可重复读:在同一事务中,多次读取同一数据返回的结果有所不同。换句话说就是,后续读取可以读到另一事务已提交的更新数据。相反,“可重复读”在同一事务中多次读取数据时,能够保证所读数据一样,也就是,后续读取不能读到另一事务已提交的更新数据。目前要实现可重复读的话,一般数据库采用快照技术,在某一时刻(点),当你访问数据的时候,它把这个数据作为一个镜像,以后在同一个事务中再去读取相同记录的数据时,它都可以从快照里面返回这个数据,不管外部怎么样对它操作,在多次读取的时候都不会受到影响。 
    幻读:一个事务读取到另一事务已提交的insert数据。

  • 相关阅读:
    波段是金牢记六大诀窍
    zk kafka mariadb scala flink integration
    Oracle 体系结构详解
    图解 Database Buffer Cache 内部原理(二)
    SQL Server 字符集介绍及修改方法演示
    SQL Server 2012 备份与还原详解
    SQL Server 2012 查询数据库中所有表的名称和行数
    SQL Server 2012 查询数据库中表格主键信息
    SQL Server 2012 查询数据库中所有表的索引信息
    图解 Database Buffer Cache 内部原理(一)
  • 原文地址:https://www.cnblogs.com/telwanggs/p/6913476.html
Copyright © 2011-2022 走看看