《精通Spring4.x 企业应用开发实战》第12章 Spring 的事务管理难点剖析

前言

汇总:《精通Spring4.x 企业应用开发实战》

一、DAO 和事务管理的牵绊

很少有使用 Spring 而不使用 Spring 事务管理器的应用,因此常常有人会问:是否用了 Spring, 就一定要用 Spring 事务管理器,否则就无法进行数据的持久化操作呢?事务管理器和 DAO 是什么关系呢?
也许是 DAO 和事务管理如影随形的缘故吧,这个看似简单的问题实实在在地存在着,从初学者心中涌出,萦绕在老把式的脑际。答案当然是否定的。我们都知道,事务管理的目的是保证数据操作的事务性(原子性、一致性、隔离性、持久性,即所谓的ACID)。脱离了事务,DAO 照样可以顺利地进行数据操作

二、应用分层的迷惑

1、Web、 Service 及 DAO 三层是Web 应用开发常见的模式,但有些开发人员却错误地认为: 如果要使用 Spring 的事务管理就一定要先进行三层的划分,更有甚者认为每层一定要先定义一个接口,然后再定义一个实现类。
2、对将“面向接口编程” 奉为圭臬,认为放之四海而皆准的论调,笔者并不是很赞同。
3、是的,“面向接口编程”是 Martin Fowler、Rod Johnson 这些大师所提倡的行事原则。如果拿这条原则去开发框架、产品或大型项目,怎么强调都不为过。
4、但是,对于一般的开发人员来说,也许做的是一个普通工程项目,往往只是一些对数据库增、删、查、改的功能。此时,过分强制“面向接口编程”除了会带来更多的类文件,并不会有什么好处。

三、事务方法嵌套调用的迷茫(事务传播行为)

1.Spring 事务传播机制回顾

Spring 事务的一个被讹传很广的说法是:一个事务方法不应该调用另一个事务方法,否则将产生两个事务。结果造成开发人员在设计事务方法时束手束脚,生怕一不小心就踩到地雷。

其实这是未正确认识 Spring 事务传播机制而造成的误解。Spring 对事务控制的支持统一在TransactionDefinition 类中描述,该类有以下几个重要的接口方法。
● int getPropagationBehavior():事务的传播行为。
● int getlsolationLevel():事务的隔离级别。
● int getTimeout():事务的过期时间。
● boolean isReadOnly():事务的读/写特性。

很明显,除了事务的传播行为,对于事务的其他特性,Spring 是借助底层资源的功能来完成的,Spring 无非充当了一个代理的角色但是事务的传播行为却是 Spring 凭借自身的框架提供的功能,是 Spring提供给开发者最珍贵的礼物,讹传的说法玷污了 Spring事务框架最美丽的光环。
所谓事务传播行为,就是多个事务方法相互调用时,事务如何在这些方法间传播。Spring 支持以下 7 种事务传播行为:
在这里插入图片描述Spring 默认的事务传播行为是 PROPAGATION_ REQUIRED,它适合绝大多数情况。如果多个 ServiveX#methodX()均工作在事务环境下(均被 Spring 事务增强),且程序中存在调用链 Service1#method1()->Service2#method2()->Service3# method3(),那么这3个服务类的3 个方法通过 Spring 的事务传播机制都工作在同一个事务中。

2.相互嵌套的服务方法

线程同步场景下,UserService#logon()方法内部调用了 UserService#updateLastLogonTime()和 ScoreService#addScore()方法,这两个类都继承于 BaseService。它们之间的类结构如图 12-1 所示:
在这里插入图片描述同一条线程下,UserService#logon()方法内部调用了 ScoreService#addScore()方法,二者分别通过Spring AOP 进行了事务增强,则它们工作在同一事务中。

同一个线程下,ScoreService#addScore()方法添加到了UserService#logon()方法的事务上下文中,二者共享同一个事务。所以最终的结果是UserService 的 logon()和 updateLastLogonTime()方法及 ScoreService 的 addScore()方法工作在同一个事务中。

具体验证代码,请看书或者语雀版。

四、多线程的困惑

1. Spring 通过单实例化 Bean 简化多线程问题

