Maison  >  Article  >  Java  >  Analyse du mécanisme de session de Mybatis du point de vue du code source (détails)

Analyse du mécanisme de session de Mybatis du point de vue du code source (détails)

不言
不言avant
2019-03-21 16:30:123051parcourir

Le contenu de cet article concerne l'analyse du mécanisme de session de Mybatis du point de vue du code source (détails). J'espère que les amis dans le besoin pourront s'y référer. vous être utile aidé.

Le camarade de classe Zhong assis à côté de moi a entendu dire que je maîtrisais le code source de Mybatis (je n'arrivais pas à savoir qui avait divulgué la nouvelle), alors il m'a posé une question en passant : Dans le même méthode, Mybatis Voulez-vous créer plusieurs sessions SqlSession lorsque vous demandez la base de données plusieurs fois ?

Peut-être que je me suis trop masturbé récemment. À cette époque, mon esprit était flou et mes yeux étaient flous, même si je lui ai répondu à l'époque : Si plusieurs demandes sont dans la même transaction, alors plusieurs requêtes partagent une SqlSession, sinon une SqlSession sera créée pour chaque requête. C'est du bon sens auquel nous sommes tous habitués dans le développement quotidien, mais je ne l'ai pas analysé du point de vue des principes pour M. Zhong. En conséquence, M. Zhong ne pensait pas à la nourriture et aux boissons. Je me sentais profondément coupable, alors j'ai secrètement pris ma décision, déterminé à donner une explication à mon camarade de classe Zhong.

Si vous n'êtes pas convaincu, lancez une démo

Testez si chaque requête créera une SqlSession lorsqu'aucune transaction n'est ajoutée à la méthode :

Analyse du mécanisme de session de Mybatis du point de vue du code source (détails)

Comme le montre le journal, lorsqu'aucune transaction n'est ajoutée, Mapper crée en effet une SqlSession pour interagir avec la base de données à chaque fois qu'il demande la base de données. lors de l'ajout d'une transaction. Situation :

Analyse du mécanisme de session de Mybatis du point de vue du code source (détails)

Comme le montre le journal, après l'ajout d'une transaction à la méthode, une seule SqlSession a été créée pour les deux requêtes, qui une fois prouve encore une fois ma réponse ci-dessus, mais répondre ainsi ne reflète pas le professionnalisme qu'un conducteur expérimenté devrait avoir, je vais donc démarrer la voiture.

Qu'est-ce que SqlSession

Avant de commencer, il faut d'abord comprendre, qu'est-ce que SqlSession ?

En termes simples, SqlSession est l'interface de session API de premier niveau pour le travail de Mybatis. Toutes les opérations de base de données sont implémentées via elle, car il s'agit d'une session, c'est-à-dire qu'une SqlSession ne doit survivre que dans une seule requête métier. On peut dire qu'une SqlSession correspond à cette session de base de données. Elle n'est pas active en permanence et doit être créée à chaque accès à la base de données.

Par conséquent, SqlSession n'est pas thread-safe. Chaque thread doit avoir sa propre instance SqlSession. Vous ne devez pas créer une SqlSession sous une forme singleton, ni utiliser des champs statiques et des variables d'instance pour provoquer l'apparition d'un problème de transaction SqlSession. , c'est pourquoi plusieurs requêtes partagent une session SqlSession dans la même transaction. Nous illustrons cela à partir du processus de création de SqlSession :

  1. Obtenez l'environnement à partir de la source de données de la classe de configuration Configuration ; >Récupérez TransactionFactory et DataSource à partir de la source de données et créez un objet de gestion de connexion Transaction
  2. Créer un objet Executor (SqlSession n'est que la façade de toutes les opérations, le vrai travail est Executor , qui encapsule tous les détails de l'opération de le JDBC sous-jacent) ;
  3. Créer une session SqlSession.
  4. Chaque fois qu'une session SqlSession est créée, un objet de gestion de connexion exclusif à la SqlSession sera créé. Si la SqlSession est partagée, des problèmes de transaction surviendront.

Analyse du point de vue du code source

Quelle étape est le point d'entrée pour l'analyse du code source ? Si vous avez lu l'analyse du code source de mybatis que j'ai écrite auparavant, je pense que vous ne vous attarderez pas devant le code source de Mybatis et que vous ne trouverez toujours pas l'entrée.

Comme mentionné dans l'article précédent, la classe d'implémentation de Mapper est un proxy. Ce qui exécute réellement la logique est MapperProxy.invoke(). Cette méthode exécute finalement sqlSessionTemplate.

org.mybatis.spring.SqlSessionTemplate :

Il s'agit de la méthode de construction finale pour créer SqlSessionTemplate. On peut voir que SqlSession est utilisé dans sqlSessionTemplate, qui est une classe proxy dynamique. implémenté par SqlSessionInterceptor. Nous sommes donc allés directement dans la forteresse :
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());
}

Toutes les méthodes de Mapper finiront par utiliser cette méthode pour gérer toutes les opérations de base de données. Les yeux de l'étudiant inactif Zhong étaient flous, et il ne le savait pas. s'il s'est abandonné et s'est trop masturbé, il m'a regardé d'un air vide et m'a demandé s'il y avait une différence entre Spring intégrant mybatis et utiliser mybatis seul. , vous n'avez donc pas besoin d'écrire beaucoup de code redondant et de vous concentrer sur le développement commercial.
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);
      }
    }
  }
}

Cette méthode de proxy dynamique effectue principalement les traitements suivants :

  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教程视频栏目!

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer