Spring整合Mybatis时如何保证SqlSession的安全性


思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜


深入剖析Mybatis中一级缓存线程不安全的原因一文中我们曾提到, 对于Mybatis内部默认实现的SqlSession而言其存在线程相关的问题,并对出问题的原因进行深入分析。

同时,笔者也曾在从BeanFactory入手,讲透Spring整合Mybatis的底层原理一文中指出Spring整合Mybatis框架时,MapperFactoryBean中的getObject方法会返回给Spring容器的SqlSession的实际类型其实为SqlSessionTemplate

具体来看,在SqlSessionTemplate中会持有一个SqlSessionProxy对象。而这个SqlSessionProxy便是SqlSession动态代理对象。

当时你可能会觉得Spring这样做有点绕,想不清楚Spring为何会这样设计,有这样的疑惑也别慌,接下来我们就来扒一扒它为什么会这样设计!


前言

深入剖析Mybatis中一级缓存线程不安全的原因一文中,我们对MybatisSqlSession的默认实现DefaultSqlSession的线程不安全性进行了分析。事实上,MyBatis中的DefaultSqlSession是非线程安全的主要因为它内部包含了一级缓存,而一级缓存默认一定程度上通常被认为是会话级别的。因此DefaultSqlSession会在其生命周期内维护一个SqlSession的本地缓存,用于存储已经查询过的数据对象。

进一步,在多线程环境下,多个线程同时操作同一个DefaultSqlSession实例时,其比如会导致如下一些问题的发生:

  1. 数据不一致: 如果一个线程在DefaultSqlSession中查询了某个数据并将其缓存在一级缓存中,而另一个线程在同一个DefaultSqlSession中进行了更新或删除操作,就有可能导致数据不一致。
  2. 脏读: 一个线程在事务未提交的情况下查询了数据并缓存在一级缓存中,而另一个线程在同一个DefaultSqlSession中进行了更新操作,这可能导致一个线程读取到了未提交的脏数据。

注:具体可参考 深入剖析Mybatis中一级缓存线程不安全的原因 中的例子。

为了解决MybatisSqlSession的线程不安全的问题SqlSessionTemplate对象应用而生。对于SqlSessionTemplate你可能会感到陌生,这主要是因为其本身就并属于Mybatis体系,所以感到陌生也是很正常的。

SqlSessionTemplate其实是MyBatis-Spring模块提供的一个实现其主要用于服务于SpringMybatis的整合。接下来,我们便来看看SqlSessionTemplate内部看看其在保证线程安全方面到底做了哪些工作。


SqlSessionTemplate的秘密

对于SqlSessionTemplate这样一个陌生的类,你可能会疑惑,究竟该从何处入手去阅读源码。这里笔者笔者教你一招,即当我们阅读接触一个完全陌生类的时候,不妨先去关注其构造方法,先知晓类是如何构造出来,然后再去分析其功能 。这样做的主要原因在于:对于所有的代码而言,其在编写时必然遵循先声明后使用的原则!

为此,我们首先先来看看SqlSessionTemplate构造方法内部究竟做了哪些准备工作:

java 复制代码
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
    PersistenceExceptionTranslator exceptionTranslator) {
  <1> 参数校验
  notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
  notNull(executorType, "Property 'executorType' is required");
  <2> 相关成员变量设定
  this.sqlSessionFactory = sqlSessionFactory;
  this.executorType = executorType;
  this.exceptionTranslator = exceptionTranslator;
  this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
      new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}

可以看到在SqlSessionTemplate 的构造方法内部其主要做了如下操作:

  1. 参数校验及赋值:

    • 使用 notNull 方法检查传入的 sqlSessionFactoryexecutorType 参数是否为非空,如果为空,则抛出 IllegalArgumentException 异常。
    • 如果 sqlSessionFactoryexecutorType 不为空,将它们赋值给类的成员变量 this.sqlSessionFactorythis.executorType
  2. 创建动态代理对象:

    • 使用 Java 的动态代理创建一个代理对象,实现 SqlSession 接口。
    • 代理对象通过 SqlSessionInterceptor 进行拦截,主要用于实现 SqlSession 接口中的方法调用,以及在方法调用前后执行一些额外的逻辑。
  3. 赋值代理对象:

    • 将创建的代理对象赋值给 this.sqlSessionProxy,该对象即为 SqlSessionTemplate 类的实例变量,它实际上是一个实现了 SqlSession 接口的动态代理对象。