由于 Spring 的事务管理器是通过线程相关的 ThreadLocal来保存数据访问基础设施(Connection 实例)的,再结合 IoC 和 AOP 实现高级声明式事务的功能,所以 Spring 的事务天然地和线程有着千丝万缕的联系

我们知道 Web 容器本身就是多线程的,Web 容器为一个 HTTP 请求创建一个独立的线程(实际上大多数 Web 容器采用共享线程池),所以由此请求所涉及的 Spring 容器中的 Bean 也运行在多线程环境下。在绝大多数情况下,Spring 的 Bean 都是单实例的(singleton), 单实例 Bean 的最大好处是线程无关性,不存在多线程并发访问的问题,也就是线程安全的。

一个类能够以单实例的方式运行的前提是“无状态”,即一个类不能拥有状态化的成员变量。我们知道,在传统的编程中,DAO 必须持有一个 Connection, 而 Connection就是状态化的对象(非线程安全)。所以传统的 DAO 不能做成单实例的,每次要用时都必须创建一个新的实例。传统的 Service 由于内部包含了若干有状态的 DAO 成员变量,所以其本身也是有状态的。

但在 Spring 中,DAO 和 Service 都以单实例的方式存在。Spring 通过 ThreadLocal将有状态的变量(如 Connection 等)本地线程化,达到另一个层面上的“线程无关”,从而实现线程安全。Spring 不遗余力地将有状态的对象无状态化,就是要达到单实例化Bean 的目的。
由于 Spring 已经通过 ThreadLocal 的设施将 Bean 无状态化,所以 Spring 中的单实例 Bean 对线程安全问题拥有了一种天生的免疫能力。不但单实例的 Service 可以成功运行在多线程环境中,Service 本身还可以自由地启动独立线程以执行其他的 Service。

2.启动独立线程调用事务方法

下面对 logon()方法进行改造,让其在方法内部再启动一个新线程,在这个新线程中执行积分添加的操作,看看究竟会发生哪些事务行为,如代码清单12-12 所示。

package com.smart.multithread;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Service;
import org.apache.commons.dbcp.BasicDataSource;

/**
 * @author 陈雄华
 * @version 1.0
 */
@Service("userService")
public class UserService extends BaseService {
    private JdbcTemplate jdbcTemplate;
    private ScoreService scoreService;

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Autowired
    public void setScoreService(ScoreService scoreService) {
        this.scoreService = scoreService;
    }

    @Transactional
    public void logon(String userName) {
        System.out.println("before userService.updateLastLogonTime method...");
        updateLastLogonTime(userName);
        System.out.println("after userService.updateLastLogonTime method...");
        // ①在同一个线程中调用scoreService#addScore(),将运行在同一个事务中
//      scoreService.addScore(userName, 20);

        //②在一个新线程中执行scoreService#addscore(),将启动一个新的事务
        Thread myThread = new MyThread(this.scoreService, userName, 20);//使用一个新线程运行
        myThread.start();
    }

    // ③负责执行scoreService#addscore () 的线程类
    @Transactional
    public void updateLastLogonTime(String userName) {
        String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
        jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
    }

    private class MyThread extends Thread {
        private ScoreService scoreService;
        private String userName;
        private int toAdd;
        private MyThread(ScoreService scoreService, String userName, int toAdd) {
            this.scoreService = scoreService;
            this.userName = userName;
            this.toAdd = toAdd;
        }

        public void run() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("before scoreService.addScor method...");
            scoreService.addScore(userName, toAdd);
            System.out.println("after scoreService.addScor method...");
        }
    }

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/multithread/applicatonContext.xml");
        UserService service = (UserService) ctx.getBean("userService");

        JdbcTemplate jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
        //插入一条记录,初始分数为10
        jdbcTemplate.execute("INSERT INTO t_user(user_name,password,score,last_logon_time) VALUES('tom','123456',10," + System.currentTimeMillis() + ")");


        //调用工作在无事务环境下的服务类方法,将分数添加20分
        System.out.println("before userService.logon method...");
        service.logon("tom");
        System.out.println("after userService.logon method...");
        jdbcTemplate.execute("DELETE FROM t_user WHERE user_name='tom'");

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述在①处,在主线程(main)中执行的 UserService#logon0方法的事务启动;在②处,其对应的事务提交。而在子线程(Thread-2)中执行的 ScoreService#addScore()方法的事务在③处启动;在④处提交其对应的事务。

