欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

jdbc一个connection对应的是一个事物

程序员文章站 2022-06-05 09:00:27
...

Spring事务管理中的Connection-Passing

对于层次划分清晰的应用来说,我们通常将事务管理放在Service层,而将数据访问逻辑放在Dao层,这样做的目的是不用因为将事务管理代码放在DAO层,而降低数据访问逻辑的重要性,也可以将Service层根据相应逻辑,来决定提交或者回滚事务。一般的Service对象可能需要在同一个业务方法中调用多个数据访问对象的方法。比如:

1
2
3
4
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为其服务的。
首先从开始事务进行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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类中##

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
... //省略部分代码
 public static void bindResource(Object key, Object value) throws IllegalStateException {
        Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
        Assert.notNull(value, "Value must not be null");
        Map<Object, Object> map = (Map)resources.get();
        if (map == null) {
            map = new HashMap();
            resources.set(map);
        }
        Object oldValue = ((Map)map).put(actualKey, value);
        if (oldValue instanceof ResourceHolder && ((ResourceHolder)oldValue).isVoid()) {
            oldValue = null;
        }
        if (oldValue != null) {
            throw new IllegalStateException("Already value [" + oldValue + "] for key [" + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]");
        } else {
            if (logger.isTraceEnabled()) {
                logger.trace("Bound value [" + value + "] for key [" + actualKey + "] to thread [" + Thread.currentThread().getName() + "]");
            }
        }
    }

该方法我就不逐步分析了,如果熟悉ThreadLocal机制的同学一定也会很快理解。总的来说,该方法就是将传进来的key和value作为键值对存储在HashMap中,再把HashMap存到ThreadLocal中。此后每个线程从该ThreadLocal中get到的一定是属于自己线程的HashMap,从而取值。


ok,现在我们知道了Connection是如何绑定线程并放在Spring容器中,继续看是在何时需要获取该Connection的吧,我们给TransactionSynchronizationManager类中的getResource方法打上断点。


调用getResource方法来获取ConnectionHandler的时间点有下面这些:

  1. 执行sql的时候,会通过DataSourceUtils#doGetConnection方法调用getResource的连接,这里就不放代码了。
  2. 事务开启、提交或者回滚。
  3. 连接的释放
  4. 在获取DataSourceTransactionObject时,代码如下。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     protected Object doGetTransaction() {
            DataSourceTransactionManager.DataSourceTransactionObject txObject = new DataSourceTransactionManager.DataSourceTransactionObject();
            txObject.setSavepointAllowed(this.isNestedTransactionAllowed());
            ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(this.dataSource);
    //跟踪可以发现其实从ThreadLocal中获得了与该线程绑定的Resource,而该Resource就是之前doBegin中bindResource的ConnectionHolder实例。
            txObject.setConnectionHolder(conHolder, false);
    //将该ConnectionHolder实例注入到txObject
            return txObject;
    }
    

返回的DataSourceTransactionObject将作为判断是否存在当前事务的主要依据。代码如下:

1
2
3
4
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方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static Object doUnbindResource(Object actualKey) {
        Map<Object, Object> map = (Map)resources.get();
        if (map == null) {
            return null;
        } else {
            Object value = map.remove(actualKey);
            if (map.isEmpty()) {
                resources.remove();
            }
            if (value instanceof ResourceHolder && ((ResourceHolder)value).isVoid()) {
                value = null;
            }
            if (value != null && logger.isTraceEnabled()) {
                logger.trace("Removed value [" + value + "] for key [" + actualKey + "] from thread [" + Thread.currentThread().getName() + "]");
            }
            return value;
        }
    }

 

该方法就是移除指定资源或者Map。
总结:
因为代理的原因,Spring的Connection-Passing机制确保每个被代理事务管理的方法中所有同一个线程(为什么要强调这个,因为事务过程的Connection就是用ThreadLocal管理的)
的对数据库操作都在同一个Connection(Session会话)中执行,因为提交和回滚都以Connection为单位。即不同的Connection提交和回滚不会影响另一个Connection的执行过程。
以上就是Spring利用ThreadLocal来保证每个线程调用的每个业务方法中使用的是同一个Connection,以确保事务的控制。

思考一下

1
2
3
4
5
6
7
8
9
10
11
12
13
@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,问其中用到了几个数据库连接池的连接?
分情况讨论:

  • 如果这两个service,每个Service调用的两个dao都是在同一个线程同一个数据源并且开启了事务管理,则每个service都会使用一个Connection。
  • 如果不在Spring事务管理下,则无论在不在同一个数据源在不在同一个线程,每次调用Dao执行sql,都会使用DataSourceUtils.doGetConnection获取一个连接,执行完之后释放。
    所以该题目应该是用到2-4个数据库连接。

    数据源就是我们配置的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]