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有所帮助的话,不妨点赞+收藏。当然如果觉得笔者文章还可以不妨加个关注~~~

相关推荐
懒羊羊不懒@2 分钟前
Java基础语法—最小单位、及注释
java·c语言·开发语言·数据结构·学习·算法
ss2736 分钟前
手写Spring第4弹: Spring框架进化论:15年技术变迁:从XML配置到响应式编程的演进之路
xml·java·开发语言·后端·spring
DokiDoki之父17 分钟前
MyBatis—增删查改操作
java·spring boot·mybatis
兩尛34 分钟前
Spring面试
java·spring·面试
舒一笑38 分钟前
🚀 PandaCoder 2.0.0 - ES DSL Monitor & SQL Monitor 震撼发布!
后端·ai编程·intellij idea
Java中文社群41 分钟前
服务器被攻击!原因竟然是他?真没想到...
java·后端
Full Stack Developme1 小时前
java.nio 包详解
java·python·nio
零千叶1 小时前
【面试】Java JVM 调优面试手册
java·开发语言·jvm
代码充电宝1 小时前
LeetCode 算法题【简单】290. 单词规律
java·算法·leetcode·职场和发展·哈希表
li3714908901 小时前
nginx报400bad request 请求头过大异常处理
java·运维·nginx