Spring 事务管理高级应用难点剖析

11 篇文章 0 订阅
8 篇文章 0 订阅

参考自:http://www.ibm.com/developerworks/cn/java/j-lo-spring-ts1/index.html

IBM技术博客论坛


第一部分:

有可能数据库本身不支持事务,如使用MylSAM引擎的MySQL数据库,那么在Spring中用和没用事务管理器都是无效的.


但是对于Hibernate来说就不不同,因为Hibernate自己定义了事务管理,重点和Hibernate一级缓存有关.当我们调用CRUD方法的时候

Hibernate并不直接向数据库发送SQL语句,只有在提交事务或者是Flush一级缓存的时候才真正向数据库发送SQL

所以,即使底层数据库不支持事务,Hibernate 的事务管理也是有一定好处的,不会对数据操作的效率造成负面影响。所以,如果是使用 Hibernate 数据访问技术,没有理由不配置 HibernateTransactionManager 事务管理器。


对于简单的应用来说不必要使用应用分层,因为这样会导致生成不必要的包和文件


有关事务方法的嵌套:

实际上就是使用事务传播行为

所谓事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。Spring 支持 7 种事务传播行为:

  • PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
  • PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
  • PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。
  • PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
  • PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
  • PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。

当时PROPAGATION_REQUIRED传播行为的时候,如果一个事务方法中调用另外一个事务方法的时候实际上直接把另外一个事务加入到第一个事务当中.


多线程:
一般来说Web容器本身的多线程的.而相对于每一个DAO和Service来说都应该是有状态的(Connection)的.所以一般来说Bean都应该做到无状态.
但是在 Spring 中,DAO 和 Service 都以单实例的方式存在。Spring 是通过 ThreadLocal 将有状态的变量(如 Connection 等)本地线程化,达到另一个层面上的“线程无关”,从而实现线程安全。Spring 不遗余力地将状态化的对象无状态化,就是要达到单实例化 Bean 的目的。


如果在一个事务方法的线程中调用另外一个线程的事务方法:
在  相同线程中 进行相互嵌套调用的事务方法工作于相同的事务中。如果这些相互嵌套调用的方法工作在不同的线程中,不同线程下的事务方法工作在独立的事务中。



第二部分:
联合军混合作战情况:
就是一个用ORM框架( Hibernate,JPA,JDO),一个使用JDBC技术(Spring JDBC),那么如何进行事务管理呢?
其实Spring足够的智能可以自己处理这些事务关系处理,其实就是把这2种数据访问技术合成在一起,共用同一个事务

如下
    @Autowired
    private ScoreService scoreService;

    public void logon(String userName) {
        System.out.println("logon method...");
        updateLastLogonTime(userName); //①使用Hibernate数据访问技术
        scoreService.addScore(userName, 20); //②使用Spring JDBC数据访问技术
    }
	public void updateLastLogonTime(String userName) {
        	System.out.println("updateLastLogonTime...");
        	User user = hibernateTemplate.get(User.class,userName);
        	user.setLastLogonTime(System.currentTimeMillis());
        	hibernateTemplate.flush(); //③请看下文的分析
    }

这个事务方法里面同时使用了2种数据处理技术
在③处,我们显式调用了 flush() 方法,将 Session 中的缓存同步到数据库中,这个操作将即时向数据库发送一条更新记录的 SQL 语句。之所以要在此显式执行 flush() 方法,原因是:默认情况下,Hibernate 要在事务提交时才将数据的更改同步到数据库中,而事务提交发生在 logon() 方法返回前。如果所有针对数据库的更改都使用 Hibernate,这种数据同步延迟的机制不会产生任何问题。但是,我们在 logon() 方法中同时采用了 Hibernate 和 Spring JDBC 混合数据访问技术。Spring JDBC 无法自动感知 Hibernate 一级缓存,所以如果不及时调用 flush() 方法将数据更改同步到数据库,则②处通过 Spring JDBC 进行数据更改的结果将被 Hibernate 一级缓存中的更改覆盖掉,因为,一级缓存在 logon() 方法返回前才同步到数据库!
其实就是说Hibernate的事务处理是在事务方法返回前一刻才实际提交事务,这样的话JDBC的事务修改等就可能被覆盖了,导致数据操作失败
结果:
Hibernate 和 JDBC 这两种数据访问技术在同一事务上下文中“共用”一个连接。在④处,提交 Hibernate 事务,接着在⑤处触发调用底层的 Connection 提交事务。


特殊方法成漏网之鱼:

