微信公众号「后端进阶」,专一后端技术分享:Java、Golang、WEB框架、分布式中间件、服务治理等等。
老司机倾囊相授,带你一路进阶,来不及解释了快上车!
坐在我旁边的钟同窗据说我精通Mybatis源码(我就想不通,是谁透漏了风声),就顺带问了我一个问题:在同一个方法中,Mybatis屡次请求数据库,是否要建立多个SqlSession会话?java
可能最近撸多了,当时脑子里一片模糊,眼神迷离,虽然我当时回答他:若是多个请求同一个事务中,那么多个请求都在共用一个SqlSession,反之每一个请求都会建立一个SqlSession。这是咱们在日常开发中都习觉得常的常识了,但我却没有从原理的角度给钟同窗分析,致使钟同窗茶饭不思,做为老司机的我,感到深深的自责,因而我暗自下定决心,要给钟同窗一个交代。git
测试在方法中不加事务时,每一个请求是否会建立一个SqlSession:github
从日志能够看出,在没有加事务的状况下,确实是Mapper的每次请求数据库,都会建立一个SqlSession与数据库交互,下面咱们再看看加了事务的状况:spring
从日志能够看出,在方法中加了事务后,两次请求只建立了一个SqlSession,再次证实了我上面的回答,可是仅仅这样回答是体现彻底不出一个老司机应有的职业素养的,因此,我要发车了。sql
在发车以前,咱们必须得先搞明白,什么是SqlSession?数据库
简单来讲,SqlSession是Mybatis工做的最顶层API会话接口,全部的数据库操做都经由它来实现,因为它就是一个会话,即一个SqlSession应该仅存活于一个业务请求中,也能够说一个SqlSession对应这一次数据库会话,它不是永久存活的,每次访问数据库时都须要建立它。后端
所以,SqlSession并非线程安全,每一个线程都应该有它本身的 SqlSession 实例,千万不能将一个SqlSession搞成单例形式,或者静态域和实例变量的形式都会致使SqlSession出现事务问题,这也就是为何多个请求同一个事务中会共用一个SqlSession会话的缘由,咱们从SqlSession的建立过程来讲明这点:安全
每次建立一个SqlSession会话,都会伴随建立一个专属SqlSession的链接管理对象,若是SqlSession共享,就会出现事务问题。微信
源码分析从哪一步做为入口呢?若是是看过我以前写的那几篇关于mybatis的源码分析,我相信你不会在Mybatis源码前磨磨蹭蹭,迟迟找不到入口。session
在以前的文章里已经说过了,Mapper的实现类是一个代理,真正执行逻辑的是MapperProxy.invoke(),该方法最终执行的是sqlSessionTemplate。
org.mybatis.spring.SqlSessionTemplate:
private final SqlSession sqlSessionProxy; 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; this.sqlSessionProxy = (SqlSession) newProxyInstance( SqlSessionFactory.class.getClassLoader(), new Class[] { SqlSession.class }, new SqlSessionInterceptor()); }
这个是建立SqlSessionTemplate的最终构造方法,能够看出sqlSessionTemplate中用到了SqlSession,是SqlSessionInterceptor实现的一个动态代理类,因此咱们直接深刻要塞:
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); } } } }
Mapper全部的方法,最终都会用这个方法来处理全部的数据库操做,茶饭不思的钟同窗眼神迷离不知道是否是自暴自弃致使撸多了,眼神空洞地望着我,问我spring整合mybatis和mybatis单独使用是否有区别,其实没区别,区别就是spring封装了全部处理细节,你就不用写大量的冗余代码,专一于业务开发。
该动态代理方法主要作了如下处理:
org.mybatis.spring.SqlSessionUtils#getSqlSession:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED); notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED); SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); SqlSession session = sessionHolder(executorType, holder); if (session != null) { return session; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Creating a new SqlSession"); } session = sessionFactory.openSession(executorType); registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session); return session; }
是否是看到了不服跑个demo时看到的日志“Creating a new SqlSession”了,那么证实我直接深刻的地方挺准确的,没有丝毫偏差。在这个方法当中,首先是从TransactionSynchronizationManager(如下称当前线程事务管理器)获取当前线程threadLocal是否有SqlSessionHolder,若是有就从SqlSessionHolder取出当前SqlSession,若是当前线程threadLocal没有SqlSessionHolder,就从sessionFactory中建立一个SqlSession,具体的建立步骤上面已经说过了,接着注册会话到当前线程threadLocal中。
先来看看当前线程事务管理器的结构:
public abstract class TransactionSynchronizationManager { // ... // 存储当前线程事务资源,好比Connection、session等 private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources"); // 存储当前线程事务同步回调器 // 当有事务,该字段会被初始化,即激活当前线程事务管理器 private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations"); // ... }
这是spring的一个当前线程事务管理器,它容许将当前资源存储到当前线程ThreadLocal中,从前面也可看出SqlSessionHolder是保存在resources中。
org.mybatis.spring.SqlSessionUtils#registerSessionHolder:
private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator, SqlSession session) { SqlSessionHolder holder; // 判断当前是否有事务 if (TransactionSynchronizationManager.isSynchronizationActive()) { Environment environment = sessionFactory.getConfiguration().getEnvironment(); // 判断当前环境配置的事务管理工厂是不是SpringManagedTransactionFactory(默认) if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registering transaction synchronization for SqlSession [" + session + "]"); } holder = new SqlSessionHolder(session, executorType, exceptionTranslator); // 绑定当前SqlSessionHolder到线程ThreadLocal中 TransactionSynchronizationManager.bindResource(sessionFactory, holder); // 注册SqlSession同步回调器 TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory)); holder.setSynchronizedWithTransaction(true); // 会话使用次数+1 holder.requested(); } else { if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional"); } } else { throw new TransientDataAccessResourceException( "SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization"); } } } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because synchronization is not active"); } } }
注册SqlSession到当前线程事务管理器的条件首先是当前环境中有事务,不然不注册,判断是否有事务的条件是synchronizations的ThreadLocal是否为空:
public static boolean isSynchronizationActive() { return (synchronizations.get() != null); }
每当咱们开启一个事务,会调用initSynchronization()方法进行初始化synchronizations,以激活当前线程事务管理器。
public static void initSynchronization() throws IllegalStateException { if (isSynchronizationActive()) { throw new IllegalStateException("Cannot activate transaction synchronization - already active"); } logger.trace("Initializing transaction synchronization"); synchronizations.set(new LinkedHashSet<TransactionSynchronization>()); }
因此当前有事务时,会注册SqlSession到当前线程ThreadLocal中。
Mybatis本身也实现了一个自定义的事务同步回调器SqlSessionSynchronization,在注册SqlSession的同时,也会将SqlSessionSynchronization注册到当前线程事务管理器中,它的做用是根据事务的完成状态回调来处理线程资源,即当前若是有事务,那么当每次状态发生时就会回调事务同步器,具体细节可移步至Spring的org.springframework.transaction.support包。
回到SqlSessionInterceptor代理类的逻辑,发现判断会话是否须要提交要调用如下方法:
org.mybatis.spring.SqlSessionUtils#isSqlSessionTransactional:
public static boolean isSqlSessionTransactional(SqlSession session, SqlSessionFactory sessionFactory) { notNull(session, NO_SQL_SESSION_SPECIFIED); notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED); SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); return (holder != null) && (holder.getSqlSession() == session); }
取决于当前SqlSession是否为空而且判断当前SqlSession是否与ThreadLocal中的SqlSession相等,前面也分析了,若是当前没有事务,SqlSession是不会保存到事务同步管理器的,即没有事务,会话提交。
org.mybatis.spring.SqlSessionUtils#closeSqlSession:
public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) { notNull(session, NO_SQL_SESSION_SPECIFIED); notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED); SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); if ((holder != null) && (holder.getSqlSession() == session)) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Releasing transactional SqlSession [" + session + "]"); } holder.released(); } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Closing non transactional SqlSession [" + session + "]"); } session.close(); } }
方法不管执行结果如何都须要执行关闭会话逻辑,这里的判断也是判断当前是否有事务,若是SqlSession在事务当中,则减小引用次数,没有真实关闭会话。若是当前会话不存在事务,则直接关闭会话。
虽然说钟同窗问了我一个Mybatis的问题,我却中了Spring的圈套,猛然发现整个事务链路都处在Spring的管控当中,这里涉及到了Spring的自定义事务的一些机制,其中当前线程事务管理器是整个事务的核心与中轴,当前有事务时,会初始化当前线程事务管理器的synchronizations,即激活了当前线程同步管理器,当Mybatis访问数据库会首先从当前线程事务管理器获取SqlSession,若是不存在就会建立一个会话,接着注册会话到当前线程事务管理器中,若是当前有事务,则会话不关闭也不commit,Mybatis还自定义了一个TransactionSynchronization,用于事务每次状态发生时回调处理。
钟同窗,this is for you!