所以可以得出这样的结论:在相同线程中进行相互嵌套调用的事务方法工作在相同的事务中。如果这些相互嵌套调用的方法工作在不同的线程中,则不同线程下的事务方法工作在独立的事务中

五、联合军种作战的混乱

1.Spring 事务管理器的应对

让我想起来了在EDM中,基于原先的mybatis,然后我又整合了MongoDB。造成了mybatis事务失效。

Spring 抽象的 DAO 体系兼容多种数据访问技术,它们各有特色、各有干秋。如Hibernate 是一个非常优秀的 ORM实现方案,但对底层 SQL 的控制不太方便; 而 MyBatis则通过模板化技术让用户方便地控制 SQL,但没有 Hibernate 那样高的开发效率;自由度最高的当然是直接使用 Spring JDBC 了,但它也是底层的,灵活的代价是代码的繁复。很难说哪种数据访问技术是最优秀的,只有在某种特定的场景下才能给出答案。所以在一个应用中往往采用多种数据访问技术,一般是两种,一种采用 ORM 技术框架,而另一种采用偏 JDBC 的底层技术;二者珠联璧合,形成联合军种,共同御敌。

但是,这种联合军种如何应对事务管理的问题呢?我们知道,Spring 为每种数据访问技术提供了相应的事务管理器,难道需要分别为它们配置对应的事务管理器吗?它们到底是如何协同工作的呢?这些层出不穷的问题往往压制了开发人员使用联合军种的想法。

其实,在这个问题上,我们低估了 Spring 事务管理的能力。如果用户采用了一种高端的 ORM 技术(Hibernate、JPA、JDO),同时还采用了一种 JDBC 技术(Spring JDBC、MyBatis):由于前者的会话(Session)是对后者连接(Connection)的封装,Spring 会“足够智能地”在同一个事务线程中让前者的会话封装后者的连接。所以,只要直接采用前者的事务管理器就可以了。 表12-1 给出了混合数据访问技术框架所对应的事务管理器。
在这里插入图片描述

2.Hibernate+Spring JDBC 混合框架的事务管理

由于一般不会出现同时使用多个 ORM 框架的情况(如 Hibernate+JPA),所以我们不拟对此命题展开论述,只重点研究 ORM 框架+JDBC 框架的情况。 Hiberate+SpringJDBC 可能是被使用得最多的组合,本节通过实例观察事务管理的运作情况,如代码清单12-13 所示。(具体代码,请看书)

六、特殊方法成漏网之鱼

1.哪些方法不能实施 Spring AOP 事务

由于 Spring 事务管理是基于接口代理或动态字节码技术,通过 AOP 实施事务增强的,虽然 Spring 依然支持 AspectJ LTW 在类加载期实施增强,但这种方法很少使用,所以我们不予关注。

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

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

具体代码示例:看书或者语雀。

七、数据连接泄露

1.底层连接资源的访问问题

连接泄露的定义:简单的说,就是获取连接且使用完毕后,没有close该连接,造成连接泄露。

那么,如何获取这些被 Spring 管控的数据连接呢?Spring 提供了两种方法:

  1. 其一是使用数据资源获取工具类;
  2. 其二是对数据源(或其衍生品,如 Hibernate 的 SessionFactory)进行代理。

2.Spring JDBC 数据连接泄露