总体来看,上述SqlSessionTemplate的构造方法主要做了一些实例构造前的初始化工作。其中包括参数校验、异常转换器的初始化以及动态代理对象的创建等。

虽然SqlSessionTemplate在构造的过程中做了这多工作,但你觉得哪项工作最关键呢?我想答案无疑是代理对象构建 。原因也很简单:其他操作无疑就是参数的一些赋值类操作,只有该操作又涉及到其他对象的构建!


SqlSessionProxy的构建

所以,接下来我们便来看看在构建SqlSessionProxy时其内部又会完成哪些工作,因此其实分析的重点又在于 (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(), new Class[] { SqlSession.class }, new SqlSessionInterceptor())这行代码。

看到newProxyInstance方法,如果你Java基础扎实的话,其实会很容易想到到Jdk中动态代理。此处,你可能会疑惑平时我们用的不都是Proxy.newProxyInstance()的使用吗?怎么此处直接使用newProxyInstance就可以了?至于原因笔者便就不在此过多赘述了,欢迎在评论区留言~

既然涉及到动态代理,那动态代理的核心在哪呢?那肯定InvocationHandler实现类中的Invoke方法。所以,接下来我们便来看看SqlSessionInterceptor究竟藏了哪些秘密。其代码如下:

java 复制代码
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //获取SqlSession(这个sqlSession才是真正使用的,它不是线程安全的) 
 
    SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
        SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
    try {
    //调用sqlSession对象的方法(select、update等)
      Object result = method.invoke(sqlSession, args);
      if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
    
        sqlSession.commit(true);
      }
      return result;
    } catch (Throwable t) {
     // .. 省略异常处理逻辑
  }
}

不难发现,SqlSessionInterceptorinvoke方法其主要做了如下几点核心操作:

  1. 通过getSqlSession方法获取SqlSession对象
  2. 调用SqlSession的接口方法操作数据库获取结果,并完成结果集的返回

知晓了SqlSessionInterceptorinvoke方法的核心操作后,我们接下来再看看其在获取SqlSession时与我们之前的操作到底有何不同。其内部逻辑如下所示:

java 复制代码
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {

 
    //SqlSessionHolder是SqlSession的包装类
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    
    //从ThrealLocal上下文中获取则通过SqlSessionFactory获取SqlSession
    session = sessionFactory.openSession(executorType);
     //注册SqlSessionHolder到ThrealLocal中
    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

可以看到其在获取SqlSession时大致完成了如下几步操作:

  1. 获取当前事务关联的 SqlSessionHolder。 首先,通过 TransactionSynchronizationManager.getResource(sessionFactory) 获取当前事务中与给定 SqlSessionFactory 相关联的 SqlSessionHolder。此外,如果当前事务中存在 SqlSessionHolder,则尝试从中获取 SqlSession

  2. 获取 SqlSession。 当成功SqlSessionHolder后便会尝试从 SqlSessionHolder 中获取 SqlSession。由于其可能返回 null 或者已经存在的 SqlSession。因此,如果无法从 SqlSessionHolder 中获取到 SqlSession,就说明当前事务中还没有与给定 SqlSessionFactory 关联的 SqlSession。 便通过 sessionFactory.openSession(executorType) 创建一个新的 SqlSession,并设置指定的 ExecutorType

  3. 注册 SqlSessionHolder。**

    • 通过 registerSessionHolder 方法将新创建的 SqlSession 注册到 TransactionSynchronizationManager 中。
    • 这样,在同一个事务中的其他地方,就能够通过 TransactionSynchronizationManager.getResource(sessionFactory) 获取到相同的 SqlSessionHolder
  4. 返回 SqlSession: 将获取到的 SqlSession 返回给调用方。

我们重点来看下将SqlSessionHolder注册到ThrealLocal的逻辑,也即registerSessionHolder的主要逻辑。

java 复制代码
private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,
    PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
  SqlSessionHolder holder;
  // <1> 检查当前是否存在事务同步活动
  if (TransactionSynchronizationManager.isSynchronizationActive()) {
    // <2> 获取 SqlSessionFactory 的配置信息中的 Environment
    Environment environment = sessionFactory.getConfiguration().getEnvironment();
   // <3> 检查该 Environment 是否使用了 SpringManagedTransactionFactory 作为事务工厂
    if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
      // <4> 创建一个 `SqlSessionHolder` 对象,用于包装 SqlSession、执行器类型(executorType)、异常转换器(exceptionTranslator)等信息。
  
      holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
      
    // <5 > 调用 `TransactionSynchronizationManager.bindResource()` 方法将 SqlSessionHolder 绑定到当前事务中。
      TransactionSynchronizationManager.bindResource(sessionFactory, holder);
      TransactionSynchronizationManager
          .registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
      holder.setSynchronizedWithTransaction(true);
      holder.requested();
    } 
   
   // .. 省略其他无关代码

}

