mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/testjpa?serverTimezone=GMT%2B8"/> <property name="username" value="root"/> <property name="password" value="123123"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mappers/UsersMapper.xml"/> </mappers></configuration>
UsersMapper.xml
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.pack.mapper.UsersMapper"> <select id="selectList" resultType="com.pack.domain.Users"> select * from t_users </select></mapper>
UsersMapper.java
package com.pack.mapper;import java.util.List;import com.pack.domain.Users;public interface UsersMapper { List<Users> selectList() ;}
Users.java
public class Users{ private String id ; private String username ; private String password ;}
UsersMapperTest.java測試類
public class UsersMapperTest { private static final int MAX = 100 ; private SqlSessionFactory sqlSessionFactory ; private Thread[] threads = new Thread[MAX] ; private CountDownLatch cdl = new CountDownLatch(MAX) ; @Before public void init() throws Exception { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);} @Test public void testSelectList() throws Exception { SqlSession session = sqlSessionFactory.openSession() ; UsersMapper mapper = session.getMapper(UsersMapper.class) ; for (int i = 0; i < MAX; i++) { threads[i] = new Thread(() -> { try { cdl.await() ; System.out.println(mapper.selectList()) ; } catch (InterruptedException e) { e.printStackTrace(); } }) ; } for (int i = 0; i < MAX; i++) { threads[i].start() ; cdl.countDown() ; } System.in.read() ;}}
啟動100個線程同時查詢,結果如下:
### Cause: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.Listat org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:153)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:145)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:147)at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:80)at org.apache.ibatis.binding.MapperProxy$PlainMethodInvoker.invoke(MapperProxy.java:145)at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:86)at com.sun.proxy.$Proxy8.selectList(Unknown Source)at test.UsersMapperTest.lambda$0(UsersMapperTest.java:39)at java.lang.Thread.run(Thread.java:745)Caused by: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.Listat org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:152)at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:89)at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:151)... 9 more[Users [id=1, username=admin, password=123123], Users [id=2, username=guest, password=111111]][Users [id=1, username=admin, password=123123], Users [id=2, username=guest, password=111111]]
程序拋出了異常ClassCastException類型轉換異常。也就是在多個線程同時使用SqlSession時出現了類型轉換錯誤。
根據錯誤信息,把錯誤定位到DefaultSqlSession.java:153
public class DefaultSqlSession implements SqlSession { private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) { try { MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, handler); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); // 這里拋出的異常 } finally { ErrorContext.instance().reset(); }} }
繼續根據錯誤日志,確定是執行下面這行代碼出現錯誤
executor.query(ms, wrapCollection(parameter), rowBounds, handler);
而executor根據錯誤日志確定為BaseExecutor類
public abstract class BaseExecutor implements Executor { public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { // ... List<E> list; try { queryStack++; // 從本地緩存中獲取數據,如果有會強制轉換為List對象 // 位置1: list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { // 如果緩存中沒有,則會進入該方法執行 list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); } } finally { queryStack--; } // ... return list;} private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List<E> list; // 先將一個枚舉值存入到緩存中ExecutionPlaceholder localCache.putObject(key, EXECUTION_PLACEHOLDER); // 位置2 try { // 做實際的查詢 list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); } finally { // 刪除上面存入的值 localCache.removeObject(key); } // 將查詢出來的數據緩存起來 localCache.putObject(key, list); // 位置3 if (ms.getStatementType() == StatementType.CALLABLE) { localOutputParameterCache.putObject(key, parameter); } return list;}}public enum ExecutionPlaceholder { EXECUTION_PLACEHOLDER}
分析:當線程1執行到‘位置2’時,此時緩存中緩存了ExecutionPlaceholder枚舉值,這是線程2開始執行‘位置1’此時線程2從緩存中是能獲取值,此值是ExecutionPlaceholder枚舉值,該值怎么可能轉換為List,所以這里就會拋出類型轉換異常了。
如果想正確執行,只能是每個線程創建一個新的SqlSession對象。
// 獲取SqlSession對象// SqlSessionFactory的實現是DefaultSqlSessionFactory對象sqlSessionFactory.openSession()
進入openSession()方法
public class DefaultSqlSessionFactory implements SqlSessionFactory { @Override public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);} private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { final Environment environment = configuration.getEnvironment(); final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); final Executor executor = configuration.newExecutor(tx, execType); // SqlSession默認實現使用的DefaultSqlSession。 return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); }}}
DefaultSqlSession.java
/*** The default implementation for {@link SqlSession}.* Note that this class is not Thread-Safe.** @author Clinton Begin*/public class DefaultSqlSession implements SqlSession { // ... }// 在這注釋中已經提到了該類is not Thread-Safe.
在Springboot中是如何保證線程安全的呢?
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version></dependency>
@SpringBootApplication@MapperScan({"com.pack.mapper"})public class SpringBootTransactionalApplication { public static void main(String[] args) { SpringApplication.run(SpringBootTransactionalApplication.class, args);}}// 重點在這@Import上@Import(MapperScannerRegistrar.class)public @interface MapperScan {}
public class MybatisAutoConfiguration implements InitializingBean { @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean() ; // ... return factory.getObject() ;} @Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { ExecutorType executorType = this.properties.getExecutorType(); if (executorType != null) { return new SqlSessionTemplate(sqlSessionFactory, executorType); } else { return new SqlSessionTemplate(sqlSessionFactory); }}}
@MapperScan注解中應用了@Import(MapperScannerRegistrar.class)
在這Import類中會注冊一個MapperScannerConfigurer配置類
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { AnnotationAttributes mapperScanAttrs = AnnotationAttributes .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName())); if (mapperScanAttrs != null) { registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry, generateBaseBeanName(importingClassMetadata, 0)); }} void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs, BeanDefinitionRegistry registry, String beanName) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class); builder.addPropertyValue("processPropertyPlaceHolders", true); Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass"); if (!Annotation.class.equals(annotationClass)) { builder.addPropertyValue("annotationClass", annotationClass); } Class<?> markerInterface = annoAttrs.getClass("markerInterface"); if (!Class.class.equals(markerInterface)) { builder.addPropertyValue("markerInterface", markerInterface); } // ... List<String> basePackages = new ArrayList<>(); basePackages.addAll( Arrays.stream(annoAttrs.getStringArray("value")).filter(StringUtils::hasText).collect(Collectors.toList())); basePackages.addAll(Arrays.stream(annoAttrs.getStringArray("basePackages")).filter(StringUtils::hasText) .collect(Collectors.toList())); basePackages.addAll(Arrays.stream(annoAttrs.getClassArray("basePackageClasses")).map(ClassUtils::getPackageName) .collect(Collectors.toList())); if (basePackages.isEmpty()) { basePackages.add(getDefaultBasePackage(annoMeta)); } // ... builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages)); registry.registerBeanDefinition(beanName, builder.getBeanDefinition()); } }
這里注冊了一個核心類MapperScannerConfigurer該類用來掃描Mapper接口,并注冊為Bean。
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware { // 實際Mapper接口注冊的是MapperFactoryBean對象一個FactoryBean對象 private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class; public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { // ... // 該類用來掃描指定包下的類,并如果符合條件(是接口類)將其注冊為Bean(FactoryBean) ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); scanner.setAddToConfig(this.addToConfig); scanner.setAnnotationClass(this.annotationClass); scanner.setMarkerInterface(this.markerInterface); // 為null scanner.setSqlSessionFactory(this.sqlSessionFactory); // 為null scanner.setSqlSessionTemplate(this.sqlSessionTemplate); // 為null scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName); // 為null scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName); scanner.setResourceLoader(this.applicationContext); scanner.setBeanNameGenerator(this.nameGenerator); scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass); if (StringUtils.hasText(lazyInitialization)) { scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization)); } if (StringUtils.hasText(defaultScope)) { scanner.setDefaultScope(defaultScope); } scanner.registerFilters(); scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); } public Set<BeanDefinitionHolder> doScan(String... basePackages) { // 調用父類的doSan方法進行查找所有符合條件的類,并將其注冊到容器中 Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages); // 對找到的BeanDefinition對象進行處理 processBeanDefinitions(beanDefinitions); return beanDefinitions; } private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { AbstractBeanDefinition definition; BeanDefinitionRegistry registry = getRegistry(); for (BeanDefinitionHolder holder : beanDefinitions) { definition = (AbstractBeanDefinition) holder.getBeanDefinition(); boolean scopedProxy = false; if (ScopedProxyFactoryBean.class.getName().equals(definition.getBeanClassName())) { definition = (AbstractBeanDefinition) Optional .ofNullable(((RootBeanDefinition) definition).getDecoratedDefinition()) .map(BeanDefinitionHolder::getBeanDefinition).orElseThrow(() -> new IllegalStateException( "The target bean definition of scoped proxy bean not found. Root bean definition[" + holder + "]")); scopedProxy = true; } String beanClassName = definition.getBeanClassName(); // the mapper interface is the original class of the bean // but, the actual class of the bean is MapperFactoryBean definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59 // 重點是這里指定BeanClass對象,一個FactoryBean工廠Bean。 definition.setBeanClass(this.mapperFactoryBeanClass); definition.getPropertyValues().add("addToConfig", this.addToConfig); // ... if (ConfigurableBeanFactory.SCOPE_SINGLETON.equals(definition.getScope()) && defaultScope != null) { definition.setScope(defaultScope); } // ... } }}
通過上面的源碼可知,所有的Mapper接口都會通過MapperFactoryBean(是個FactoryBean)來注冊的Bean對象,在注入Mapper Bean的時候實際注入的是FactoryBean#getObject的返回值類型。
通過上面知道了所有的Mapper都是通過FactoryBean來構建的。
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> { public T getObject() throws Exception { // getSqlSession()方法返回的是SqlSessionTemplate對象 return getSqlSession().getMapper(this.mapperInterface); } }
MapperFactoryBean類繼承了SqlSessionDaoSupport對象
public abstract class SqlSessionDaoSupport extends DaoSupport { private SqlSessionTemplate sqlSessionTemplate; public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) { this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory); } } public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) { this.sqlSessionTemplate = sqlSessionTemplate; } public SqlSession getSqlSession() { return this.sqlSessionTemplate; }}
在該類中提供了幾個setter方法,當在注冊當前MapperFactoryBean對象的時候就會注入在MybatisAutoConfiguration自動配置類中注冊的SqlSessionFactory和SqlSessionTemplate兩個對象。
SqlSessionTemplate對象實現了SqlSession接口。
到這里你應該知道了,在Spring環境下使用的SqlSession對象實際是SqlSessionTemplate對象。
接下來查看SqlSessionTemplate是如何保證線程安全的。
在Spring環境下使用的SqlSessionTemplate對象。
public class SqlSessionTemplate implements SqlSession, DisposableBean { private final SqlSession sqlSessionProxy; // ... public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { this.sqlSessionFactory = sqlSessionFactory; this.executorType = executorType; this.exceptionTranslator = exceptionTranslator; // 實際的執行是InvocationHandler#invoke方法 this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[] { SqlSession.class }, new SqlSessionInterceptor()); } // 這里隨便列出一個方法 // 實現的SqlSession接口中的所有方法,實際都是有一個Proxy代理對象執行的 // 該代理對象在構造方法中被創建 public <T> T selectOne(String statement) { return this.sqlSessionProxy.selectOne(statement); } private class SqlSessionInterceptor implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 重點是這里的getSqlSession方法了 // 該方法是調用SqlSessionUtils#getSqlSession(這里靜態導入) SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator); try { Object result = method.invoke(sqlSession, args); // ... return result; } } }}
SqlSessionUtils#getSqlSession方法
public final class SqlSessionUtils { public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { // ... // 重點來了,先從同步事物管理器TransactionSynchronizationManager // 中通過sessionFactory為key獲取SqlSessionHolder對象 SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); // 如果存在執行返回(保證同一個線程使用同一個SqlSession對象) SqlSession session = sessionHolder(executorType, holder); if (session != null) { return session; } // 通過SqlSessionFactory對象獲取SqlSession對象 session = sessionFactory.openSession(executorType); // 將獲取的SqlSession對象保存到ThreadLocal中 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) { // 創建SqlSessionHolder對象,將創建的SqlSession對象保存 holder = new SqlSessionHolder(session, executorType, exceptionTranslator); // 將當前的SqlSessionHolder對象綁定到ThreadLocal中 TransactionSynchronizationManager.bindResource(sessionFactory, holder); // 注冊事務回調事件 TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory)); // 將資源標記為與事務同步。 holder.setSynchronizedWithTransaction(true); holder.requested(); } else { // ... } } // ... }}public abstract class TransactionSynchronizationManager { 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); Map<Object, Object> map = resources.get(); // set ThreadLocal Map if none found if (map == null) { map = new HashMap<>(); resources.set(map); } Object oldValue = map.put(actualKey, value); // Transparently suppress a ResourceHolder that was marked as void... if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) { oldValue = null; } // ... } public static Object getResource(Object key) { Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); Object value = doGetResource(actualKey); return value; } @Nullable private static Object doGetResource(Object actualKey) { // 從當前的ThreadLocal中獲取對象 Map<Object, Object> map = resources.get(); if (map == null) { return null; } 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; }}
通過上面的源碼分析清楚的知道,在Spring中SqlSession的線程安全是通過ThreadLocal來保證的,通過Spring提供的事務通過管理器來保存SqlSession對象,這樣就使得同一個線程獲取的是同一個SqlSession。
在事務管理方法在Spring環境下使用的是SpringManagedTransactionFactory事務管理器工廠
public class SpringManagedTransactionFactory implements TransactionFactory { @Override public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) { return new SpringManagedTransaction(dataSource); } @Override public Transaction newTransaction(Connection conn) { throw new UnsupportedOperationException("New Spring transactions require a DataSource"); } @Override public void setProperties(Properties props) { } }
事務對象
public class SpringManagedTransaction implements Transaction { private static final Logger LOGGER = LoggerFactory.getLogger(SpringManagedTransaction.class); private final DataSource dataSource; private Connection connection; private boolean isConnectionTransactional; private boolean autoCommit; public SpringManagedTransaction(DataSource dataSource) { notNull(dataSource, "No DataSource specified"); this.dataSource = dataSource; } @Override public Connection getConnection() throws SQLException { if (this.connection == null) { openConnection(); } return this.connection; } private void openConnection() throws SQLException { // 在Spring環境下,事務由Spring管理,所以這里先從Spring的ThreadLocal中獲取連接對象 this.connection = DataSourceUtils.getConnection(this.dataSource); this.autoCommit = this.connection.getAutoCommit(); this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource); } @Override public void commit() throws SQLException { if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) { this.connection.commit(); } } @Override public void rollback() throws SQLException { if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) { this.connection.rollback(); } } @Override public void close() throws SQLException { DataSourceUtils.releaseConnection(this.connection, this.dataSource); } @Override public Integer getTimeout() throws SQLException { ConnectionHolder holder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); if (holder != null && holder.hasTimeout()) { return holder.getTimeToLiveInSeconds(); } return null; }}public abstract class DataSourceUtils { public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { try { return doGetConnection(dataSource); } } public static Connection doGetConnection(DataSource dataSource) throws SQLException { ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { conHolder.requested(); if (!conHolder.hasConnection()) { conHolder.setConnection(fetchConnection(dataSource)); } return conHolder.getConnection(); } Connection con = fetchConnection(dataSource); if (TransactionSynchronizationManager.isSynchronizationActive()) { try { 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); } } //...catch } return con; }}
如果SqlSession沒有被Spring管理(也就是事務是自行處理沒有用Spring的事務管理@Transactional)那么Spring會強制提交事務。如果沒有在Spring環境下,Mybatis事務是不會自動提交的(的看你openSession方法參數如何傳)。
本文鏈接:http://www.www897cc.com/showinfo-26-10419-0.html為什么說MyBatis默認的DefaultSqlSession是線程不安全?
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 一文帶你弄懂 CSS 布局知識
下一篇: 事務提交之后異步執行工具類封裝