谈谈Mybatis的SqlSession和一级缓存为什么失效?

如果你觉得内容对你有帮助的话,不如给个赞,鼓励一下更新😂。

本文如有改动,最新版本请移步:谈谈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 事务对象 的生成。也就是说:

  • 一个事务 Transaction 对象与一个 SqlSession 对象 是一一对应的关系。
  • 同一个SqlSession 不管执行多少次数据库操作。只要没有执行close,那么整个操作都是在同一个 Transaction 中执行的。

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

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

我们都知道mybatis有一级缓存和二级缓存。一级缓存是SqlSession级别的缓存,在操作数据库时,每个SqlSession类的实例对象缓存的数据区域(Map)可以用于存储缓存数据,不同的SqlSession类的实例对象缓存的数据区域是互不影响的。
一级缓存工作原理图:
image.png
二级缓存是Mapper级别的缓存,多个SqlSession实例对象可以共用二级缓存,二级缓存是跨SqlSession的。
Mybatis缓存模式图如下:
image.png
我们知道在和Mybatis和Spring的整合中不管是创建MapperProxy 的 SqlSession 还是 MapperMethod中调用的SqlSession其实都是** SqlSessionTemplate **。
如果不知道SqlSessionTemplate是什么时候创建的,那么可以看我这篇文章,里面有详细说话:源码解读 | Mybatis和Spring是怎么整合的

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 是 DefaultSqlSession ),此时的SqlSession 可能是新创建的,也可能是上一次的请求的SqlSession。
  • 反射执行 SqlSession 方法
  • 判断当前的 SqlSession 是否由事务所管控,如果是则不commit
  • 判断如果是PersistenceExceptionTranslator且不为空,那么就关闭当前会话,并且将sqlSession置为空防止finally重复关闭
  • 只要当前会话不为空, 那么就会关闭当前会话操作,关闭当前会话操作又会根据当前会话是否有事务来决定会话是释放还是直接关闭。

我们都知道一级缓存是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();
    }
  }

总结

  1. 同一事务中不管调用多少次 mapper里的方法 ,最终都是用得同一个sqlSession,即一个事务中使用的是同一个sqlSession。
  2. 同一事务中,Mybatis的一级缓存才会有效。
  3. 如果没有开启事务,调用一次mapper里的方法将会新建一个sqlSession来执行方法。

这篇文章到这就结束啦。
短句:人所缺乏的不是才干而是志向,不是成功的能力而是勤劳的意志。表扬一下自己坚持看完了这篇文章吧!
喜欢的话就给个赞 + 收藏 + 关注吧!🤓

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Linn-cn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值