不难发现,在 registerSessionHolder中会调用TransactionSynchronizationManager中的bindResource方法完成SqlSession的绑定。具体来看,其会通过一个ThreadLocal<Map<Object, Object>>结构来保存线程对应的SqlSession,进而实现实现Sqlsession的线程安全。

说了这么多,让我们做一个简单的回顾从而将内容串起来。当使用 SqlSessionTemplate 后获取SqlSession的过程大致如下:

  1. 当调用 SqlSessionTemplate 的方法时,它会首先尝试从当前线程的 ThreadLocal 中获取 SqlSessionHolder 对象。
  2. 如果存在 SqlSessionHolder,则从中获取 SqlSession;如果不存在,说明当前线程还没有与 SqlSessionFactory 关联的 SqlSession
  3. 在这种情况下,SqlSessionTemplate 会通过 sqlSessionFactory.openSession(...) 创建一个新的 SqlSession

进一步,SqlSessionTemplate 中具体来通过 TransactionSynchronizationManager 来进行 SqlSession 的管理。在一个事务内,SqlSessionTemplate 会将 SqlSessionHolder 绑定到 TransactionSynchronizationManager 中,而 SqlSessionHolder 中包含了实际的 SqlSession 对象。因此,每次调用到sqlSessionProxy的方法时,都会从Spring事务管理器中获取SqlSession,并最终调用从Spring事务管理器中获取到的SqlSession对象,进而调用其内部对应方法,因为使用到了ThreadLocal进行绑定,这样就没每个线程分别构建了一个不同的SqlSession对象,也就避免了SqlSession共用所带来的问题!


总结

总体而言,SqlSessionTemplate 通过结合 ThreadLocal、事务管理和异常处理等有效确保了在 MyBatis 操作中能够获取到线程安全的 SqlSession 实例,同时也保证了在事务结束后正确关闭和处理 SqlSession

最后,如果觉得文章对你理解Mybatis有所帮助的话,不妨点赞+收藏。当然如果觉得笔者文章还可以不妨加个关注~~~

相关推荐
陈大爷(有低保)14 分钟前
UDP Socket聊天室(Java)
java·网络协议·udp
kinlon.liu28 分钟前
零信任安全架构--持续验证
java·安全·安全架构·mfa·持续验证
王哲晓1 小时前
Linux通过yum安装Docker
java·linux·docker
java6666688881 小时前
如何在Java中实现高效的对象映射:Dozer与MapStruct的比较与优化
java·开发语言
Violet永存1 小时前
源码分析:LinkedList
java·开发语言
执键行天涯1 小时前
【经验帖】JAVA中同方法,两次调用Mybatis,一次更新,一次查询,同一事务,第一次修改对第二次的可见性如何
java·数据库·mybatis
Adolf_19931 小时前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
Jarlen1 小时前
将本地离线Jar包上传到Maven远程私库上,供项目编译使用
java·maven·jar
蓑 羽1 小时前
力扣438 找到字符串中所有字母异位词 Java版本
java·算法·leetcode
叫我:松哥1 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap