mybatis源码解析八(spring处理sqlsession线程安全问题)

上一期,分析了下关于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> resources = new NamedThreadLocal<>("Transactional resources");
         */
        SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

        //会话持有者的数量执行+1操作,有人想获取持有者资源,将引用计数器加1
        SqlSession session = sessionHolder(executorType, holder);
        if (session != null) {
            return session;
        }

        //如果回话持有者中不存在SqlSession.则调用sessionFactory开启一个session
        LOGGER.debug(() -> "Creating a new SqlSession");
        session = sessionFactory.openSession(executorType);

        /**
         * 1.2
         * 注册到会话持有者中,如果存在事务,就会将当前的会话持有者和sessionFactory添加到thredLocal中
         */
        registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

        return session;
    }

这节我们的重点是分析SqlSession.所以事物我们暂且放下,看懂了,代码比较简单.我这里在总结下
1:当我们引入spring框架后,我们执行查询,SqLSessionTemplete户通过代理方式"拦截"执行方法,由于DefaultSqSLession线程不安全
2:这里获取线程安全的SqlSession,
3:判断是否存在事务
4:事务不存在,直接调用SqlSessionFactory获取sesison返回,直接执行后续的方法,也就是说,不通的线程访问,或者同一个线程范根同样的查询方法,都会去创建一个新的session,这样的话,也就导致以及缓存不存在了.
如果存在事务,回去事务资源管理器获取session持有者,如果存在,就将当前的引用计数加1,如果不存在,则通过SqlSessionFactory.openSession获取一个session,然后注册到事务管理器中,这里是将当前资源添加到ThreadLOcal中,,在local中定义了一个map类似于这样的结构ThreadLocal>,
5:调用DefaultSqLSession执行后续的逻辑,所以也就是说,当存在事物的情况下,一级缓存才有意义

我们通过案例来加强一下理解,以下案例,查询两次,默认回去走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);

    }

我们看到结果中查询两次都走了数据库查询,一级缓存没有起作用


image.png

再来看另一个案例,加了注解后,第二次查询回去走缓存

     */
    @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);

    }

image.png

那就有一个问题,为什么mybatis不适用线程安全额SqlSessionManager那,而是默认使用线程不安全的DefaultSqlSession那
我觉得这个问题的答案是
DefaultSqlSession已经开发完了,那马在他的基础上修改的话,势必要考虑的东西比较多,还不如直接通过代理的方式增强这个方法,调用底层的DefauleSqLSession,如果再来个框架,整合mybatis,又要修改这一块,导致越来越臃肿,通过代理的方式不修改原来代码的基础上,实现了该功能,其实是很值得我们借鉴的,做到了解耦操作.
文中要是有不合理的地方,还请包涵指正,

你可能感兴趣的:(mybatis源码解析八(spring处理sqlsession线程安全问题))