上一期,分析了下关于mybatis的处理sqlsession线程安全的问题,主要是通过sqlSessionManager代理类增强的形式,通过每次创建一个新的DefautSqlsession或者将当前线程放入到Threadlocal中实现的,那么我们在使用mybatis的时候,一般不可能单独使用mybatis的,一般都是和sprig框架配合使用,现在都是面向spring编程了,所以,本次我们一起分析下spring是怎样保证sqlSession线程安全的,
我们先通过一个案例,看看结果,一般在引入spring后,我们都会通过SqlSessionTemplete操作mybatis,当扫描到@Mapper的注解后,在执行业务逻辑代码,执行数据库交互的时候,这个时候,就会被SqlSessionTemplete拦截了,所以,不管是我们显示的调用SqlSessionTemplete执行相关的操作,还是在引入spring框架后,,默认的执行,都是通过SqlSessionTempete拦截保证sqlSqssion线程安全的,那马接下来,我们就一起分析下,看看spring到底是怎样支持sqlSession线程安全的
我们先通过一个案例来说明问题,下面是一个特备简单的案例,我们通过他,先来分析下,然后在通过多线程来分析具体的线程安全问题
@Test
public void test5(){
final RyxAccount ryxAccountByPrimaryKey = ryxAccountService.getRyxAccountByPrimaryKey(1);
System.out.println(ryxAccountByPrimaryKey);
final RyxAccount ryxAccountByPrimaryKey1 = ryxAccountService.getRyxAccountByPrimaryKey(1);
System.out.println(ryxAccountByPrimaryKey1);
}
当我们执行 ryxAccountService.getRyxAccountByPrimaryKey的接口方法的时候,到mapper接口,SqlSqlSessionTemplete通过代理的方式拦截mapper接口,生成代理类,获取sqlSession,执行后续的数据库crud
public class SqlSessionTemplate implements SqlSession, DisposableBean {
private final SqlSessionFactory sqlSessionFactory;
private final ExecutorType executorType;
private final SqlSession sqlSessionProxy;
private final PersistenceExceptionTranslator exceptionTranslator;
省略........
/**
* Constructs a Spring managed {@code SqlSession} with the given
* {@code SqlSessionFactory} and {@code ExecutorType}.
* A custom {@code SQLExceptionTranslator} can be provided as an
* argument so any {@code PersistenceException} thrown by MyBatis
* can be custom translated to a {@code RuntimeException}
* The {@code SQLExceptionTranslator} can also be null and thus no
* exception translation will be done and MyBatis exceptions will be
* thrown
*
* @param sqlSessionFactory a factory of SqlSession
* @param executorType an executor type on session
* @param exceptionTranslator a translator of exception
*/
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
//jdk动态代理方法
this.sqlSessionProxy = (SqlSession) newProxyInstance(
//获取类加载器
SqlSessionFactory.class.getClassLoader(),
//具体需要代理的类
new Class[] { SqlSession.class },
//执行增强的方法
new SqlSessionInterceptor());
}
省略......
/**
* Proxy needed to route MyBatis method calls to the proper SqlSession got
* from Spring's Transaction Manager
* It also unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to
* pass a {@code PersistenceException} to the {@code PersistenceExceptionTranslator}.
*/
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
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)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
}
看到这段代码是不是有似曾相识的感觉,没错,前一期在介绍SqlSesionManager的时候,也是这个写法,通过动态代理的技术增强目标方法,这里的作用就是在生成目标类之前,获取sqlSession(线程安全的),接下来我们吧重心放到SqlSessionInterceptor 的getSqlSession中去
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
//参数校验
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
/**
* 1.1
* 这个步骤都是指的是存在事务的情况下的结果,也就是说,在你的方法上开启了事务注解的时候,才有意义,当不开启事务注解的时候,
* 会直接调用openSession返回session,也就是说,不存在事务的情况下,每一个线程都是相当于开启一个新的session,也就不会存在一级缓存的问题
* 当然也就不会存在线程安全问题
* 调用事务同步管理器方法获取SqlSession回话持有者,回去ThreadLocal中去取会话持有者对象.会话持有者对象
* 放在ThreadLocal中.ThreadLocal
这节我们的重点是分析SqlSession.所以事物我们暂且放下,看懂了,代码比较简单.我这里在总结下
1:当我们引入spring框架后,我们执行查询,SqLSessionTemplete户通过代理方式"拦截"执行方法,由于DefaultSqSLession线程不安全
2:这里获取线程安全的SqlSession,
3:判断是否存在事务
4:事务不存在,直接调用SqlSessionFactory获取sesison返回,直接执行后续的方法,也就是说,不通的线程访问,或者同一个线程范根同样的查询方法,都会去创建一个新的session,这样的话,也就导致以及缓存不存在了.
如果存在事务,回去事务资源管理器获取session持有者,如果存在,就将当前的引用计数加1,如果不存在,则通过SqlSessionFactory.openSession获取一个session,然后注册到事务管理器中,这里是将当前资源添加到ThreadLOcal中,,在local中定义了一个map类似于这样的结构ThreadLocal
我们通过案例来加强一下理解,以下案例,查询两次,默认回去走SqLSessionTemplete,我们执行下,看看结果
@Test
public void test5(){
final RyxAccount ryxAccountByPrimaryKey = ryxAccountService.getRyxAccountByPrimaryKey(1);
System.out.println(ryxAccountByPrimaryKey);
final RyxAccount ryxAccountByPrimaryKey1 = ryxAccountService.getRyxAccountByPrimaryKey(1);
System.out.println(ryxAccountByPrimaryKey1);
}
我们看到结果中查询两次都走了数据库查询,一级缓存没有起作用
再来看另一个案例,加了注解后,第二次查询回去走缓存
*/
@Test
@Transactional
public void test5(){
final RyxAccount ryxAccountByPrimaryKey = ryxAccountService.getRyxAccountByPrimaryKey(1);
System.out.println(ryxAccountByPrimaryKey);
final RyxAccount ryxAccountByPrimaryKey1 = ryxAccountService.getRyxAccountByPrimaryKey(1);
System.out.println(ryxAccountByPrimaryKey1);
}
那就有一个问题,为什么mybatis不适用线程安全额SqlSessionManager那,而是默认使用线程不安全的DefaultSqlSession那
我觉得这个问题的答案是
DefaultSqlSession已经开发完了,那马在他的基础上修改的话,势必要考虑的东西比较多,还不如直接通过代理的方式增强这个方法,调用底层的DefauleSqLSession,如果再来个框架,整合mybatis,又要修改这一块,导致越来越臃肿,通过代理的方式不修改原来代码的基础上,实现了该功能,其实是很值得我们借鉴的,做到了解耦操作.
文中要是有不合理的地方,还请包涵指正,