spring事务和事务传播

Posted by hcy on September 9, 2020

spring事务和事务传播

1.原生的事务控制是这样的

​ 原生的事务是这样使用的,需要将连接设为非自动提交,执行完sql语句后,可以选择提交或者回滚。

1
2
3
4
5
6
7
8
9
10
11
public void test() throws SQLException {
    Connection connection = dataSource.getConnection();
    try {
        connection.setAutoCommit(false);
        connection.createStatement().executeQuery("select * from user where id = 1");
        connection.createStatement().executeQuery("select * from user where id = 2");
        connection.commit();
    } catch (Exception e) {
        connection.rollback();
    }
}

除了直接提交外,还可以设置savepoint,可以回滚到指定的savepoint。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void test2() throws SQLException {
    Connection connection = dataSource.getConnection();
    try {
        connection.setAutoCommit(false);
        connection.createStatement().executeQuery("select * from user where id = 1");
        //执行出错时,可以回滚到保存点
        Savepoint savepoint = connection.setSavepoint();
        try {
            connection.createStatement().executeQuery("select * from user where id = 2");
        } catch (Exception e) {
            connection.rollback(savepoint);
        }
        connection.releaseSavepoint(savepoint);
        connection.commit();
    } catch (Exception e) {
        connection.rollback();
    }
}

2.spring的事务控制

spring的事务传播级别有七种,分为三类

1.必须要在事务里运行的

事务级别 解释
REQUIRED 如果当前存在事务,加入当前事务,否则创建新的
REQUIRES_NEW 如果当前存在事务,则挂起他,开启新的事物执行
MANDATORY 如果当前存在事务,加入当前事务,否则报错
NESTED 如果当前存在事务,使用嵌套事物执行,否则开启新事务

2.必须以非事务运行的

事务级别 解释
NOT_SUPPORTED 以非事务方式执行,如果当前存在事务,则挂起他
NEVER 以非事务方式执行,如果当前存在事务,则报错

3.有没有事物都可以运行的

事务级别 解释
SUPPORTS 当前存在事务就加入,没有就已非事务方式执行

​ Spring是使用Aop对方法进行拦截实现自动管理事务的,在方法前后进行拦截。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
方法执行前执行
{
    通过DataSource获取一个连接
    设置连接的AutoCommit = false
    存储到ThreadLocal上,与线程绑定
}

执行方法
{
	使用ThreadLocal上的连接执行sql语句,需要保证使用的是ThreadLocal上绑定的连接才能自动管理事物  
}

方法执行后
{
	从ThreadLocal上拿到链接,根据方法执行中是否报错,决定回滚或者提交
}

​ 如何保证方法内获取的连接就是ThreadLocal上存储的连接呢?

​ 获取连接,可以使用DataSourceUtils工具类,如果当前ThreadLocal存在连接,它会返回ThreadLocal上的,否则才返回数据源内的。

1
Connection connection = DataSourceUtils.getConnection(dataSource);

​ 如果使用的是MyBatisMybatis获取连接的是使用Transaction对象获取连接的。整合Spring时使用的是SpringManagedTransaction对象,其会按照Spring的规范,使用DataSourceUtils获取连接。

​ 如果使用的是JdbcTemplate,此对象获取连接方法也是使用的DataSourceUtils工具类。

​ 其他orm框架要想使用Spring的事物管理,都需要使用DataSourceUtils工具类获取连接,这样才能被Spring管理。

下面是DataSourceUtils工具类获取连接的实现逻辑

​ 先从ThreadLocal上获取,没有再从连接池上获取的逻辑就是工具类DataSourceUtils实现的。

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
	public static Connection doGetConnection(DataSource dataSource) throws SQLException {
		//从ThreadLocal上获取连接,委托给TransactionSynchronizationManager
		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();
		}
		
        //否则从连接池获取连接
		Connection con = dataSource.getConnection();

        //如果开启了事物,则设置到ThreadLocal上供下次使用,否则不设置
		if (TransactionSynchronizationManager.isSynchronizationActive()) {
			ConnectionHolder holderToUse = conHolder;
			if (holderToUse == null) {
				holderToUse = new ConnectionHolder(con);
			}
			else {
				holderToUse.setConnection(con);
			}
			holderToUse.requested();
			TransactionSynchronizationManager.registerSynchronization(
					new ConnectionSynchronization(holderToUse, dataSource));
			holderToUse.setSynchronizedWithTransaction(true);
			if (holderToUse != conHolder) {
				TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
			}
		}
		return con;
	}

