思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
在深入剖析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
有所帮助的话,不妨点赞+收藏。当然如果觉得笔者
文章还可以不妨加个关注~~~