深入理解MyBatis(七)—MyBatis事务

来源:互联网 发布:php log打印到日志 编辑:程序博客网 时间:2024/06/09 23:20

深入理解MyBatis(七)—MyBatis事务

MyBatis可以通过XML配置文件设定是否进行事务管理, 事务管理主要包括事务的提交,回滚等;

本文主要介绍了事务的入口,MyBatis事务操作对数据库SELECT操作和UPDATE操作的影响等;

个人主页:tuzhenyu’s page
原文地址:深入理解MyBatis(七)—MyBatis事务

(0) MyBatis事务遇到的问题

  • 如果开启MyBatis事务管理,则需要手动进行事务提交,否则事务会回滚到原状态;
String resource = "mybatis/config.xml";InputStream is = Main.class.getClassLoader().getResourceAsStream(resource);SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(is);SqlSession session = sessionFactory.openSession();User user = new User();user.setName("liuliu");user.setPassword("123123");user.setScore("88");String statement = "mybatis.mapping.UserMapper.insertUser";session.insert(statement,user);session.close();
  • 如果在具体操作执行完后不通过sqlSession.commit()方法提交事务,事务在sqlSession关闭时会自动回滚到原状态;只有执行了commit()事务提交方法才会真正完成操作;

    • 如果不执行sqlSession.commit()操作,直接执行sqlSession.close(),则会在close()中进行事务回滚;

    • 如果不执行sqlSession.commit()操作也不手动关闭sqlSession,在程序结束时关闭数据库连接时会进行事务回滚;

String resource = "mybatis/config.xml";InputStream is = Main.class.getClassLoader().getResourceAsStream(resource);SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(is);SqlSession session = sessionFactory.openSession();User user = new User();user.setName("liuliu");user.setPassword("123123");user.setScore("88");String statement = "mybatis.mapping.UserMapper.insertUser";session.insert(statement,user);session.commit();session.close();

(1) 事务管理入口

  • 在XML配置文件中定义事务工厂类型,JDBC或者MANAGED分别对应JdbcTransactionFactory.class和ManagedTransactionFactory.class;

    • 如果type=”JDBC”则使用JdbcTransactionFactory事务工厂则MyBatis独立管理事务,直接使用JDK提供的JDBC来管理事务的各个环节:提交、回滚、关闭等操作;

    • 如果type=”MANAGED”则使用ManagedTransactionFactory事务工厂则MyBatis不在ORM层管理事务而是将事务管理托付给其他框架,如Spring;

<environments default="development">    <environment id="development">        <transactionManager type="JDBC" />    //相当于<transactionManager type="JdbcTransactionFactory.class" />        <dataSource type="POOLED">            <property name="driver" value="com.mysql.jdbc.Driver" />            <property name="url" value="jdbc:mysql://localhost:3306/test" />            <property name="username" value="root" />            <property name="password" value="BIUBIU" />        </dataSource>    </environment></environments>
  • 在从SessionFactory中获取sqlSession时候会根据environment配置获取相应的事务工厂TransactionFactory,并从中获取事务实例当做参数传递给Executor,用来从中获取Connection数据库连接;
public SqlSession openSession() {  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);}
  • getTransactionFactoryFromEnvironment()方法根据XML配置获取JdbcTransactionFactory或者ManagedTransactionFactory;ManagedTransactionFactory类中的commit()方法和rollback()方法都为空,事务相关操作不发挥作用;
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);    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();  }}

(2) 事务对UPDATE操作的影响

事务的提交

  • 在sqlSession中执行了UPDATE操作,需要执行sqlSession.commit()方法提交事务,不然在连接关闭时候会自动回滚;
@Overridepublic void commit() {  commit(false);}@Overridepublic void commit(boolean force) {  try {    executor.commit(isCommitOrRollbackRequired(force));    dirty = false;  } catch (Exception e) {    throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);  } finally {    ErrorContext.instance().reset();  }}
  • exector.commit()事务提交方法归根到底是调用了transaction.commit()事务的提交方法;这里的transaction就是根据配置对应的JdbcTransaction或者ManagedTransaction;
