对于层次划分清晰的应用来说,我们通常将事务管理放在Service层,而将数据访问逻辑放在Dao层,这样做的目的是不用因为将事务管理代码放在DAO层,而降低数据访问逻辑的重要性,也可以将Service层根据相应逻辑,来决定提交或者回滚事务。一般的Service对象可能需要在同一个业务方法中调用多个数据访问对象的方法。比如:
public void serviceMethod(){
dao1.add();
dao2.delete();
}
因为JDBC局部事务是控制是由java.sql.Connection来完成的,要保证两个DAO的数据访问处于一个事务中,我们需要保证他们使用的是同一个java.sql.Connection.
通常采用称为connection-passing的方式,即为当前同一个事务的各个dao的数据访问方法传递当前事务对应的同一个Connection。
传递java.sql.Connection,最好的办法是整个事务对应的java.sql.Connection实例放到统一的一个地方,但要保证每个业务请求的Connection又能各不干扰。或许你已经想到了ThreadLocal。对该类不理解的可以去看我之前的那篇文章ThreadLocal的一些理解。今天我们看看Spring是如何控制ThreadLocal为其服务的。
首先从开始事务进行分析
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)transaction;
Connection con = null;
try {
if (txObject.getConnectionHolder() == null || txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
//如果当前事务ConnectionHolder为空或者处在事务同步中
Connection newCon = this.dataSource.getConnection();
//获取数据库连接
if (this.logger.isDebugEnabled()) {
this.logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
}
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
//true代表这是新的连接
//2
}
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
}
txObject.getConnectionHolder().setTransactionActive(true);
//激活事务
int timeout = this.determineTimeout(definition);
if (timeout != -1) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}
if (txObject.isNewConnectionHolder()) {
//如果是本次是新的连接
TransactionSynchronizationManager.bindResource(this.getDataSource(), txObject.getConnectionHolder());
//将该ConnectionHolder绑定到当前线程 下面详细讲解
}
} catch (Throwable var7) {
if (txObject.isNewConnectionHolder()) {
DataSourceUtils.releaseConnection(con, this.dataSource);
txObject.setConnectionHolder((ConnectionHolder)null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", var7);
}
}
上面方法主要就是获取连接并设置事务的各种属性信息,关键的是将DataSource和txObject.getConnectionHolder()传入了bindResource中,ConnectionHolder对象就包装着本次事务所获取的连接。我们来看看bindResource
方法
##该方法在TransactionSynchronizationManager类中##
private static final ThreadLocal
该方法我就不逐步分析了,如果熟悉ThreadLocal机制的同学一定也会很快理解。总的来说,该方法就是将传进来的key和value作为键值对存储在HashMap中,再把HashMap存到ThreadLocal中。此后每个线程从该ThreadLocal中get到的一定是属于自己线程的HashMap,从而取值。
静态变量:
private static final ThreadLocal
TransactionSynchronizationManager内部用ThreadLocal对象存储资源,ThreadLocal存储的为DataSource生成的actualKey为key值和ConnectionHolder作为value值封装成的Map。
在某个线程第一次调用时候,封装Map资源为:key值为DataSource生成actualKey【Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);】value值为DataSource获得的Connection对象封装后的ConnectionHolder。
ok,现在我们知道了Connection是如何绑定线程并放在Spring容器中,继续看是在何时需要获取该Connection的吧,我们给TransactionSynchronizationManager类中的getResource方法打上断点。
调用getResource方法来获取ConnectionHandler的时间点有下面这些:
protected boolean isExistingTransaction(Object transaction) {
DataSourceTransactionManager.DataSourceTransactionObject txObject = (DataSourceTransactionManager.DataSourceTransactionObject)transaction;
return txObject.getConnectionHolder() != null && txObject.getConnectionHolder().isTransactionActive();
}
DataSourceTransactionManager判断是否存在当前事务的两个标准就是ConnectionHolder是否为空和TransactionActive是否为true,如果之前该连接上调用了doBegin创建事务,则这里肯定会返回true。
完成本次事务的所有业务逻辑之后则会在提交事务完成后,调用TransactionSynchronizationManager
类的doUnbindResource
方法
private static Object doUnbindResource(Object actualKey) {
Map
该方法就是移除指定资源或者Map。
总结:
因为代理的原因,Spring的Connection-Passing机制确保每个被代理事务管理的方法中所有同一个线程(为什么要强调这个,因为事务过程的Connection就是用ThreadLocal管理的)
的对数据库操作都在同一个Connection(Session会话)中执行,因为提交和回滚都以Connection为单位。即不同的Connection提交和回滚不会影响另一个Connection的执行过程。
以上就是Spring利用ThreadLocal来保证每个线程调用的每个业务方法中使用的是同一个Connection,以确保事务的控制。
@Override
public void transfer(final String inUser, final String outUser, final int money) throws Exception{
new Thread(new Runnable() {
@Override
public void run()
{
manager.out(outUser, money); //1
}
}).start();
int i=1/0;//拟突发断电
accountDao.in(inUser, money);
}
如上会回滚注释1处的执行代码吗?
答案是不会。前面我们说过Connection是绑定在线程的。transfer方法是一个事务方法。Spring事务管理器在事务方法和事务结束
过程中都会获得绑定在该线程的Connection,因此事务的提交和回滚只针对该Connection有效,也就是说其他线程调用的数据访问方法
不会由当前事务方法的Connection管理。因此如上所示,注释1处的代码在另一个线程中执行,其Connection和当前transfer方法的事务
Connection大概率不是同一个。*(也有可能是同一个,有可能事务rollback之后释放连接,刚好轮到该线程获取上次释放的连接,我们可以设置执行延迟,
但无论如何已经是两个事务边界了。)因此,在断电之后,注释1处的代码并没有回滚。
理解Spring事务管理是由JDBC的Connection来确定事务边界的有助于理解后续的Spring事务处理分布式事务的局限性。因此分布式情况下,可能有多个操作
都运行在不同机器上的服务方法组合,因为我们需要知道所有方法的结果,并且进行全部提交和全部回滚,以确保一致性,而普通的事务管理无法做到这些,
如何有效地确保这些方法执行的正确性,当然这就属于分布式事务的范畴了,我们这里不做讨论。
面试题:
一个Controller调用两个Service,这两Service又都分别调用两个Dao,问其中用到了几个数据库连接池的连接?
分情况讨论:
数据源就是我们配置的DataSource。(即便一个数据源使用了多个Mysql数据库也是在一个连接中,url不指定Database,sql语句中指定Database,
例如spring.datasource.url=jdbc:mysql://localhost:3306
,此时就可以sql操作多个数据库,通过database.table,此时在同一个事务管理下的service,即便使用了两个Dao,
操作两个完全不同的数据库,d1.t1,d2.t2,但是因为在同一个数据源中,同一个线程中,则也会共用一个数据库连接,事务也会生效)
(分布式事务初了解)[http://www.importnew.com/26349.html]
(浅谈事务和一致性:刚性or柔性?)[https://juejin.im/post/5aa8b8636fb9a028c67567c6]