java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Mybatis的SqlSession和一级缓存的失效原因

Mybatis的SqlSession和一级缓存的失效原因分析及解决

作者:Linn-cn

这篇文章主要介绍了Mybatis的SqlSession和一级缓存的失效原因分析及解决,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

SqlSession解读

SqlSession是什么?

SqlSession是Mybatis 中定义的,用来表示与关系数据库的一次会话,会话定义了各种具体的操作,查询、数据更新(包含保存、更新、删除)操作。而这些操作都在与数据库建立会话的基础上进行的。

SqlSession 可以看作是对Connection 更加高级的抽象,从其方法上更加可以看出他具有更加明显的操作特征。

SqlSession分类

mybatis的SqlSession有三种:

DefaultSqlSession、SqlSessionManager、SqlSessionTemplate,前两者是mybtais默认情况下使用的,第三种主要用到mybatis和spring整合的时候。

SqlSession的创建

那么SqlSession 是如何被创建的?

在学习Mybatis时,我们常常看到的 SqlSession 创建方式是 SqlSessionFactory.openSession() ,那么我们就从它作为切入点,先来看看 SqlSessionFactory.openSession() 的方法源码(需要注意的是这里是实现类DefaultSqlSessionFactory )

代码如下:

  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
      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();
    }
  }

通过源码我们知道每次 SqlSession(准确地说是 DefaultSqlSession )的创建都会有一个 Transaction 事务对象 的生成。也就是说:

但需要注意的是,我们整合Spring之后用到的其实都是 SqlSessionTemplate ,与这里的 DefaultSqlSession 不是同一个SqlSession对象,不懂的看上面的。

为什么和Spring整合后的SqlSession一级缓存偶尔会失效?

我们都知道mybatis有一级缓存和二级缓存。一级缓存是SqlSession级别的缓存,在操作数据库时,每个SqlSession类的实例对象缓存的数据区域(Map)可以用于存储缓存数据,不同的SqlSession类的实例对象缓存的数据区域是互不影响的。 

二级缓存是Mapper级别的缓存,多个SqlSession实例对象可以共用二级缓存,二级缓存是跨SqlSession的。 

我们知道在和Mybatis和Spring的整合中不管是创建MapperProxy 的 SqlSession 还是 MapperMethod中调用的SqlSession其实都是** SqlSessionTemplate **。

SqlSessionTemplate的神秘面纱

如果你阅读了上面的链接文章,就知道 每创建一个 MapperFactoryBean 就会创建一个 SqlSessionTemplate 对象,而 MapperFactoryBean 在获取 MapperProxy 时会将 SqlSessionTemplate 传递到 MapperProxy中。 也就是说 SqlSessionTemplate 的生命周期是与 MapperProxy 的生命周期是一致的。

SqlSessionTemplate 内部维护了一个 sqlSessionProxy ,而 sqlSessionProxy 是通过动态代理创建的一个 SqlSession 对象, SqlSessionTemplate 的 数据库操作方法 insert/update 等等都是委托 sqlSessionProxy 来执行的,我们看一下它的构造方法:

// 构造方法
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
                          PersistenceExceptionTranslator exceptionTranslator) {
    ...省略无关紧要的代码
        this.sqlSessionProxy = (SqlSession) newProxyInstance(
        SqlSessionFactory.class.getClassLoader(),
        new Class[] { SqlSession.class },
        new SqlSessionInterceptor());
}

我们会发现这个类也继承了SqlSession接口,我们选择一个查询方法来深入看一下为什么mybatis一级缓存偶尔会失效,我们进入到他的selectList方法,看下他的实现逻辑:

public class SqlSessionTemplate implements SqlSession, DisposableBean {...}

@Override
public <E> List<E> selectList(String statement, Object parameter) {
    return this.sqlSessionProxy.selectList(statement, parameter);
}

我们发现,这个方法内部内部的查询又交给了一层代理,由这一层代理去真正执行的查询操作,而这个代理就是在SqlSessionTemplate创建的时候进行设置的。

如果熟悉动态代理的话,就知道,我们接下来需要看的就是SqlSessionInterceptor,我们进入到里面看一下他的实现:

private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // 去获取SqlSession
      SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
      try {
        // 通过反射调用真正的处理方法
        Object result = method.invoke(sqlSession, args);
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // 提交数据
          sqlSession.commit(true);
        }
        // 返回查询的数据
        return result;
      } catch (Throwable t) {
        ...省略无关紧要的代码
      } finally {
        if (sqlSession != null) {
          // 关闭SqlSession的连接
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

整个 invoke 分5个步骤:

我们都知道一级缓存是SqlSession级别的缓存,那么一级缓存失效,肯定是因为SqlSession不一致,那么我们进入到getSqlSession方法中:

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {
    ...省略无关紧要的代码
    // 从ThreadLocal变量里面获取到Spring的事务同步管理器
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
    // 调用静态方法sessionHoler 判断是否存在符合要求的sqlSession
    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }
    // 如果SqlSessionHolder中获取的SqlSession为空,则新建一个SqlSession
    session = sessionFactory.openSession(executorType);
    // 判断当前是否存在事务,将sqlSession 绑定到sqlSessionHolder 中,并放到threadLoacl 当中
    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
    return session;
  }

看到这,我们应该知道为什么Spring和MyBatis整合后,偶尔会一级缓存失效了,是因为Spring只有在开启了事务之后,在同一个事务里的SqlSession会被缓存起来,同一个事务中,多次查询是可以命中缓存的!

SqlSessionInterceptor#invoke方法里面,他在关闭的SqlSession的时候同样对是否开启事务做了处理,感兴趣的可以看closeSqlSession方法的源码:

public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
	...省略无关紧要的代码
    SqlSessionHolder holder = 
          (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
    // 查看事务同步管理器是否存在 session 
    if ((holder != null) && (holder.getSqlSession() == session)) {
      holder.released();
    } else {
      // 如果不存在就将该Session关闭掉
      session.close();
    }
  }

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文