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

 更新时间:2025年05月22日 09:21:26   作者: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 事务对象 的生成。也就是说:

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

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

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

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

  • 一级缓存工作原理图:

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

  • Mybatis缓存模式图如下:

我们知道在和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 是 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();
    }
  }

总结

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

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

相关文章

  • springboot jpa之返回表中部分字段的处理详解

    springboot jpa之返回表中部分字段的处理详解

    这篇文章主要介绍了springboot jpa之返回表中部分字段的处理详解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • 详解spring security 配置多个AuthenticationProvider

    详解spring security 配置多个AuthenticationProvider

    这篇文章主要介绍了详解spring security 配置多个AuthenticationProvider ,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05
  • java线程池不同场景下使用示例经验总结

    java线程池不同场景下使用示例经验总结

    这篇文章主要为大家介绍了java线程池不同场景如何使用的示例源码及经验总结,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步
    2022-03-03
  • Java中保留两位小数的四种方法实现实例

    Java中保留两位小数的四种方法实现实例

    今天小编就为大家分享一篇关于Java中保留两位小数的四种方法实现实例,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-02-02
  • JSP代码实现 金字塔(倒置)示例

    JSP代码实现 金字塔(倒置)示例

    这篇文章主要介绍了JSP代码实现 金字塔(倒置)示例,需要的朋友可以参考下
    2014-02-02
  • 五个很实用的IDEA使用技巧分享

    五个很实用的IDEA使用技巧分享

    IntelliJ IDEA 是一款优秀的 Java 集成开发环境,它提供了许多强大的功能和快捷键,可以帮助开发者提高编码效率和质量,本文就在为你介绍博主常用的五个IntelliJ IDEA使用技巧,希望能够给你带来一些工作效率上的提升
    2023-10-10
  • Java设计模式之中介者模式(Mediator Pattern)简介

    Java设计模式之中介者模式(Mediator Pattern)简介

    这篇文章主要介绍了Java设计模式之中介者模式(Mediator Pattern),需要的朋友可以参考下
    2014-07-07
  • Java中如何自定义一个类加载器

    Java中如何自定义一个类加载器

    这篇文章主要介绍了Java中如何自定义一个类加载器,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-11-11
  • springboot整合多数据源配置方式

    springboot整合多数据源配置方式

    这篇文章主要介绍了springboot整合多数据源配置,多数据源整合springboot+mybatis使用分包方式整合,springboot+druid+mybatisplus使用注解整合,本文通过实例代码给大家介绍的非常详细,需要的朋友可以参考下
    2021-12-12
  • java开发时各类工具的使用规范

    java开发时各类工具的使用规范

    这篇文章主要介绍了java编码时各类工具的使用规范,多人协作、共同开发一个项目,如果没有统一的代码规范的话,项目中的每个人都按照自己的习惯率性而为,就会导致整个项目的代码看上去杂乱无章,可读性非常差,并且持续增加后续的维护成本。对此感兴趣可以来了解一下
    2020-07-07

最新评论