前言

  • 使用 MyBatis-Spring 的主要原因是它允许 MyBatis 参与到 Spring 的事务管理中。而 不是给 MyBatis 创建一个新的特定的事务管理器,MyBatis-Spring 利用了存在于 Spring 中的 DataSourceTransactionManager

如何集成 Spring的事务管理

  • 配置 DataSourceTransactionManager Bean
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<!-- 数据库连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">

<!-- 基本属性 url、IUser、password -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/iframe?useUnicode=true&amp;characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>

<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="1"/>
<property name="minIdle" value="1"/>
<property name="maxActive" value="20"/>

<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="60000"/>

<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000"/>

<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000"/>

<!--
用来检测连接是否有效的sql,要求是一个查询语句。
如果validationQuery为null,testOnBorrow、testOnReturn、
testWhileIdle都不会其作用
-->
<property name="validationQuery" value="SELECT 1 FROM DUAL"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>

<!--
打开PSCache,并且指定每个连接上PSCache的大小
如果用Oracle,则把poolPreparedStatements配置为true,mysql可以配置为false
-->
<property name="poolPreparedStatements" value="true"/>
<property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>

<!-- 配置监控统计拦截的filters,去掉后监控界面sql无法统计 -->
<property name="filters" value="stat,wall,log4j"/>

<!--
如果配置了proxyFilters,此配置可以不配置
druid.stat.mergeSql=true 合并执行的相同sql,避免因为参数不同而统计多条sql语句
druid.stat.slowSqlMillis=10000 用来配置SQL慢的标准,执行时间超过slowSqlMillis的就是慢

<property name="connectionProperties" value="druid.stat.mergeSql=true;druid.stat.slowSqlMillis=10000" />
-->

<!-- 监控统计拦截的filters -->
<!-- 并在filters属性中配置了log4j -->
<!--<property name="proxyFilters">-->
<!--<list>-->
<!--<ref bean="stat-filter" />-->
<!--<ref bean="log-filter" />-->
<!--</list>-->
<!--</property>-->

<!-- 连接属性 -->
<property name="connectionProperties">
<value>clientEncoding=UTF-8</value>
</property>
</bean>

<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
<constructor-arg index="1" value="BATCH" />
</bean>

<!-- 配置SqlSessionFactory对象 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="typeAliasesPackage" value="org.springframework.iframe.entity"/>
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>

<!-- 配置扫描Dao接口包,动态实现Dao接口,注入到spring容器中 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<property name="basePackage" value="org.springframework.iframe.mapper"></property>
</bean>


<!-- (事务管理)transaction manager, use JtaTransactionManager for global tx -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<tx:annotation-driven proxy-target-class="false" transaction-manager="transactionManager" />
  • 要注意, 为事务管理器指定的 DataSource 必须和用来创建 SqlSessionFactoryBean 的 是同一个数据源,否则事务管理器就无法工作了。

  • 一旦 SpringDataSourceTransactionManager 配置好了,你可以在 Spring 中你可以使用@Transactional 注解来完成事物操作。在事务处理期间,一个单独的 SqlSession 对象(线程级别)将会被创建 和使用。当事务完成时,这个 SqlSession 会以合适的方式提交或回滚。相反如果没有开启事物那么SqlSession 对象就是方法级别的了,每次调用Mapper里的方法都会返回一个新的SqlSession 来处理,下面来看其内部是怎么实现的

事务实现解析

  • Spring集成以后,Spring提供了一个全局唯一的SqlSessionTemplate 来完成DefailtSqlSession的功能

  • 进入SqlSessionTemplate 可以看到里面有个SqlSession 属性,看属性名可以看出这里又用了动态代理,为什么又要代理呢?下面来看看

1
2
// SqlSession代理
private final SqlSession sqlSessionProxy;
  • 观察其构造方法,这里形成SqlSession代理类,再来看动态代理类SqlSessionInterceptor做了什么
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {

notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");

this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
// 形成SqlSession代理类
this.sqlSessionProxy = (SqlSession) newProxyInstance(
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
}
  • 进入SqlSessionInterceptor类,这个SqlSession代理类的出现是为了让Spring 来管理SqlSession 的,从而实现事务管理
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
46
/**
* SqlSession 代理类,MyBatis路由方法调用得到有Spring Transaction的SqlSession
* Proxy needed to route MyBatis method calls to the proper SqlSession got
* from Spring's Transaction Manager
* It also unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to
* pass a {@code PersistenceException} to the {@code PersistenceExceptionTranslator}.
*/
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取SqlSession(这个SqlSession才是真正使用的,它不是线程安全的)
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
// 调用真实SqlSession的方法
Object result = method.invoke(sqlSession, args);
// 判断一下当前的sqlSession是否被Spring托管
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
// 没有使用事务
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
// 关闭SqlSession,如果sqlSession被Spring管理 则调用holder.released(); 使计数器-1
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
  • 进入getSqlSession()方法,这里是获取SqlSession 的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

// 根据sqlSessionFactory从当前线程对应的资源map中获取SqlSessionHolder
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
// 如果找不到,则根据执行类型构造一个新的sqlSession
LOGGER.debug(() -> "Creating a new SqlSession");
session = sessionFactory.openSession(executorType);

registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

return session;
}
  • 关注TransactionSynchronizationManager 内部成员,这里使用TreadLocal记录事务的一些属性,用于应用扩展同步器的使用,在事务的开启,挂起,提交等各个点上回调应用的逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
   // 应用代码随事务的声明周期绑定的对象
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");

// synchronizations-使用的同步器,用于应用扩展
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");

// 事务的名称
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");

// 事务是否是只读
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");

// 事务的隔离界别
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");

// 事务是否开启
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
  • 回到SqlSessionInterceptorinvoke方法,这里有个if判断if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) { 来判断是否开启了Spring事务,如果该Session未被Spring托管则自动commit
1
2
3
4
5
6
7
8
public static boolean isSqlSessionTransactional(SqlSession session, SqlSessionFactory sessionFactory) {
notNull(session, NO_SQL_SESSION_SPECIFIED);
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);

SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

return (holder != null) && (holder.getSqlSession() == session);
}
  • 关注invoke方法的finally块的 closeSqlSession()方法,如果是开启了事务则没有执行session.close();
1
2
3
4
5
6
finally {
if (sqlSession != null) {
// 关闭SqlSession,如果sqlSession被Spring管理 则调用holder.released(); 使计数器-1
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
notNull(session, NO_SQL_SESSION_SPECIFIED);
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);

SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
if ((holder != null) && (holder.getSqlSession() == session)) {
LOGGER.debug(() -> "Releasing transactional SqlSession [" + session + "]");
// 如果是开启了事务 SqlSession是没有被close的,所以方法体内使用的是一个SqlSession,当然一级缓存是生效的
holder.released();
} else {
LOGGER.debug(() -> "Closing non transactional SqlSession [" + session + "]");
session.close();
}
}

总结

  • 通过上述代码可以得出如果开启了事务,同一事务中同一个sqlSessionFactory创建的唯一sqlSession,一个事务中使用的是同一个sqlSession,为什么要用同一个sqlSession呢,是为了使用同一个connection (JDBC)
  • 如果没有开启事务,调用一次mapper里的方法将会新建一个sqlSession来执行方法

参考