对于基于接口动态代理的 AOP 事务增强来说,由于接口的方法是 public 的,这就要求实现类的实现方法必须是 public 的(不能是 protected,private 等),同时不能使用 static 的修饰符。所以,可以实施接口动态代理的方法只能是使用“public”或“public final”修饰符的方法,其它方法不可能被动态代理,相应的也就不能实施 AOP 增强,也不能进行 Spring 事务增强了。

基于 CGLib 字节码动态代理的方案是通过扩展被增强类,动态创建子类的方式进行 AOP 增强植入的。由于使用 final、static、private 修饰符的方法都不能被子类覆盖,相应的,这些方法将不能被实施 AOP 增强。所以,必须特别注意这些修饰符的使用,以免不小心成为事务管理的漏网之鱼。

@Service("userService")
public class UserService {
    
	//① private方法因访问权限的限制,无法被子类覆盖
    private void method1() {
        System.out.println("method1");
    }
    
	//② final方法无法被子类覆盖
    public final void method2() {
        System.out.println("method2");
    }

    //③ static是类级别的方法,无法被子类覆盖
    public static void method3() {
        System.out.println("method3");
    }
    
	//④ public方法可以被子类覆盖,因此可以被动态字节码增强
    public void method4() {
        System.out.println("method4");
    } 
}
在配置文件中配置好:
<aop:config proxy-target-class="true">
	    <!-- ①显式使用CGLib动态代理 -->
        <!-- ②希望对UserService所有方法实施事务增强 -->
        <aop:pointcut id="serviceJdbcMethod"
            expression="execution(* user.special.UserService.*(..))"/>
        <aop:advisor pointcut-ref="serviceJdbcMethod" 
            advice-ref="jdbcAdvice" order="0"/>
    </aop:config>
    <tx:advice id="jdbcAdvice" transaction-manager="jdbcManager">
        <tx:attributes>
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>
在 ① 处,我们通过 proxy-target-class="true"显式使用 CGLib 动态代理技术,在 ② 处通过 AspjectJ 切点表达式表达 UserService 所有的方法,希望对 UserService 所有方法都实施 Spring AOP 事务增强。

表 2. 不能被 Spring AOP 事务增强的方法
动态代理策略不能被事务增强的方法
基于接口的动态代理除 public 外的其它所有的方法,此外 public static 也不能被增强
基于 CGLib 的动态代理private、static、final 的方法
这些方法不能被 Spring 进行 AOP 事务增强,是指这些方法不能启动事务,但是外层方法的事务上下文依就可以顺利地传播到这些方法中。
这些不能被 Spring 事务增强的方法和可被 Spring 事务增强的方法唯一的区别在 “是否可以主动启动一个新事务”:前者不能而后者可以。对于事务传播行为来说,二者是完全相同的,前者也和后者一样不会造成数据连接的泄漏问题。换句话说,如果这些“特殊方法”被无事务上下文的方法调用,则它们就工作在无事务上下文中;反之,如果被具有事务上下文的方法调用,则它们就工作在事务上下文中。
就是说不能自己创建事务,但是可以用别人已经创建好的事务,通过加入别人的事务来提交.
在实际开发中,最容易造成隐患的是基于 CGLib 的动态代理时的“public static”和“public final”这两种特殊方法。原因是它们本身是 public 的,所以可以直接被外部类(如 Web 层的 Controller 类)调用,只要调用者没有事务上下文,这些特殊方法也就以无事务的方式运作。

第三部分:

Spring JDBC数据连接泄露:

如果直接使用SpringJDBC中获取连接会出现连接泄露:

直接从数据源获取连接,后续程序没有显式释放该连接

Connection conn = jdbcTemplate.getDataSource().getConnection();

这样的代码就会出现连接泄露,不会自己销毁.除非使用Spring已经封装好的框架等

也可以使用一个工具类DataSourceUtils获取数据连接,这个工具保证了会自己销毁连接.

但是使用DataSourceUtils也可能会出现连接泄露,因为如果当不存在事务的时候就会立刻返回,以至于没有成功的销毁连接

说明有事务上下文时,需要等到整个事务方法(即 logon())返回后,事务上下文绑定的连接才释放。但在没有事务上下文时,logon() 调用 JdbcTemplate 执行完数据操作后,马上就释放连接。

所以要修改这个漏洞,就要使用手动的释放连接

finally {

        // ②显式使用DataSourceUtils释放连接

        DataSourceUtils.releaseConnection(conn,jdbcTemplate.getDataSource());

    }

另一种保证数据源不泄露的方式是使用TransactionAwareDataSourceProxy来代理数据源

在配置文件中配置

<!-- ①对数据源进行代理-->

<bean id="dataSourceProxy" 

    class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy"

    p:targetDataSource-ref="dataSource"/>

其他ORM框架一样提供了这种代理,具体可以查询Spring官网

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值