​ 无论使用的是什么orm框架,只要执行Sql时使用的是ThreadLocal里面的连接,就能被Spring的事务管理器管理。


3.事物传播属性的影响

​ 例如下面这样有一个TestController,调用TestService的方法。Controller 和 Service都开启了事物。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
   //controller 存在事务
	public class TestController {
        @Autowired
        private TestService testService;

        @RequestMapping("test")
        @Transactional
        public void test(){
            sqlSession.selectList("com.item.dao.selectById", 1);
            testService.getItem();
        }
	}



	//service 存在事务
	public class TestService{
        
        @Transactional
        public Item getItem() {
            sqlSession.selectList("com.item.dao.selectById", 1);
            return null;
        }
    }

如果getItem事物传播级别是 REQUIRED

getItem方法会加入当前事物,共享同一条连接,加入之前会先调用connection.savePoint()保存调用点,getItem里面回滚不会回滚整个事物,而是回滚到savePointgetItem执行完成commit也不会提交事物,而是清除savePoint

如果getItem事物传播级别是MANDATORY

​ 同REQUIRED,加入当前事物

如果getItem事物传播级别是 SUPPORTS

​ 同REQUIRED,加入当前事物。

如果getItem事物传播级别是 REQUIRES_NEW

​ 先将当前ThreadLocal里的连接存储起来,如 oldConnection = ThreadLocal.get(),再清空ThreadLocal。

​ 执行getItem时,因为此时ThreadLocal上已经没有连接了,会新建一个连接。

getItem执行完成提交或者回滚后,将刚才存储再临时变量oldConnection的连接重新设置到ThreadLocal。

​ 这样将ThreadLocal连接转移到临时变量里,就叫做对该连接挂起

如果getItem事物传播级别是 NESTED

​ 开启嵌套事物。

如果getItem事物传播级别是 NOT_SUPPORTED

​ 从线程池里获取新连接,使用新连接执行。

如果getItem事物传播级别是 NEVER

​ 因为当前已经存在事物,抛出**Existing transaction found for transaction marked with propagation 'never'异常。

如果Controller上没有事物,只是Service上有事物

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   
	public class TestController {
        @Autowired
        private TestService testService;

        @RequestMapping("test")
        public void test(){
            sqlSession.selectList("com.item.dao.selectById", 1);
            testService.getItem();
        }
	}



	public class TestService{
        
        @Transactional
        public Item getItem() {
            sqlSession.selectList("com.item.dao.selectById", 1);
            return null;
        }
    }

如果getItem事物传播级别是REQUIRES

​ 因为当前ThreadLocal不存在连接,会开启新事物连接。

如果getItem事物传播级别是REQUIRES_NEW

​ 会开启新事物连接。

如果getItem事物传播级别是 NESTED

​ 会开启新事物连接。

如果getItem事物传播级别是 MANDATORY

​ 当前没有事物连接,会抛出No existing transaction found for transaction marked with propagation 'mandatory'异常。

如果getItem事物传播级别是 NOT_SUPPORTED

​ 从连接池里拿到新的连接执行(没有事物管理)。

如果getItem事物传播级别是 NEVER

​ 从连接池里拿到新的连接执行(没有事物管理)。

如果getItem事物传播级别是 SUPPORTS

​ 从连接池里拿到新的连接执行(没有事物管理)。


原理分析

​ 核心原理就是Spring事先从连接池获取连接,与线程绑定,代码里要使用DataSourceUtils来获取连接,这样能保证获取的连接就是Spring管理的那一条连接。这样Spring就可以在方法执行完成后执行commit 或者捕获到特定的异常时rollback。

​ 对于事物传播属性,就是定义事物方法和事物方法之间,或非事物方法和事物方法之间的调用处理逻辑。

再重复下上面的那张表

传播属性 解释
REQUIRED 有就加入,没有就新建,最终会在事物中执行
REQUIRES_NEW 有就先挂起旧连接,创建新的连接,最终会在新的事物连接中执行
MANDATORY 要求当前必须存在事物,不存在就报错
NOT_SUPPORTED 获取一个非事物连接执行,不需要挂起旧的,因为新连接不需要事物管理
NEVER 要求当前必须不存在事物,和MANDATORY相反
SUPPORTS 存在就加入,不存在就使用非事物执行
NESTED 存在就使用嵌套事物,不存在就创建

一些名词解释

加入当前事物

​ 当前线程ThreadLocal已经存在一个connection,直接拿来用,用之前创建savePoint,如果rollback,只是rollback到savePoint不会全部回滚,commit也不会真的commit,而是清除savePoint。

挂起当前事物

​ 因为当前ThreadLocal上已经绑定了connection,要想创建新的事物连接,需要将ThreadLocal上的connection先临时存储到其他地方,这就叫做挂起。新连接执行完成后,再将存储的旧连接还原回去。

一些重要的类

​ 此方法就是使用ThreadLocal绑定资源的类,注重看下getResource方法

1
org.springframework.transaction.support.TransactionSynchronizationManager#getResource

​ 此方法利用TransactionSynchronizationManager保存一条连接到ThreadLocal上,如果没有就利用连接池获取新连接。

1
org.springframework.jdbc.datasource.DataSourceUtils#getConnection

​ 此方法就是事物管理器根据不同传播属性进行不同方式的处理,需要重点看这个方法的源码。

1
org.springframework.transaction.support.AbstractPlatformTransactionManager#getTransaction

​ 此方法就是获取当前线程绑定的连接,其利用了TransactionSynchronizationManager获取当前线程上绑定的连接。

1
org.springframework.jdbc.datasource.DataSourceTransactionManager#doGetTransaction

​ 这两个方法就是如果需要挂起当前连接是的操作,它会解绑当前ThreadLocal上的连接

1
2
org.springframework.transaction.support.AbstractPlatformTransactionManager#suspend
org.springframework.jdbc.datasource.DataSourceTransactionManager#doSuspend

​ 此方法重点看,其实对事物方法进行拦截时的操作,方法执行前通过事物管理器创建事物(其实就是创建连接,绑定到ThreadLocal上),使用try catch捕捉异常,决定是否回滚,最后在commit。

1
org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction

​ 此方法就是进行事物commit操作的,但并不一定真实提交,如果当前存在savePoint说明是加入到其他连接里的,提交操作就是清除savePoint而已。

1
org.springframework.transaction.support.AbstractPlatformTransactionManager#processCommit

​ 这是提交或回滚后清理各种资源你的方法,如果SuspendedResources不为空的话,说明现在还有一个事物被挂起呢,将获取SuspendedResources,将其还原回去。

1
org.springframework.transaction.support.AbstractPlatformTransactionManager#cleanupAfterCompletion

总结

​ 以上就是我对Spring事物的看法,逻辑不复杂,但代码分支太多,可以先从整体把握,再逐渐看细节。

再重复一遍简单的原理:

​ 使用Aop对方法调用前,调用后进行拦截,调用前获取连接设置到ThreadLocal上,调用后将ThreadLocal上的连接回滚或提交。

​ 遇到需要开启新事物的,将当前事物挂起,处理新事物,新事物提交后,还原当前事物连接。

更新

​ AbstractPlatformTransactionManager类上有一个setTransactionSynchronization方法。它可以控制,如果方法受事务管理器管理,但是不需要开启事务的场景下,是否共用同一条jdbc链接。

​ 即一个方法添加了注解,受事务管理,但不需要开启新事物,方法内的若干个请求是否使用同一条数据库连接。默认是开启的,可通过上面那个方法进行控制。

​ 举个例子,如果事务传播级别为NEVER,且外层没有开启事务。这时因为是NEVER级别所以不会开启新的事物,但是方法内执行的多个sql语句是被限制使用同一个sql连接的。

​ 同理NOT_SUPPORTED也可以达成这种条件。

​ 关注下 SYNCHRONIZATION_ALWAYS,SYNCHRONIZATION_ON_ACTUAL_TRANSACTION,SYNCHRONIZATION_NEVER 三个字段。


转载请注明出处:https://www.huangchaoyu.com/2020/09/09/spring事务和事务传播/