本文将带您了解关于由工作问题到Mybatis缓存与Spring事务管理的新内容,同时我们还将为您解释请简述mybatis的工作执行流程的相关知识,另外,我们还将为您提供关于Mybaits源码解析(十二
本文将带您了解关于由工作问题到Mybatis缓存与Spring事务管理的新内容,同时我们还将为您解释请简述mybatis的工作执行流程的相关知识,另外,我们还将为您提供关于Mybaits 源码解析 (十二)----- Mybatis的事务如何被Spring管理?Mybatis和Spring事务中用的Connection是同一个吗?、MyBatis 不支持 Spring 事务管理的问题总结、MyBatis 在 Spring 中的事务管理、mybatis-spring事务处理机制分析的实用信息。
本文目录一览:- 由工作问题到Mybatis缓存与Spring事务管理(请简述mybatis的工作执行流程)
- Mybaits 源码解析 (十二)----- Mybatis的事务如何被Spring管理?Mybatis和Spring事务中用的Connection是同一个吗?
- MyBatis 不支持 Spring 事务管理的问题总结
- MyBatis 在 Spring 中的事务管理
- mybatis-spring事务处理机制分析
由工作问题到Mybatis缓存与Spring事务管理(请简述mybatis的工作执行流程)
太长不看 人士直接到 结论分析;
问题背景:
项目使用SpringBoot+SpringMVC+Mybatis框架
工作中遇到一个工作流向外同步的问题,在本地工作流操作完之后,调用接口推动其他平台的工作流流转。
在本地工作流操作完之后,数据库中业务数据对应的工作流状态会发生变化,比如auditStatus从0转为1等。
主要现象:
工作流的本地操作调用系统的公用接口,在本地工作流操作完之后(已经更改了数据的流程状态),调用dao层根据id去数据库检索该条业务数据。
这时候意料之外的情况出现了,检索出的数据中流程状态仍是工作流未操作之前的。
问题分析:
像这种情况,首先想到的就是Spring的事务,具体点就是事务的传播机制(propagation)和数据隔离级别(ioslation)。代码结构如下
@Override
@Transactional(propagation = Propagation.required)
public void executeSth(Student student) {
Student origin = studentServiceStrong.getById(student.getId());
System.out.println("before update:"+origin);
ShitHandler handler = new ShitHandler();
int i = handler.updateSomeShitByIdWithNewBean(student);
Student dbRec = studentServiceStrong.getById(student.getId());
}
生产环境中的代码是使用的全局事务配置,没有直接加注解,我在这里为了直观写上了等价的注解,
handler.updateSomeShitByIdWithNewBean(student);这一句的内部操作事务的Propagation为REQUIRES_NEW.
现场库使用的是MysqL。下来分析的时候,ioslation走的innodb默认的repeatable read,按理不会出现这种情况,因为外层事务读取的时候内层事务已经提交了。
在深入update方法内部看,原来在这个公共接口中,在新事务创建之前,有对该条数据根据id进行检索,此次检索与更新完之后的检索在同一事务中,根据repeatable read的特点,
两次检索的结果一样,即没有检索到更新,是解释的通的。所以这里先将ioslation设置为read committed,理论上就可以让后一次select检索到数据了。
但是,可怕的但是,系统支持多数据源,在本地用oracle库进行测试时,也出现了同样的问题,oracle默认隔离级别为read committed,按理说最后一次检索应该能检索到更新。
所以我们发现,并不是ioslation的问题,起码不全是ioslation的问题。还有什么可能呢,mybatis缓存。
结论分析:
在以上代码中,MysqL中的同一事务的两次检索结果不一致,最直接的原因并不是repeatable read的特性导致,因为这里其实后一次并未真正落到数据库(oracle也是)---日志并没有对应的sql输出,而是读了Mybatis的一级缓存。
当我们将缓存禁用后,这时在MysqL中的不一致才是repeatable read特性导致的(此时在oracle已经正常)。
Mybatis一级缓存默认开启,在开启时,同一次会话(session)中的第一次检索的对应数据会进行缓存,缓存对应的规则可以参考这篇文章: https://www.cnblogs.com/happyflyingpig/p/7739749.html
众所周知,一级缓存在数据被update时会被对应的移除,但是,实践显示,有一个非常重要的前提:select和之前update在同一事务中,如果在不同的事务中,mybatis不会失效对应的缓存
关闭Mybatis一级缓存:
全局关闭:
mybatis.configuration.local-cache-scope=statement
某一次检索关闭:
在对应的statement中添加 flushCache="true"
加个题外话,很多人说new 出来的对象中的事务spring不能控制,其实这样说是不全面的,这里挖个坑,以后有时间了专门开一篇说。
解决方案:
因此,最后的解决方案为,在select的statement中加上 flushCache="true" ,且将几个相关方法的ioslation设置为read committed
对应资源:
自己写的小demo,有兴趣可以跑跑看 https://gitee.com/uptotwo/ssm
Mybaits 源码解析 (十二)----- Mybatis的事务如何被Spring管理?Mybatis和Spring事务中用的Connection是同一个吗?
不知道一些同学有没有这种疑问,为什么Mybtis中要配置dataSource,Spring的事务中也要配置dataSource?那么Mybatis和Spring事务中用的Connection是同一个吗?我们常用配置如下
<!--会话工厂 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>
<!--spring事务管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!--使用注释事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />
看到没,sqlSessionFactory中配置了dataSource,transactionManager也配置了dataSource,我们来回忆一下SqlSessionFactoryBean这个类
1 protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
2
3 // 配置类
4 Configuration configuration;
5 // 解析mybatis-Config.xml文件,
6 // 将相关配置信息保存到configuration
7 XMLConfigBuilder xmlConfigBuilder = null;
8 if (this.configuration != null) {
9 configuration = this.configuration;
10 if (configuration.getVariables() == null) {
11 configuration.setVariables(this.configurationProperties);
12 } else if (this.configurationProperties != null) {
13 configuration.getVariables().putAll(this.configurationProperties);
14 }
15 //资源文件不为空
16 } else if (this.configLocation != null) {
17 //根据configLocation创建xmlConfigBuilder,XMLConfigBuilder构造器中会创建Configuration对象
18 xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
19 //将XMLConfigBuilder构造器中创建的Configuration对象直接赋值给configuration属性
20 configuration = xmlConfigBuilder.getConfiguration();
21 }
22
23 //略....
24
25 if (xmlConfigBuilder != null) {
26 try {
27 //解析mybatis-Config.xml文件,并将相关配置信息保存到configuration
28 xmlConfigBuilder.parse();
29 if (LOGGER.isDebugEnabled()) {
30 LOGGER.debug("Parsed configuration file: ''" + this.configLocation + "''");
31 }
32 } catch (Exception ex) {
33 throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
34 }
35 }
36
37 if (this.transactionFactory == null) {
38 //事务默认采用SpringManagedTransaction,这一块非常重要
39 this.transactionFactory = new SpringManagedTransactionFactory();
40 }
41 // 为sqlSessionFactory绑定事务管理器和数据源
42 // 这样sqlSessionFactory在创建sqlSession的时候可以通过该事务管理器获取jdbc连接,从而执行SQL
43 configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
44 // 解析mapper.xml
45 if (!isEmpty(this.mapperLocations)) {
46 for (Resource mapperLocation : this.mapperLocations) {
47 if (mapperLocation == null) {
48 continue;
49 }
50 try {
51 // 解析mapper.xml文件,并注册到configuration对象的mapperRegistry
52 XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
53 configuration, mapperLocation.toString(), configuration.getSqlFragments());
54 xmlMapperBuilder.parse();
55 } catch (Exception e) {
56 throw new NestedIOException("Failed to parse mapping resource: ''" + mapperLocation + "''", e);
57 } finally {
58 ErrorContext.instance().reset();
59 }
60
61 if (LOGGER.isDebugEnabled()) {
62 LOGGER.debug("Parsed mapper file: ''" + mapperLocation + "''");
63 }
64 }
65 } else {
66 if (LOGGER.isDebugEnabled()) {
67 LOGGER.debug("Property ''mapperLocations'' was not specified or no matching resources found");
68 }
69 }
70
71 // 将Configuration对象实例作为参数,
72 // 调用sqlSessionFactoryBuilder创建sqlSessionFactory对象实例
73 return this.sqlSessionFactoryBuilder.build(configuration);
74 }
我们看第39行,Mybatis集成Spring后,默认使用的transactionFactory是SpringManagedTransactionFactory,那我们就来看看其获取Transaction的方法
private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
try {
boolean autoCommit;
try {
autoCommit = connection.getAutoCommit();
} catch (SQLException e) {
// Failover to true, as most poor drivers
// or databases won''t support transactions
autoCommit = true;
}
//从configuration中取出environment对象
final Environment environment = configuration.getEnvironment();
//从environment中取出TransactionFactory
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
//创建Transaction
final Transaction tx = transactionFactory.newTransaction(connection);
//创建包含事务操作的执行器
final Executor executor = configuration.newExecutor(tx, execType);
//构建包含执行器的SqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
if (environment == null || environment.getTransactionFactory() == null) {
return new ManagedTransactionFactory();
}
//这里返回SpringManagedTransactionFactory
return environment.getTransactionFactory();
}
@Override
public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
//创建SpringManagedTransaction
return new SpringManagedTransaction(dataSource);
}
SpringManagedTransaction
也就是说mybatis的执行事务的事务管理器就切换成了SpringManagedTransaction,下面我们再去看看SpringManagedTransactionFactory类的源码:
public class SpringManagedTransaction implements Transaction {
private static final Log LOGGER = LogFactory.getLog(SpringManagedTransaction.class);
private final DataSource dataSource;
private Connection connection;
private boolean isConnectionTransactional;
private boolean autoCommit;
public SpringManagedTransaction(DataSource dataSource) {
Assert.notNull(dataSource, "No DataSource specified");
this.dataSource = dataSource;
}
public Connection getConnection() throws SQLException {
if (this.connection == null) {
this.openConnection();
}
return this.connection;
}
private void openConnection() throws SQLException {
//通过DataSourceUtils获取connection,这里和JdbcTransaction不一样
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("JDBC Connection [" + this.connection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring");
}
}
public void commit() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Committing JDBC Connection [" + this.connection + "]");
}
//通过connection提交,这里和JdbcTransaction一样
this.connection.commit();
}
}
public void rollback() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Rolling back JDBC Connection [" + this.connection + "]");
}
//通过connection回滚,这里和JdbcTransaction一样
this.connection.rollback();
}
}
public void close() throws SQLException {
DataSourceUtils.releaseConnection(this.connection, this.dataSource);
}
public Integer getTimeout() throws SQLException {
ConnectionHolder holder = (ConnectionHolder)TransactionSynchronizationManager.getResource(this.dataSource);
return holder != null && holder.hasTimeout() ? holder.getTimeToLiveInSeconds() : null;
}
}
org.springframework.jdbc.datasource.DataSourceUtils#getConnection
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
try {
return doGetConnection(dataSource);
}
catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
}
}
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
Assert.notNull(dataSource, "No DataSource specified");
//TransactionSynchronizationManager重点!!!有没有很熟悉的感觉??
//还记得我们前面Spring事务源码的分析吗?@Transaction会创建Connection,并放入ThreadLocal中
//这里从ThreadLocal中获取ConnectionHolder
ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
logger.debug("Fetching JDBC Connection from DataSource");
//如果没有使用@Transaction,那调用Mapper接口方法时,也是通过Spring的方法获取Connection
Connection con = fetchConnection(dataSource);
if (TransactionSynchronizationManager.isSynchronizationActive()) {
logger.debug("Registering transaction synchronization for JDBC Connection");
ConnectionHolder holderToUse = conHolder;
if (conHolder == null) {
holderToUse = new ConnectionHolder(con);
} else {
conHolder.setConnection(con);
}
holderToUse.requested();
TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
holderToUse.setSynchronizedWithTransaction(true);
if (holderToUse != conHolder) {
//将获取到的ConnectionHolder放入ThreadLocal中,那么当前线程调用下一个接口,下一个接口使用了Spring事务,那Spring事务也可以直接取到Mybatis创建的Connection
//通过ThreadLocal保证了同一线程中Spring事务使用的Connection和Mapper代理类使用的Connection是同一个
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
}
}
return con;
} else {
conHolder.requested();
if (!conHolder.hasConnection()) {
logger.debug("Fetching resumed JDBC Connection from DataSource");
conHolder.setConnection(fetchConnection(dataSource));
}
//所以如果我们业务代码使用了@Transaction注解,在Spring中就已经通过dataSource创建了一个Connection并放入ThreadLocal中
//那么当Mapper代理对象调用方法时,通过SqlSession的SpringManagedTransaction获取连接时,就直接获取到了当前线程中Spring事务创建的Connection并返回
return conHolder.getConnection();
}
}
想看怎么获取connHolder
org.springframework.transaction.support.TransactionSynchronizationManager#getResource
//保存数据库连接的ThreadLocal
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");
@Nullable
public static Object getResource(Object key) {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
//获取ConnectionHolder
Object value = doGetResource(actualKey);
....
return value;
}
@Nullable
private static Object doGetResource(Object actualKey) {
/**
* 从threadlocal <Map<Object, Object>>中取出来当前线程绑定的map
* map里面存的是<dataSource,ConnectionHolder>
*/
Map<Object, Object> map = resources.get();
if (map == null) {
return null;
}
//map中取出来对应dataSource的ConnectionHolder
Object value = map.get(actualKey);
// Transparently remove ResourceHolder that was marked as void...
if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
map.remove(actualKey);
// Remove entire ThreadLocal if empty...
if (map.isEmpty()) {
resources.remove();
}
value = null;
}
return value;
}
我们看到直接从ThreadLocal中取出来的conn,而spring自己的事务也是操作的这个ThreadLocal中的conn来进行事务的开启和回滚,由此我们知道了在同一线程中Spring事务中的Connection和Mybaits中Mapper代理对象中操作数据库的Connection是同一个,当取出来的conn为空时候,调用org.springframework.jdbc.datasource.DataSourceUtils#fetchConnection获取,然后把从数据源取出来的连接返回
private static Connection fetchConnection(DataSource dataSource) throws SQLException {
//从数据源取出来conn
Connection con = dataSource.getConnection();
if (con == null) {
throw new IllegalStateException("DataSource returned null from getConnection(): " + dataSource);
}
return con;
}
我们再来回顾一下上篇文章中的SqlSessionInterceptor
1 private class SqlSessionInterceptor implements InvocationHandler {
2 private SqlSessionInterceptor() {
3 }
4
5 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
6 SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
7
8 Object unwrapped;
9 try {
10 Object result = method.invoke(sqlSession, args);
11 // 如果当前操作没有在一个Spring事务中,则手动commit一下
12 // 如果当前业务没有使用@Transation,那么每次执行了Mapper接口的方法直接commit
13 // 还记得我们前面讲的Mybatis的一级缓存吗,这里一级缓存不能起作用了,因为每执行一个Mapper的方法,sqlSession都提交了
14 // sqlSession提交,会清空一级缓存
15 if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
16 sqlSession.commit(true);
17 }
18
19 unwrapped = result;
20 } catch (Throwable var11) {
21 unwrapped = ExceptionUtil.unwrapThrowable(var11);
22 if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
23 SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
24 sqlSession = null;
25 Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
26 if (translated != null) {
27 unwrapped = translated;
28 }
29 }
30
31 throw (Throwable)unwrapped;
32 } finally {
33 if (sqlSession != null) {
34 SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
35 }
36
37 }
38 return unwrapped;
39 }
40 }
看第15和16行,如果我们没有使用@Transation,Mapper方法执行完后,sqlSession将会提交,也就是说通过org.springframework.jdbc.datasource.DataSourceUtils#fetchConnection获取到的Connection将会commit,相当于Connection是自动提交的,也就是说如果不使用@Transation,Mybatis将没有事务可言。
Mybatis和Spring整合后SpringManagedTransaction和Spring的Transaction的关系:
- 如果开启Spring事务,则先有Spring的Transaction,然后mybatis创建sqlSession时,会创建SpringManagedTransaction并加入sqlSession中,SpringManagedTransaction中的connection会从Spring的Transaction创建的Connection并放入ThreadLocal中获取
- 如果没有开启Spring事务或者第一个方法没有事务后面的方法有事务,则SpringManagedTransaction创建Connection并放入ThreadLocal中
spring结合mybatis后mybaits一级缓存失效分为两种情况:
- 如果没有开启事务,每一次sql都是用的新的SqlSession,这时mybatis的一级缓存是失效的。
- 如果有事务,同一个事务中相同的查询使用的相同的SqlSessioon,此时一级缓存是生效的。
如果使用了@Transation呢?那在调用Mapper代理类的方法之前就已经通过Spring的事务生成了Connection并放入ThreadLocal,并且设置事务不自动提交,当前线程多个Mapper代理对象调用数据库操作方法时,将从ThreadLocal获取Spring创建的connection,在所有的Mapper方法调用完后,Spring事务提交或者回滚,到此mybatis的事务是怎么被spring管理的就显而易见了
还有文章开头的问题,为什么Mybtis中要配置dataSource,Spring的事务中也要配置dataSource?
因为Spring事务在没调用Mapper方法之前就需要开一个Connection,并设置事务不自动提交,那么transactionManager中自然要配置dataSource。那如果我们的Service没有用到Spring事务呢,难道就不需要获取数据库连接了吗?当然不是,此时通过SpringManagedTransaction调用org.springframework.jdbc.datasource.DataSourceUtils#getConnection#fetchConnection方法获取,并将dataSource作为参数传进去,实际上获取的Connection都是通过dataSource来获取的。
原文出处:https://www.cnblogs.com/java-chen-hao/p/11839993.html
MyBatis 不支持 Spring 事务管理的问题总结
之前有个同事问我, 关于 MyBatis 不支持 Spring 事务管理的问题:
对 spring,mybatis 进行整合时发现事务不能进行回滚处理,上网查了很多资料依旧还没解释,很多都是说要抛出一个 runtimeException 才能回滚的,但尝试过多种还不能。
这里我给出我的解决方法和答案,我自己尝试成功
我用的是 SSM3 的框架 Spring MVC 3.1 + Spring 3.1 + Mybatis3.1 + Oracle 数据库
第一种情况:
Spring MVC 和 Spring 整合的时候,SpringMVC 的 springmvc.xml 文件中 配置扫描包,不要包含 service 的注解,Spring 的 applicationContext.xml 文件中 配置扫描包时,不要包含 controller 的注解,如下所示:
SpringMVC 的 xml 配置:
<context:component-scan base-package="com.insigma">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
</context:component-scan>
Spring MVC 启动时的配置文件,包含组件扫描、url 映射以及设置 freemarker 参数,让 spring 不扫描带有 @Service 注解的类。为什么要这样设置?因为 springmvc.xml 与 applicationContext.xml 不是同时加载,如果不进行这样的设置,那么,spring 就会将所有带 @Service 注解的类都扫描到容器中,等到加载 applicationContext.xml 的时候,会因为容器已经存在 Service 类,使得 cglib 将不对 Service 进行代理,直接导致的结果就是在 applicationContext 中的事务配置不起作用,发生异常时,无法对数据进行回滚。以上就是原因所在。
同样的在 Spring 的 xml 配置如下:
<context:component-scan base-package="com.insigma">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
扫描包路径,不扫描带有 @Controller 注解的类。因为这些类已经随容器启动时,在 springmvc.xml 中扫描过一遍了。
完成以上工作,我的 Spring 就支持事务回滚了。OK
第二种情况:使用了 MySQL 数据库,如果用 mysql 数据库,数据库表你如果是自动建表,那么就需要把建表的 Engine 设置为 InnoDB 格式,自动建表的格式为:MyISAM,这中格式的是不支持事务管理的。
第三种情况:在测试代码中,一定要在 catch 块中抛出 异常 ,以便 Spring 事务能发现异常。
注意以上几点就 OK 了。
MyBatis 在 Spring 中的事务管理
项目中经常遇到 MyBatis 与 Spring 的组合开发,并且相应的事务管理交给 Spring。今天我这里记录一下 Spring 中 Mybatis 的事务管理。
先看代码:
spring-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--开启注解-->
<context:annotation-config/>
<!--加载属性文件-->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:db.properties</value>
</list>
</property>
</bean>
<!--扫描组建-->
<context:component-scan base-package="com.xwszt.txdemo"/>
<!--开启事务注解-->
<tx:annotation-driven transaction-manager="transactionManager"/>
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!--配置数据源-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${mysql.jdbc.url}"/>
<property name="user" value="${mysql.jdbc.user}"/>
<property name="password" value="${mysql.jdbc.password}"/>
<!--Connection Pooling Info -->
<property name="initialPoolSize" value="3"/>
<property name="minPoolSize" value="2"/>
<property name="maxPoolSize" value="15"/>
<property name="acquireIncrement" value="3"/>
<property name="maxStatements" value="8"/>
<property name="maxStatementsPerConnection" value="5"/>
<property name="maxIdleTime" value="1800"/>
<property name="autoCommitOnClose" value="false"/>
</bean>
<!--mybatis配置-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mapper/*"/>
</bean>
<!--mybatis扫描mapper对应类的配置-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.xwszt.txdemo.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!--事务配置-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
db.properties
##mysql
jdbc.driver=com.mysql.jdbc.Driver
mysql.jdbc.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&useAffectedRows=true&allowPublicKeyRetrieval=true
mysql.jdbc.user=root
mysql.jdbc.password=*********(这里根据自己修改)
db.sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL,
`password` varchar(50) DEFAULT NULL,
`salt` varchar(50) DEFAULT NULL,
`sex` varchar(10) DEFAULT NULL,
`address` varchar(50) DEFAULT NULL,
`cellphone` varchar(30) DEFAULT NULL,
`email` varchar(30) DEFAULT NULL,
`islock` smallint(1) unsigned NOT NULL DEFAULT ''0'',
`isvalidate` smallint(1) unsigned NOT NULL DEFAULT ''1'',
`isdel` smallint(1) unsigned NOT NULL DEFAULT ''0'',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=124 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
SET FOREIGN_KEY_CHECKS = 1
UserDAO.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.xwszt.txdemo.dao.UserDAO">
<insert id="insert" parameterType="com.xwszt.txdemo.entities.User">
insert into `user`
(`id`, `username`, `password`, `salt`, `sex`, `address`, `cellphone`, `email`, `islock`,`isvalidate`,`isdel`)
values
(#{id}, #{username},#{password},#{salt},#{sex},#{address},#{cellphone},#{email},#{lock},#{validate},#{del})
</insert>
</mapper>
UserDAO.java
package com.xwszt.txdemo.dao;
import com.xwszt.txdemo.entities.User;
public interface UserDAO {
void insert(User user);
}
User.java
package com.xwszt.txdemo.entities;
import lombok.Data;
import java.io.Serializable;
@Data
public class User implements Serializable {
private Long id;
private String username;
private String password;
private String salt;
private String sex;
private String address;
private String cellphone;
private String email;
private boolean lock;
private boolean validate;
private boolean del;
}
UserService.java
package com.xwszt.txdemo.service;
public interface UserService {
void doSomething() throws Exception;
boolean saveUser() throws Exception;
}
UserServiceImpl.java
package com.xwszt.txdemo.service.impl;
import com.xwszt.txdemo.dao.UserDAO;
import com.xwszt.txdemo.entities.User;
import com.xwszt.txdemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserService target;
@Autowired
private UserDAO userDAO;
@Override
public void doSomething() throws Exception {
target.saveUser();
}
@Transactional
@Override
public boolean saveUser() throws Exception {
User user = new User();
user.setId(123l);
user.setUsername("zhangsan");
user.setPassword("123");
user.setSalt("456");
user.setSex("FEMAIL");
user.setAddress("上海市张江高科");
user.setCellphone("13582911229");
user.setEmail("978732467@qq.com");
user.setLock(false);
user.setValidate(true);
user.setDel(false);
userDAO.insert(user);
return true;
}
}
UserTest.java
package com.xwszt.txdemo;
import com.xwszt.txdemo.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-content.xml")
public class UserTest {
@Autowired
private UserService userService;
@Test
public void saveUserTest() {
try {
userService.doSomething();
} catch (Exception e) {
e.printStackTrace();
}
}
}
到此为止,代码已经贴完了。
那么,问题在哪儿呢?
在 Service 层,如果你仔细看,发现在 service 层 saveUser 方法上加了注解 @Transactional,那么运行测试代码,不出意外(网络断了等)的情况下数据库里肯定会插入一条数据。
假如,我把 @Transactional 这个注解去掉了,也就是说 saveUser 不再使用 spring 的事务管理了,那么数据库里是不是没有插入数据呢?答案是否定的。数据库里依然会插入一条数据。
那这又是为什么呢?
如果使用 Spring 进行事务管理,这里提交的时候是 Spring 的事务管理 commit 了事务。
在没有使用 Spring 管理的事务时,是没有使用 Spring 容器管理的 SqlSession 提交了事务。
==================================================
接下来,一个新的问题。
在 saveUser 方法上使用了 @Transactional 注解,表明这个方法是 Spring 容器管理的事务,那么我在 userDAO.insert (user); 之后抛出异常,那么插入的数据会回滚吗?
@Transactional
@Override
public boolean saveUser() throws Exception {
User user = new User();
user.setId(123l);
user.setUsername("zhangsan");
user.setPassword("123");
user.setSalt("456");
user.setSex("FEMAIL");
user.setAddress("上海市张江高科");
user.setCellphone("13582911229");
user.setEmail("978732467@qq.com");
user.setLock(false);
user.setValidate(true);
user.setDel(false);
userDAO.insert(user);
if (true) {
throw new Exception("破坏性测试");
}
return true;
}
答案是:不会回滚。
那怎样才会回滚呢?配置 rollback 即可。即:
@Transactional(rollbackFor = Exception.class)
mybatis-spring事务处理机制分析
Spring官方并没有提供对MyBatis的集成方案,于是MyBatis项目组自己写了一个项目mybatis-spring专门用于在spring中使用MyBatis。
mybatis-spring的实现很大程度上依赖spring jdbc的事务管理,所以我们先看一下在spring中直接使用jdbc访问数据库时是如何处理事务的。无论你是使用@Transactional注解这样的AOP配置方式,还是TransactionTemplate这样的编码方式,最终执行的操作事务的代码都会是类似下面这样
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
PlatformTransactionManager txManager = new DataSourceTransactionManager(dataSource);
TransactionStatus status = txManager.getTransaction(def);
try {
//get jdbc connection...
//execute sql...
txManager.commit(status);
}
catch (Exception e) {
txManager.rollback(status);
throw e;
}
可以看到PlatformTransactionManager的getTransaction(), rollback(), commit()是spring处理事务的核心api,分别对应事务的开始,提交和回滚。
spring事务处理的一个关键是保证在整个事务的生命周期里所有执行sql的jdbc connection和处理事务的jdbc connection始终是同一个。然后执行sql的业务代码一般都分散在程序的不同地方,如何让它们共享一个jdbc connection呢?这里spring做了一个前提假设:即一个事务的操作一定是在一个thread中执行,且一个thread中如果有多个不同jdbc connection生成的事务的话,他们必须顺序执行,不能同时存在。(这个假设在绝大多数情况下都是成立的)。基于这个假设,spring在transaction创建时,会用ThreadLocal把创建这个事务的jdbc connection绑定到当前thread,接下来在事务的整个生命周期中都会从ThreadLocal中获取同一个jdbc connection。
我们看一下详细调用过程
- TransactionSynchronizationManager负责从ThreadLocal中存取jdbc connection
- 创建事务的时候会通过dataSource.getConnection()获取一个新的jdbc connection,然后绑定到ThreadLocal
- 在业务代码中执行sql时,通过DataSourceUtils.getConnection()从ThreadLocal中获取当前事务的jdbc connection, 然后在该jdbc connection上执行sql
- commit和rollback事务时,从ThreadLocal中获取当前事务的jdbc connection,然后对该jdbc connection进行commit和rollback
对spring jdbc的事务处理有了了解后,我们来看mybatis是如何通过spring处理事务的。
先看一下配置
<bean id="transactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="sqlSessionFactory">
<property name="dataSource" ref="dataSource" />
<property name="transactionFactory">
<bean/>
</property>
</bean>
<bean id="sqlSession">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
- mybatis-spring依赖DataSourceTransactionManager来处理事务,并没有创建自己的PlatformTransactionManager实现。
- mybatis通过SqlSessionFactoryBuilder创建SqlSessionFactory,而mybatis-spring通过SqlSessionFactoryBean创建SqlSessionFactory。
- 配置使用SpringManagedTransactionFactory来创建MyBatis的Transaction实现SpringManagedTransaction
- 配置使用SqlSessionTemplate代替通过SqlSessionFactory.openSession()获取SqlSession
然后看其调用过程
可以看到mybatis-spring处理事务的主要流程和spring jdbc处理事务并没有什么区别,都是通过DataSourceTransactionManager的getTransaction(), rollback(), commit()完成事务的生命周期管理,而且jdbc connection的创建也是通过DataSourceTransactionManager.getTransaction()完成,mybatis并没有参与其中,mybatis只是在执行sql时通过DataSourceUtils.getConnection()获得当前thread的jdbc connection,然后在其上执行sql。
下面结合代码来看
<SqlSessionUtils>:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Creating a new SqlSession");
}
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
SqlSessionHolder holder;
if (TransactionSynchronizationManager.isSynchronizationActive()) {
Environment environment = sessionFactory.getConfiguration().getEnvironment();
if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Registering transaction synchronization for SqlSession [" + session + "]");
}
holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
TransactionSynchronizationManager.bindResource(sessionFactory, holder);
TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
holder.setSynchronizedWithTransaction(true);
holder.requested();
} else {
if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional");
}
} else {
throw new TransientDataAccessResourceException(
"SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");
}
}
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because synchronization is not active");
}
}
}
执行sql时调用sqlSessionTemplate的insert,update,delete方法,sqlSessionTemplate是DefaultSqlSession的一个代理类,它通过SqlSessionUtils.getSqlSession()试图从ThreadLocal获取当前事务所使用的SqlSession。如果是第一次获取时会调用SqlSessionFactory.openSession()创建一个SqlSession并绑定到ThreadLocal,同时还会通过TransactionSynchronizationManager注册一个SqlSessionSynchronization。
<SqlSessionSynchronization>:
public void beforeCommit(boolean readOnly) {
// Connection commit or rollback will be handled by ConnectionSynchronization or
// DataSourceTransactionManager.
// But, do cleanup the SqlSession / Executor, including flushing BATCH statements so
// they are actually executed.
// SpringManagedTransaction will no-op the commit over the jdbc connection
// TODO This updates 2nd level caches but the tx may be rolledback later on!
if (TransactionSynchronizationManager.isActualTransactionActive()) {
try {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Transaction synchronization committing SqlSession [" + this.holder.getSqlSession() + "]");
}
this.holder.getSqlSession().commit();
} catch (PersistenceException p) {
if (this.holder.getPersistenceExceptionTranslator() != null) {
DataAccessException translated = this.holder
.getPersistenceExceptionTranslator()
.translateExceptionIfPossible(p);
if (translated != null) {
throw translated;
}
}
throw p;
}
}
SqlSessionSynchronization是一个事务生命周期的callback接口,mybatis-spring通过SqlSessionSynchronization在事务提交和回滚前分别调用DefaultSqlSession.commit()和DefaultSqlSession.rollback()
<BaseExecutor>:
public void commit(boolean required) throws SQLException {
if (closed) throw new ExecutorException("Cannot commit, transaction is already closed");
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
public void rollback(boolean required) throws SQLException {
if (!closed) {
try {
clearLocalCache();
flushStatements(true);
} finally {
if (required) {
transaction.rollback();
}
}
}
}
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
<SpringManagedTransaction>:
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
public void commit() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Committing JDBC Connection [" + this.connection + "]");
}
this.connection.commit();
}
}
public void rollback() throws SQLException {
if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Rolling back JDBC Connection [" + this.connection + "]");
}
this.connection.rollback();
}
}
<DataSourceUtils>:
/**
* Determine whether the given JDBC Connection is transactional, that is,
* bound to the current thread by Spring''s transaction facilities.
* @param con the Connection to check
* @param dataSource the DataSource that the Connection was obtained from
* (may be {@code null})
* @return whether the Connection is transactional
*/
public static boolean isConnectionTransactional(Connection con, DataSource dataSource) {
if (dataSource == null) {
return false;
}
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
return (conHolder != null && connectionEquals(conHolder, con));
}
这里的DefaultSqlSession只会进行一些自身缓存的清理工作,并不会真正提交事务给数据库,原因是这里的DefaultSqlSession使用的Transaction实现为SpringManagedTransaction,SpringManagedTransaction在提交事务前会检查当前事务是否应该由spring控制,如果是,则不会自己提交事务,而将提交事务的任务交给spring,所以DefaultSqlSession并不会自己处理事务。
<SpringManagedTransaction>:
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();
}
return this.connection;
}
/**
* Gets a connection from Spring transaction manager and discovers if this
* {@code Transaction} should manage connection or let it to Spring.
* <p>
* It also reads autocommit setting because when using Spring Transaction MyBatis
* thinks that autocommit is always false and will always call commit/rollback
* so we need to no-op that calls.
*/
private void openConnection() throws SQLException {
this.connection = DataSourceUtils.getConnection(this.dataSource);
this.autoCommit = this.connection.getAutoCommit();
this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(
"JDBC Connection ["
+ this.connection
+ "] will"
+ (this.isConnectionTransactional ? " " : " not ")
+ "be managed by Spring");
}
}
DefaultSqlSession执行sql时,会通过SpringManagedTransaction调用DataSourceUtils.getConnection()从ThreadLocal中获取jdbc connection并在其上执行sql。
总结:mybatis-spring处理事务的主要流程和spring jdbc处理事务并没有什么区别,都是通过DataSourceTransactionManager的getTransaction(), rollback(), commit()完成事务的生命周期管理,而且jdbc connection的创建也是通过DataSourceTransactionManager.getTransaction()完成,mybatis并没有参与其中,mybatis只是在执行sql时通过DataSourceUtils.getConnection()获得当前thread的jdbc connection,然后在其上执行sql。
mybatis-spring做的最主要的事情是:
- 在SqlSession执行sql时通过用SpringManagedTransaction代替mybatis的JdbcTransaction,让SqlSession从spring的ThreadLocal中获取jdbc connection。
- 通过注册事务生命周期callback接口SqlSessionSynchronization,让SqlSession有机会在spring管理的事务提交或回滚时清理自己的内部缓存。
我们今天的关于由工作问题到Mybatis缓存与Spring事务管理和请简述mybatis的工作执行流程的分享已经告一段落,感谢您的关注,如果您想了解更多关于Mybaits 源码解析 (十二)----- Mybatis的事务如何被Spring管理?Mybatis和Spring事务中用的Connection是同一个吗?、MyBatis 不支持 Spring 事务管理的问题总结、MyBatis 在 Spring 中的事务管理、mybatis-spring事务处理机制分析的相关信息,请在本站查询。
本文标签: