>  기사  >  Java  >  소스코드 관점에서 본 Mybatis의 세션 메커니즘 분석(상세)

소스코드 관점에서 본 Mybatis의 세션 메커니즘 분석(상세)

不言
不言앞으로
2019-03-21 16:30:123051검색

이 글의 내용은 마이바티스의 세션 메커니즘을 소스코드 관점에서 분석한 것입니다(세부사항). 도움이 필요한 친구들이 참고하시면 좋겠습니다.

내 옆에 앉은 동급생 Zhong은 내가 마이바티스 소스 코드에 능숙하다는 말을 듣고(누가 뉴스를 유출했는지 알 수 없었다) 나에게 질문을 했다. 같은 방법으로 마이바티스는 데이터베이스를 여러 번 요청하는데, 여러 SqlSession 세션을 만들고 싶나요?

아마도 최근에 너무 많이 한 것 같습니다. 당시에는 정신이 흐리고 눈도 흐릿했습니다. 동일한 거래에 여러 요청이 있는 경우 여러 요청이 공유됩니다. 그렇지 않으면 각 요청은 새로운 SqlSession을 생성합니다. 이것은 우리 모두가 일상적인 발전에서 당연하게 여기는 상식이지만 나는 그것을 종 씨에게 원칙적인 관점에서 분석하지 않았기 때문에 종 씨는 음식과 음료에 대해 생각하지 않게 되었습니다. 자책감이 깊어서 몰래 마음을 ​​굳혔습니다. 동급생 종에게 설명하기로 했습니다.

믿을 수 없다면 데모를 실행하세요.

메서드에 트랜잭션이 추가되지 않았을 때 각 요청이 SqlSession을 생성하는지 테스트하세요.

소스코드 관점에서 본 Mybatis의 세션 메커니즘 분석(상세)

로그에서 볼 수 있듯이 실제로 Mapper는 데이터베이스를 요청할 때마다 SqlSession을 생성하여 데이터베이스와 상호 작용합니다. 트랜잭션을 추가하는 상황을 살펴보겠습니다.

소스코드 관점에서 본 Mybatis의 세션 메커니즘 분석(상세)

트랜잭션을 추가한 후 로그에서 볼 수 있듯이 방법, 두 가지 요청에 대해서만 SqlSession을 만들었고, 이는 다시 한번 위에서 내 대답을 입증했지만, 이렇게 대답하는 것만으로는 숙련된 운전자가 가져야 할 전문성이 전혀 반영되지 않으므로 차에 시동을 걸겠습니다.

SqlSession이란 무엇입니까

시작하기 전에 먼저 SqlSession이 무엇인지 이해해야 합니다.

간단히 말하면 SqlSession은 Mybatis 작업을 위한 최상위 API 세션 인터페이스입니다. 모든 데이터베이스 작업은 이를 통해 구현됩니다. 즉, SqlSession은 하나의 비즈니스 요청에서만 유지되어야 한다고 할 수도 있습니다. SqlSession이 되려면 이 데이터베이스 세션에 대응하여 영구적으로 활성화되지 않으며 데이터베이스에 액세스할 때마다 생성되어야 합니다.

따라서 SqlSession은 스레드로부터 안전하지 않습니다. 각 스레드에는 자체 SqlSession 인스턴스가 있어야 합니다. SqlSession을 싱글톤 형식으로 만들거나 정적 필드 및 인스턴스 변수를 사용하여 SqlSession에서 트랜잭션 문제를 일으키면 안 됩니다. 요청은 동일한 트랜잭션에서 SqlSession 세션을 공유합니다. SqlSession 생성 프로세스에서 이를 설명하겠습니다.

  1. 구성 구성 클래스에서 환경 데이터 소스를 가져옵니다.
  2. 데이터 소스 TransactionFactory 및 DataSource에서 가져오고 트랜잭션을 만듭니다. 연결 관리 개체
  3. Executor 개체를 만듭니다(SqlSession은 모든 작업의 ​​외관일 뿐이며 실제로는 기본 JDBC의 모든 작업 세부 정보를 캡슐화하는 Executor입니다).
  4. SqlSession 세션이 생성될 때마다 SqlSession 전용 연결 관리 개체가 생성됩니다. SqlSession을 공유하면 트랜잭션 문제가 발생합니다.

소스코드 관점에서 분석

소스코드 분석의 진입점은 무엇인가요? 이전에 제가 작성한 마이바티스의 소스코드 분석을 읽어보셨다면, 마이바티스 소스코드 앞에서 머뭇거리지 않으시고 여전히 입구를 찾지 못하실 것이라 믿습니다.

이전 기사에서 언급했듯이 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을 생성하는 최종 생성 방법입니다. SqlSessionInterceptor에 의해 구현된 동적 프록시 클래스인 sqlSessionTemplate에서 SqlSession이 사용되는 것을 볼 수 있으므로 요새:

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 모든 방법은 결국 이 방법을 사용하여 모든 데이터베이스 작업을 처리하게 됩니다. 음식과 밥에 대해 생각하지 않는 동급생 Zhong은 눈이 흐려지고 자신을 포기하고 너무 자위를 했는지 궁금해합니다. 멍한 눈으로 나에게 스프링 통합 mybatis와 mybatis만 사용하는 방법을 물었습니다. 실제로 차이점은 스프링이 모든 처리 세부 사항을 캡슐화한다는 것입니다. 중복되는 코드가 많고 비즈니스 개발에 집중합니다.

이 동적 프록시 방법은 주로 다음 처리를 수행합니다.

  1. 根据当前条件获取一个SqlSession,此时SqlSession可能是新创建的也有可能是获取到上一次请求的SqlSession;
  2. 反射执行SqlSession方法,再判断当前会话是否是一个事务,如果是一个事务,则不commit;
  3. 如果此时抛出异常,判断如果是PersistenceExceptionTranslator且不为空,那么就关闭当前会话,并且将sqlSession置为空防止finally重复关闭,PersistenceExceptionTranslator是spring定义的数据访问集成层的异常接口;
  4. finally无论怎么执行结果如何,只要当前会话不为空,那么就会执行关闭当前会话操作,关闭当前会话操作又会根据当前会话是否有事务来决定会话是释放还是直接关闭

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>> resources =
    new NamedThreadLocal("Transactional resources");
  // 存储当前线程事务同步回调器
  // 当有事务,该字段会被初始化,即激活当前线程事务管理器
  private static final ThreadLocal<set>> synchronizations =
    new NamedThreadLocal("Transaction synchronizations");
  // ...
}</set></map>

这是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>());
}</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,用于事务每次状态发生时回调处理。

本篇文章到这里就已经全部结束了,更多其他精彩内容可以关注PHP中文网的Java教程视频栏目!

위 내용은 소스코드 관점에서 본 Mybatis의 세션 메커니즘 분석(상세)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 segmentfault.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제