如果从数据源直接获取连接,且在使用完成后不主动归还给数据源(调用Connection#close0方法),则将造成数据连接泄露的问题,如代码清单12-19 所示。
在这里插入图片描述
在①处通过调用 jdbcTemplate.getDataSource().getConnection()方法显式获取一个连接,这个连接不是logon()方法事务上下文线程绑定的连接,所以如果开发者没有手工释放这个连接(显式调用 Connection#close()方法),则这个连接将永久被占用(处于 active状态),造成连接泄露。

编写模拟运行的代码,查看方法执行对数据连接的实际占用情况,请看书。

3.事务环境下通过 DataSourceUtils 获取数据连接

Spring 提供了一个能从当前事务上下文中获取绑定的数据连接的工具类,即DataSourceUtils。Spring 强调必须使用 DataSourceUtils 获取数据连接,Spring 的JdbcTemplate 内部也是通过 DataSourceUtils 来获取连接的。DataSourceUils 提供了若干获取和释放数据连接的静态方法,说明如下:

  1. static Connection doGetConnection(DataSource dataSource):首先尝试从事务上下文中获取连接,失败后再从数据源获取连接。
  2. static Connection getConnection(DataSource dataSource):和 doGetConnection()方法的功能一样,实际上,其内部就是通过调用 doGetConnection()方法获取连接的。
  3. static void doReleaseConnection(Connection con, DataSource dataSource):释放连接,放回到连接池中。
  4. static void releaseConnection(Connection con, DataSource dataSource):和 doReleaseConnection()方法的功能一样,实际上,其内部就是通过调用 doReleaseConnection()方法获取连接的。

DataSourceUils核心代码如下:
在这里插入图片描述首先查看当前是否存在事务管理上下文,并尝试从事务管理上下文中获取连接。如果获取失败,则直接从数据源中获取连接。在获取连接后,如果当前拥有事务上下文,则将连接绑定到事务上下文中

--------连接测试:
在代码清单 12-23 的 JdbcUserService 中,使用 Data SourceUtils.getConnection()方法替换直接从数据源中获取连接的代码:
在这里插入图片描述以上不会连接泄露现象。一个执行线程在运行 JdbcUserService#logon()方法时只占用一个连接,而且方法执行完毕后,该连接马上释放(ThreadLocal和线程相绑定,线程执行完毕,相应的ThreadLocal对象也被销毁)。这说明通过 DataSourceUtils.getConnection()方法确实获取了方法所在事务上下文绑定的那个连接,而不是像原来那样从数据源中获取一个新的连接。

4.无事务环境下通过 DataSourceUtils 获取数据连接

然而,是否使用 DataSourceUtils 获取数据连接就可以高枕无忧了呢?理想很丰满,现实很骨感:如果 DataSourceUtils 在没有事务上下文的方法中使用 getConnection()方法获取连接,那么依然会造成数据连接泄露。因为事务、Connection、线程没有依赖ThreadLocal进行绑定,那么即使线程结束,该Connection依然存在。

那么应该如何保证连接不泄露呢?代码如下:
在这里插入图片描述在②处显式调用 DataSourceUtils.releaseConnection()方法释放获取的连接。需要特别指出的是,一定不能在①处释放连接。因为如果 logonO方法在获取连接后,①处代码前这段代码执行时发生异常,则①处释放连接的动作将得不到执行。这将是一个非常具有隐蔽性的连接泄露的隐患点。

5.JdbcTemplate 如何做到对连接泄露的免疫

分析 JdbcTemplate 的代码,可以清楚地看到它开放的每个数据操作方法:首先使用DataSourceUtils 获取连接,然后在方法返回之前使用 DataSourceUtils 释放连接。下面看一下 JdbeTemplate 最核心的数据操作方法 execute(),如代码清单12-25 所示。

@Nullable
	private <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action, boolean closeResources)
			throws DataAccessException {

		Assert.notNull(psc, "PreparedStatementCreator must not be null");
		Assert.notNull(action, "Callback object must not be null");
		if (logger.isDebugEnabled()) {
			String sql = getSql(psc);
			logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
		}

		// 获取与'数据库事务'相绑定的'数据库连接'。
		Connection con = DataSourceUtils.getConnection(obtainDataSource());
		PreparedStatement ps = null;
		try {
			ps = psc.createPreparedStatement(con);
			// 应用用户设定的输入参数
			applyStatementSettings(ps);
			// 执行数据库操作(CRUD)。处理一些通用方法外的个性化处理,也就是 PreparedStatementCallback 类型的参数的doInPreparedStatement 方法的回调。
			T result = action.doInPreparedStatement(ps);
			handleWarnings(ps);
			return result;
		}
		catch (SQLException ex) {
			// Release Connection early, to avoid potential connection pool deadlock
			// in the case when the exception translator hasn't been initialized yet.
			if (psc instanceof ParameterDisposer) {
				((ParameterDisposer) psc).cleanupParameters();
			}
			String sql = getSql(psc);
			psc = null;
			JdbcUtils.closeStatement(ps);
			ps = null;
			// 数据库的连接释放并不是直接调用了 Connection 的API中的close 方法。考虑到存在事务的情况,如果当前线程存在事务,那么说明在当前线程中存在共用数据库连接,这种情况下直接使用 ConnectionHolder 中的 released 方法进行连接数减一,面不是真正的释放连接。
			DataSourceUtils.releaseConnection(con, getDataSource());
			con = null;
			throw translateException("PreparedStatementCallback", sql, ex);
		}
		finally {
			if (closeResources) {
				if (psc instanceof ParameterDisposer) {
					((ParameterDisposer) psc).cleanupParameters();
				}
				JdbcUtils.closeStatement(ps);
				DataSourceUtils.releaseConnection(con, getDataSource());
			}
		}
	}

在①处通过 DataSourceUtils.getConnection()方法获取连接,在②和③处通过DataSourceUtils.releaseConnection()方法释放连接。所有 JdbcTemplate 开放的数据访问API 最终都是直接或间接由 execute(StatementCallback action)方法执行数据访问操作的,因此这个方法代表了 JdbcTemplate 数据操作的最终实现方式

正是因为 JdbcTemplate 严谨的获取连接及释放连接的模式化流程保证了JdbcTemplate 对数据连接泄露问题的免疫性。所以,如有可能,应尽量使用 JdbcTemplate、HibernateTemplate 等模板进行数据访问操作,避免直接获取数据连接的操作

6.使用 TransactionAwareDataSourceProxy

如果不得己要显式获取数据连接,除了可以使用 DataSourceUtils 获取事务上下文绑定的连接,还可以通过 TransactionAwareDataSourceProxy 对数据源进行代理。数据源对象被代理后就具有了事务上下文感知的能力,通过代理数据源的 getConnection(方法获取连接和使用 DataSourceUtils.getConnectionO方法获取连接的效果是一样的。

下面是使用 TransactionAwareDataSourceProxy 对数据源进行代理的配置,如代码清单12-26 所示。
在这里插入图片描述对数据源进行代理后,就可以通过数据源代理对象的 getConnection()方法获取事务上下文中绑定的数据连接了。因此,如果数据源己经进行了 TransactionAwareDataSourceProxy代理,而且方法存在事务上下文,那么代码清单 12-19 的代码也不会生产连接泄露的问题。

7.其他数据访问技术的等价类

理解了 Spring JDBC 的数据连接泄露问题,其中的道理可以平滑地推广到其他框架中。Spring 为每个数据访问技术框架都提供了一个获取事务上下文绑定的数据连接(或其行生品)的工具类和数据源(或其行生品)的代理类。
表12-5 列出了不同数据访问技术框架对应 DataSourceUtils 的等价类:
在这里插入图片描述

表12-6 列出了不同数据访问技术框架下 TransactionAwareDataSourceProxy 的等价类:
在这里插入图片描述

总结

通过本章的学习,我们知道了以下真相。
● 在没有事务管理的情况下,DAO 照样可以顺利地进行数据操作。
● 将应用分成 Web、 Service 及 DAO 层只是一种参考的开发模式,并非是事务管理工作的前提条件。
● Spring 通过事务传播机制可以很好地应对事务方法嵌套调用的情况,开发者无须为了事务管理而刻意改变服务方法的设计。
● 由于单实例的对象不存在线程安全问题,所以经过事务管理增强的单实例 Bean可以很好地工作在多线程环境下。
● 混合使用多个数据访问技术框架的最佳组合是一个 ORM 技术框架(如 Hibernate或 JPA 等)+一个 JDBC 技术框架(如 Spring JDBC 或 MyBatis)。直接使用 ORM技术框架对应的事务管理器就可以了,但必须考虑 ORM缓存同步的问题
● Spring AoP 事务增强有两个方案:其一为基于接口的动态代理;其二为基于CGLib 动态生成子类的代理。由于 Java 语法的特性,有些特殊方法不能被 SpringAOP 代理,所以也就无法享受 AOP 织入带来的事务增强;
● 在使用 Spring JDBC 时如果直接获取 Connection,则可能会造成连接泄露。为了降低连接泄露的可能性,尽量使用 DataSourceUtils 获取数据连接。也可以对数据源进行代理,以便使数据源拥有感知事务上下文的能力。
● 可以将 Spring JDBC 防止连接泄露的解决方案平滑地应用到其他数据访问技术框架中。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@来杯咖啡

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

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值