思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
在深入剖析Mybatis中一级缓存线程不安全的原因一文中我们曾提到, 对于Mybatis内部默认实现的SqlSession而言其存在线程相关的问题,并对出问题的原因进行深入分析。
同时,笔者也曾在从BeanFactory入手,讲透Spring整合Mybatis的底层原理一文中指出在Spring整合Mybatis框架时,MapperFactoryBean中的getObject方法会返回给Spring容器的SqlSession的实际类型其实为SqlSessionTemplate。
具体来看,在SqlSessionTemplate中会持有一个SqlSessionProxy对象。而这个SqlSessionProxy便是SqlSession动态代理对象。
当时你可能会觉得Spring这样做有点绕,想不清楚Spring为何会这样设计,有这样的疑惑也别慌,接下来我们就来扒一扒它为什么会这样设计!
前言
在深入剖析Mybatis中一级缓存线程不安全的原因一文中,我们对Mybatis中SqlSession的默认实现DefaultSqlSession的线程不安全性进行了分析。事实上,MyBatis中的DefaultSqlSession是非线程安全的主要因为它内部包含了一级缓存,而一级缓存默认一定程度上通常被认为是会话级别的。因此DefaultSqlSession会在其生命周期内维护一个SqlSession的本地缓存,用于存储已经查询过的数据对象。
进一步,在多线程环境下,多个线程同时操作同一个DefaultSqlSession实例时,其比如会导致如下一些问题的发生:
- 数据不一致: 如果一个线程在
DefaultSqlSession中查询了某个数据并将其缓存在一级缓存中,而另一个线程在同一个DefaultSqlSession中进行了更新或删除操作,就有可能导致数据不一致。 - 脏读: 一个线程在事务未提交的情况下查询了数据并缓存在一级缓存中,而另一个线程在同一个
DefaultSqlSession中进行了更新操作,这可能导致一个线程读取到了未提交的脏数据。
注:具体可参考 深入剖析Mybatis中一级缓存线程不安全的原因 中的例子。
为了解决Mybatis中SqlSession的线程不安全的问题SqlSessionTemplate对象应用而生。对于SqlSessionTemplate你可能会感到陌生,这主要是因为其本身就并属于Mybatis体系,所以感到陌生也是很正常的。
而SqlSessionTemplate其实是MyBatis-Spring模块提供的一个实现其主要用于服务于Spring与Mybatis的整合。接下来,我们便来看看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 的构造方法内部其主要做了如下操作:
-
参数校验及赋值:
- 使用
notNull方法检查传入的sqlSessionFactory和executorType参数是否为非空,如果为空,则抛出IllegalArgumentException异常。 - 如果
sqlSessionFactory和executorType不为空,将它们赋值给类的成员变量this.sqlSessionFactory和this.executorType。
- 使用
-
创建动态代理对象:
- 使用 Java 的动态代理创建一个代理对象,实现
SqlSession接口。 - 代理对象通过
SqlSessionInterceptor进行拦截,主要用于实现SqlSession接口中的方法调用,以及在方法调用前后执行一些额外的逻辑。
- 使用 Java 的动态代理创建一个代理对象,实现
-
赋值代理对象:
- 将创建的代理对象赋值给
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) {
// .. 省略异常处理逻辑
}
}
不难发现,SqlSessionInterceptor的invoke方法其主要做了如下几点核心操作:
- 通过
getSqlSession方法获取SqlSession对象 - 调用
SqlSession的接口方法操作数据库获取结果,并完成结果集的返回
知晓了SqlSessionInterceptor的invoke方法的核心操作后,我们接下来再看看其在获取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时大致完成了如下几步操作:
-
获取当前事务关联的
SqlSessionHolder。 首先,通过TransactionSynchronizationManager.getResource(sessionFactory)获取当前事务中与给定SqlSessionFactory相关联的SqlSessionHolder。此外,如果当前事务中存在SqlSessionHolder,则尝试从中获取SqlSession。 -
获取
SqlSession。 当成功SqlSessionHolder后便会尝试从SqlSessionHolder中获取SqlSession。由于其可能返回null或者已经存在的SqlSession。因此,如果无法从SqlSessionHolder中获取到SqlSession,就说明当前事务中还没有与给定SqlSessionFactory关联的SqlSession。 便通过sessionFactory.openSession(executorType)创建一个新的SqlSession,并设置指定的ExecutorType。 -
注册
SqlSessionHolder。**- 通过
registerSessionHolder方法将新创建的SqlSession注册到TransactionSynchronizationManager中。 - 这样,在同一个事务中的其他地方,就能够通过
TransactionSynchronizationManager.getResource(sessionFactory)获取到相同的SqlSessionHolder。
- 通过
-
返回
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的过程大致如下:
- 当调用
SqlSessionTemplate的方法时,它会首先尝试从当前线程的ThreadLocal中获取SqlSessionHolder对象。 - 如果存在
SqlSessionHolder,则从中获取SqlSession;如果不存在,说明当前线程还没有与SqlSessionFactory关联的SqlSession。 - 在这种情况下,
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有所帮助的话,不妨点赞+收藏。当然如果觉得笔者文章还可以不妨加个关注~~~