public void commit(boolean required) throws SQLException {  if (closed) {    throw new ExecutorException("Cannot commit, transaction is already closed");  }  clearLocalCache();  flushStatements();  if (required) {    transaction.commit();  }}
  • 如果是JdbcTransaction的commit()方法,通过调用connection.commit()方法通过数据库连接实现事务提交;
public void commit() throws SQLException {  if (connection != null && !connection.getAutoCommit()) {    if (log.isDebugEnabled()) {      log.debug("Committing JDBC Connection [" + connection + "]");    }    connection.commit();  }}
  • 如果是ManagedTransaction的commit()方法,则为空方法不进行任何操作;
@Overridepublic void commit() throws SQLException {  // Does nothing}@Overridepublic void rollback() throws SQLException {  // Does nothing}

事务的回滚

sqlSession执行close()关闭操作时,如果close()操作之前进行了UPDATE操作未进行commit()事务提交则会进行事务回滚然后再关闭会话;如果update后执行了commit则直接关闭会话;

  • 在DefaultSqlSession类中如果执行了UPDATE操作则会将标志位dirty赋值为true
@Overridepublic int update(String statement, Object parameter) {  try {    dirty = true;    MappedStatement ms = configuration.getMappedStatement(statement);    return executor.update(ms, wrapCollection(parameter));  } catch (Exception e) {    throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);  } finally {    ErrorContext.instance().reset();  }}
  • 在事务提交时会将dirty赋值为false;
@Overridepublic void commit(boolean force) {  try {    executor.commit(isCommitOrRollbackRequired(force));    dirty = false;  } catch (Exception e) {    throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);  } finally {    ErrorContext.instance().reset();  }}
  • 在关闭会话时会判断dirty是否为true,如果为true则需要进行事务回滚操作,否则直接关闭会话
@Overridepublic void close() {  try {    executor.close(isCommitOrRollbackRequired(false));    closeCursors();    dirty = false;  } finally {    ErrorContext.instance().reset();  }}
  • 判断dirty标志是否为true
private boolean isCommitOrRollbackRequired(boolean force) {  return (!autoCommit && dirty) || force;}
  • 如果dirty为true则判定forceRollback为true,执行回滚操作;
@Overridepublic void close(boolean forceRollback) {  try {    try {      rollback(forceRollback);    } finally {      if (transaction != null) {        transaction.close();      }    }  } catch (SQLException e) {    // Ignore.  There's nothing that can be done at this point.    log.warn("Unexpected exception on closing transaction.  Cause: " + e);  } finally {    transaction = null;    deferredLoads = null;    localCache = null;    localOutputParameterCache = null;    closed = true;  }}

(3) 事务对SELECT的影响

事务对select操作的影响主要体现在对缓存的影响上,主要包括一级缓存和二级缓存

一级缓存

  • 因为一级缓存是Session级别的,事务的提交回滚对MyBatis的一级缓存没有影响;一级缓存放在BaseExector中的PerpectualCache类型的localCache中;
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {  ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());  if (closed) {    throw new ExecutorException("Executor was closed.");  }  if (queryStack == 0 && ms.isFlushCacheRequired()) {    clearLocalCache();  }  List<E> list;  try {    queryStack++;    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--;  }  if (queryStack == 0) {    for (DeferredLoad deferredLoad : deferredLoads) {      deferredLoad.load();    }    // issue #601    deferredLoads.clear();    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {      // issue #482      clearLocalCache();    }  }  return list;}

二级缓存

  • 在Mapper.xml文件解析时会根据文件中的标签或者创建Cache实例,并将该实例放入每一个MappedStatement中,在MappedStatement执行select操作时候会获取该cache;
@Overridepublic <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)    throws SQLException {  Cache cache = ms.getCache();  if (cache != null) {    flushCacheIfRequired(ms);    if (ms.isUseCache() && resultHandler == null) {      ensureNoOutParams(ms, parameterObject, boundSql);      @SuppressWarnings("unchecked")      List<E> list = (List<E>) tcm.getObject(cache, key);      if (list == null) {        list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);        tcm.putObject(cache, key, list); // issue #578 and #116      }      return list;    }  }  return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}

查询二级缓存

  • 根据Statement所在的Mapper的cache缓存对象和根据statement生成的cacheKey,从正式缓存中取缓存数据
List<E> list = (List<E>) tcm.getObject(cache, key);
  • 根据Statement所在的Mapper的cache缓存对象在TransactionManager中定位对应的TransactionCache,TransactionCache中保存这正式缓存delegate和临时未提交缓存entiryToAddOnCommit;
public Object getObject(Cache cache, CacheKey key) {  return getTransactionalCache(cache).getObject(key);}
  • 获取缓存时直接从正式缓存delegate中查询
@Overridepublic Object getObject(Object key) {  // issue #116  Object object = delegate.getObject(key);  if (object == null) {    entriesMissedInCache.add(key);  }  // issue #146  if (clearOnCommit) {    return null;  } else {    return object;  }}

存储二级缓存

如果从二级缓存中未命中缓存,则需要从数据库中查取,再将查询结果放入二级缓存中;查询结果首先放入二级缓存临时缓存中,只有执行了commit()事务提交才正式转移到正式缓存中;也就是说只有执行了commit()方法的缓存才被下次查询使用,不然仍会执行数据库查询任务并覆盖上次的临时缓存;

  • 根据Statement所在的Mapper的cache缓存对象在TransactionManager中定位对应的TransactionCache
public void putObject(Cache cache, CacheKey key, Object value) {  getTransactionalCache(cache).putObject(key, value);}
  • 将数据放入TransactionCache中的临时缓存entiryToAddOnCommit中
public void putObject(Object key, Object object) {  entriesToAddOnCommit.put(key, object);}

提交二级缓存

  • 执行事务提交时候会调用TransactionCacheManager内的commit()方法提交缓存,每一个cachingExecutor对应一个TransactionCacheManager,也就是一个SqlSeesion对应一个TransactionCacheManager,因此sqlSession.commit()是提交了当前会话的所以二级缓存的临时缓存;每个sqlSession对每一个Mapper的cache都有一个临时缓存,多个sqlSession共享一个Mapper的cache的正式缓存;
public void commit() {  for (TransactionalCache txCache : transactionalCaches.values()) {    txCache.commit();  }}
public void commit() {  if (clearOnCommit) {    delegate.clear();  }  flushPendingEntries();  reset();}
  • 将临时缓存entiryToAddOnCommit中的数据转移到正式缓存delegate中
private void flushPendingEntries() {  for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {    delegate.putObject(entry.getKey(), entry.getValue());  }  for (Object entry : entriesMissedInCache) {    if (!entriesToAddOnCommit.containsKey(entry)) {      delegate.putObject(entry, null);    }  }}

回滚二级缓存

  • 如果之前未执行UPDATE操作,dirty标志为false,在关闭会话之前没有进行事务提交则进行数据库回滚和缓存提交;

  • 如果执行了UPDATE操作,dirty标志为true,在关闭会话之前没有进行事务提交则进行数据库回滚和缓存回滚;

  • 可以调用session.close(true)关闭会话时强行回滚缓存;

@Overridepublic void close(boolean forceRollback) {  try {    if (forceRollback) {       tcm.rollback();    } else {      tcm.commit();    }  } finally {    delegate.close(forceRollback);  }}

总结

  • MyBatis可以通过XML配置是否独立处理事务,可以选择不单独处理事务,将事务托管给其他上层框架如spring等;

  • 如果MyBatis选择处理事务则事务会对数据库操作产生影响

    • 对UPDATE操作的影响主要表现在UPDATE操作后如果没有进行事务提交则会子啊会话关闭时进行数据库回滚;

    • 对SELECT操作的影响主要表现在二级缓存上,执行SELECT操作后如果未进行事务提交则缓存会被放在临时缓存中,后续的SELECT操作无法使用该缓存,直到进行commit()事务提交,临时缓存中的数据才会被转移到正式缓存中;