在最近作的一个项目里面,涉及到多数据源的操做,比较特殊的是,这多个数据库的表结构彻底相同,因为咱们使用的ibatis框架做为持久化层,为了防止每个数据源都配置一套规则,因此从新实现了数据源,根据线程变量中指定的数据库链接名称来获取实际的数据源。java
一个简单的实现以下:spring
public class ProxyDataSource implements DataSource { /** 数据源池配置 */ private Map<String, DataSource> dataSourcePoolConfig; public Connection getConnection() throws SQLException { return createDataSource().getConnection(); } private synchronized DataSource createDataSource() { String dbName = DataSourceContextHolder.getDbName(); return dataSourcePoolConfig.get(dbName); }
每次调用spring事务管理器以前,设置DataSourceContextHolder.set(“dbName”) sql
事务提交以后在调用 DataSourceContextHolder.clear() 方法便可数据库
可是这样设计实际使用过程当中也会遇到一些典型的问题,这就是在仔细了解spring中持久化层的设计以后,才能明白所产生的问题的缘由。下面主要总结一下spring 持久化的设计。编程
Jdbc基本的编程模型架构
因为任何持久化层的封装实际上都是对java.sql.Connection等相关对象的操做,一个典型的数据操做的流程以下:框架
但在咱们实际使用spring和ibatis的时候,都没有感受到上面的流程,其实spring已经对外已经屏蔽了上述的操做,让咱们更关注业务逻辑功能,可是咱们有必要了解其实现,以便可以更好运用和定位问题。spa
开启事务:线程
在开启事务的时候,咱们须要初始化事务上下文信息,以便在业务完成以后,须要知道事务的状态,以便进行后续的处理,这个上下文信息能够保存在 ThreadLocal里面,包括是否已经开启事务,事务的超时时间,隔离级别,传播级别,是否设置为回滚。这个信息对应用来讲是透明的,可是提供给使用者编程接口,以便告知业务结束的时候是提交事务仍是回滚事务。debug
获取链接
首先来看看spring如何获取数据库链接的,对于正常状况来看,获取链接直接调用DataSource.getConnection()就能够了,咱们在本身实现的时候也确定会这么作,可是须要考虑两种状况(这里面先不引入事务的传播属性):
1 尚未获取过链接,这是第一次获取链接
2 已经获取过链接,不是第一次获取链接,能够复用链接
解决获取数据库链接的关键问题就是如何判断是否已经可用的链接,而不须要开启新的数据库链接,同时因为数据库链接须要给后续的业务操做复用,如何保持这个链接,而且透明的传递给后续流程。对于一个简单的实现就是使用线程上下文变量ThrealLocal来解决以上两个问题。
具体的实现是:在获取数据库链接的时候,判断当前线程线程变量里面是否已经存在相关链接,若是不存在,就创新一个新的链接,若是存在,就直接获取其对应的链接。在第一次获取到数据库链接的时候,咱们还须要作一些特殊处理,就是设置自动提交为false。在业务活动结束的时候在进行提交或者回滚。这个时候就是要调用connection.setAutoCommit(false)方法。
执行sql
这一部分和业务逻辑相关,经过对外提供一些编程接口,可让业务决定业务完成以后如何处理事务,比较简单的就是设置事务状态。
提交事务:
在开启事务的时候,事务上下文信息已经保存在线程变量里面了,能够根据事务上下文的信息,来决定是不是提交仍是回滚。其实就是调用数据库链接Connection.commit 和 Connection.rollback 方法。而后须要清空线程变量中的事务上下文信息。至关于结束了当前的事务。
关闭链接:
关闭链接相对比较简单,因为当前线程变量保存了链接信息,只须要获取链接以后,调用connection.close方法便可,接着清空线程变量的数据库链接信息。
上面几个流程是一个简单的事务处理流程,在spring中都有对应的实现,见TransactionTemplate.execute方法。Spring定义了一个TransactionSynchronizationManager对象,里面保存了各类线程变量信息,
//保存了数据源和其对应链接的映射,value是一个Map结构,其中key为datasource,value为其打开的链接 private static final ThreadLocal resources //这个暂时用不到,不解释 private static final ThreadLocal synchronizations //当前事务的名字 private static final ThreadLocal currentTransactionName //是不是只读事务以及事务的隔离级别(这个通常咱们都用不到,都是默认界别) private static final ThreadLocal currentTransactionReadOnly private static final ThreadLocal currentTransactionIsolationLevel //表明是不是一个实际的事务活动,这个后面将) private static final ThreadLocal actualTransactionActive
在获取链接的时候,可见DataSourceUtils.doGetConnection()方法,就是从调用TransactionSynchronizationManager.getResource(dataSource)获取链接信息,若是为空,就直接从调用dataSource.getConnection()建立新的链接,后面在调用
TransactionSynchronizationManager.bindResource(dataSource,conn)绑定数据源到线程变量,以便后续的线程在使用。
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { conHolder.requested(); if (!conHolder.hasConnection()) { logger.debug("Fetching resumed JDBC Connection from DataSource"); conHolder.setConnection(dataSource.getConnection()); } return conHolder.getConnection(); } logger.debug("Fetching JDBC Connection from DataSource"); Connection con = dataSource.getConnection();
在提交事务的时候,见 DataSourceTransactionManager.doCommit方法,其实就是获取事务状态信息以及链接信息,调用conn.commmit方法,比较简单。
可是实际上,spring事务管理远远比上述复杂,咱们没有考虑如下几种状况:
1 若是当前操做不须要事务支持,也就是每次执行一次,就自动进行提交。如何在同一个架构里面兼容这两种状况。好比就是简单的query操做。
2 一个业务活动跨越多个事务,每一个事务的传播级别配置不同。后面会拿一个例子来讲明
对于第一个问题,比较好解决,首先就是根据线程变量里面获取数据源对应的链接,若是有链接,就复用。若是没有,就建立链接。在判断当前是否存在活动的事务上下文,若是存在事务信息,设置conn.setAutoCommit(false),而后设置线程上下文,绑定对应的数据源。若是不存在事务信息,就直接返回链接给应用。
这样就会带来一个新的问题,就是链接如何进行关闭。根据最开始的分析,在存在事务上下文的状况下,直接从获取线程获取对应的数据库链接,而后关闭。在关闭的也须要也进行判断一下便可。在spring里面,在事务中获取链接和关闭链接有一些特殊的处理,主要仍是和其jdbc以及orm框架设计兼容。在jdbcTemplate,IbatiTemplate每执行一次sql操做,就须要获取conn,执行sql,关闭conn。若是不存在事务上下文,这样作没有任何问题,获取一次链接,使用完成,而后就是比。可是若是存在事务上下文,每次获取的conn并不必定是真实的物理链接,因此关闭的时候,也不能直接关闭这数据库链接。Spring的中定义一个ConnectionHandle对象,这个对象持有一个数据库链接对象,以及该链接上的引用次数(retain属性)。每次复用一次就retain++ 操做,没关闭一次,就执行retain-- 操做,在retain 为0的时候,说明没有任何链接,就能够进行真实的关闭了。