[JAVA] Transaction distribuée avec SpringBoot + PostgreSql + mybatis + NarayanaJTA

J'ai essayé d'exécuter une transaction distribuée avec SpringBoot + PostgreSql + mybatis + NarayanaJTA

Prend en charge plusieurs bases de données, ce qui est très ennuyeux

Auparavant J'ai essayé de prendre en charge plusieurs bases de données en utilisant DynamicAbstractRoutingDataSource. Comme je l'ai confirmé plus tard, la restauration ne fonctionnait pas bien, j'ai donc essayé une autre méthode pour prendre en charge plusieurs sources de données.

Si vous voulez quelque chose qui fonctionne en premier, veuillez vous référer au lien source au bas de la page.

Pourquoi ne pas utiliser Atomikos Transaction Manager

Les livres d'introduction sur Spring Boot ont introduit les transactions distribuées utilisant Atomikos, et il y avait de nombreux exemples utilisant Atomicos pour obtenir des informations sur le net. Cependant, comme la gestion de XID, qui est la clé unique de chaque source de données, était suspecte lorsque la base de données de destination de la connexion était variable (rappelez-vous), cette fois, nous prendrons en charge plusieurs bases de données en utilisant Narayana JTA.

environnement

pom.xml


<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-narayana</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
  </dependency>
  <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.0</version>
  </dependency>
  <dependency>
    <groupId>com.integralblue</groupId>
    <artifactId>log4jdbc-spring-boot-starter</artifactId>
    <version>1.0.1</version>
  </dependency>
  <dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
  </dependency>
</dependencies>

Objectif de réalisation

Connectons-nous à PostgreSql en utilisant Narayana pour le moment

La coopération entre Spring Boot et Narayana lui-même peut être réalisée en enregistrant le Narayana DataSource Bean fourni par Spring en tant que bean DataSource.

Par exemple, vous pouvez vous connecter pour des transactions distribuées en utilisant Narayana simplement en vous enregistrant dans le bean comme ceci.

DatasourceConfigOne.java


package com.example.datasource;

import com.example.common.DataSourceUtil;
import com.example.config.MyDataBaseProperties;
import com.example.constance.DataBaseConst;
import javax.sql.DataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
@EnableConfigurationProperties(MyDataBaseProperties.class)
@MapperScan(sqlSessionFactoryRef = DataBaseConst.DataSourceOneConst.NAME,
    basePackages = DataBaseConst.DataSourceOneConst.MAPPER)
public class DatasourceConfigOne {

  @Autowired
  MyDataBaseProperties properties;

  @Autowired
  MybatisProperties mybatisProperty;

  /**
   * Get dataSource one.
   * 
   * @return
   */
  @Primary
  @Bean
  public DataSource getDataSourceOne() {
    return DataSourceUtil.getDataSource(
        this.properties.getProperty(DataBaseConst.DataSourceOneConst.DATA_SOURCE).getDetail());
  }
}

MyDataBaseProperties obtient les informations de source de données en spécifiant le nom de propriété à partir de la liste d'informations de la source de données décrite dans le fichier yaml. Dans DataSourceUtil # getDataSource, NarayanaDataSourceBean est créé en fonction de la propriété acquise.

De plus, comme Mybatis est cette fois utilisé comme ORM, une méthode de génération SqlSessionFactoryBean pour Mybatis est également ajoutée à cette classe.

DatasourceConfigOne.java


/**
 * Get SqlSessionFactory one.
 * 
 * @return
 */
@Primary
@Bean(name = DataBaseConst.DataSourceOneConst.NAME)
public SqlSessionFactoryBean getSqlSessionFactoryOne() {
  SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
  factory.setConfiguration(this.mybatisProperty.getConfiguration());
  factory.setDataSource(getDataSourceOne());
  return factory;
}

Tout ce que vous avez à faire est de lire la configuration de Mybatis et de créer une session avec les informations de la source de données. Ce paramètre de source de données sera utilisé lorsque le DAO correspondant aux packages de base MapperScan déclarés dans la classe est DI.

@MapperScan(sqlSessionFactoryRef = DataBaseConst.DataSourceOneConst.NAME,
    basePackages = DataBaseConst.DataSourceOneConst.MAPPER)

Si la base de données à laquelle se connecter est choisie, si cette classe est augmentée du nombre de bases de données connectées sur cette base, les transactions distribuées peuvent être effectuées sans problème, même avec des transactions déclaratives.

Activer toutes les fonctionnalités du gestionnaire de connexion dans Narayana

Les transactions distribuées sont certainement possibles en utilisant le Narayana DataSource Bean fourni par Spring. Cependant, comme vous pouvez le voir à partir de la source de l'extrait ci-dessous, seules quelques fonctionnalités sont disponibles.

java:org.springframework.boot.jta.narayana.NarayanaDataSourceBean


@Override
public Connection getConnection() throws SQLException {
  Properties properties = new Properties();
  properties.put(TransactionalDriver.XADataSource, this.xaDataSource);
  return ConnectionManager.create(null, properties);
}

java:com.arjuna.ats.internal.jdbc.ConnectionManager


/*
 * Connections are pooled for the duration of a transaction.
 */
public static synchronized Connection create (String dbUrl, Properties info) throws SQLException
{
    String user = info.getProperty(TransactionalDriver.userName, "");
    String passwd = info.getProperty(TransactionalDriver.password, "");
    String dynamic = info.getProperty(TransactionalDriver.dynamicClass, "");
    String poolConnections = info.getProperty(TransactionalDriver.poolConnections, "true");
    Object xaDataSource = info.get(TransactionalDriver.XADataSource);
    int maxConnections = Integer.valueOf(info.getProperty(TransactionalDriver.maxConnections, "10"));

~~~ Omis ~~~
}

Étant donné que seul XADataSource est passé en tant que propriété lors de la création d'une connexion, ni le paramètre d'utilisation du pool de connexions ni le nombre maximal de connexions ne peuvent être définis. D'autres propriétés sont utilisées uniquement pour confirmer que les sources de données sont les mêmes, donc il n'y a pas beaucoup d'effet, mais si vous utilisez 10 sources de données ou plus, cela ne devrait pas être le cas. De plus, dynamicClass, poolConnections et maxConnections définissent des éléments qui n'existent pas dans PGXADataSource de PostgreSql.

Par conséquent, créez une classe qui étend NarayanaDataSourceBean et une classe qui étend PGXADataSource. L'extension de PGXADataSource peut ne pas être nécessaire si elle est implémentée en passant la valeur de propriété de l'extérieur, mais elle est toujours créée.

MyXaDataSource.java


package com.example.common;

import java.util.Objects;
import org.postgresql.xa.PGXADataSource;

/**
 * Extends PGXADataSource for TransactionalDriver.
 * 
 * @author suimyakunosoko
 *
 */
public class MyXaDataSource extends PGXADataSource {

  /** enable pool connection. */
  private boolean poolConnections = true;

  /** max pool connection counts. */
  private int maxConnections = 10;

  public String getDynamicClass() {
    return this.getClass().getName();
  }

  public boolean getPoolConnections() {
    return this.poolConnections;
  }

  public void setPoolConnections(boolean poolConnections) {
    this.poolConnections = poolConnections;
  }

  public int getMaxConnections() {
    return this.maxConnections;
  }

  public void setMaxConnections(int maxConnections) {
    this.maxConnections = maxConnections;
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof MyXaDataSource)) {
      return false;
    }
    MyXaDataSource casted = (MyXaDataSource) obj;
    return Objects.equals(casted.getURL(), this.getURL())
        && Objects.equals(casted.getUser(), this.getUser())
        && Objects.equals(casted.getPassword(), this.getPassword());
  }

}

Ajoutez les propriétés poolConnections et maxConnections qui manquaient dans MyXaDataSource, qui est une extension de PGXADataSource. Faites correspondre la valeur initiale à Connection Manager. Étant donné que getDynamicClass est utilisé uniquement pour la vérification de la source de données, transmettez le nom de classe de manière appropriée.

Le remplacement égal est également effectué à l'aide de la méthode d'égalité de XADataSource lorsque le gestionnaire de connexion vérifie l'existence de la source de données. Si vous conservez la valeur par défaut, ce sera une comparaison entre les valeurs de hachage, donc je la remplacerai. Puisque l'URL de PGXADataSource inclut le nom d'utilisateur et le mot de passe, ce n'est pas nécessaire, mais je l'ajouterai ici.

MyNarayanaDataSourceBean.java


package com.example.common;

import com.arjuna.ats.internal.jdbc.ConnectionManager;
import com.arjuna.ats.jdbc.TransactionalDriver;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
import org.springframework.boot.jta.narayana.NarayanaDataSourceBean;

/**
 * Extends NarayanaDataSourceBean for ConnectionManager and enable TransactionalDriverProperties.
 * 
 * @author suimyakunosoko
 *
 */
public class MyNarayanaDataSourceBean extends NarayanaDataSourceBean {

  private final Properties properties;

  /**
   * Wrap NarayanaDataSourceBean for ConnectionManager.
   * 
   * @param myXaDataSource MyXaDataSource
   */
  public MyNarayanaDataSourceBean(MyXaDataSource myXaDataSource) {
    super(myXaDataSource);
    this.properties = new Properties();
    this.properties.put(TransactionalDriver.userName, myXaDataSource.getUser());
    this.properties.put(TransactionalDriver.password, myXaDataSource.getPassword());
    this.properties.put(TransactionalDriver.dynamicClass, myXaDataSource.getDynamicClass());
    this.properties.put(TransactionalDriver.poolConnections,
        String.valueOf(myXaDataSource.getPoolConnections()));
    this.properties.put(TransactionalDriver.XADataSource, myXaDataSource);
    this.properties.put(TransactionalDriver.maxConnections,
        String.valueOf(myXaDataSource.getMaxConnections()));
  }

  @Override
  public Connection getConnection() throws SQLException {
    return ConnectionManager.create(null, this.properties);
  }

  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    return ConnectionManager.create(null, this.properties);
  }

}

Modifiez pour définir des éléments qui n'ont pas été définis dans MyNarayanaDataSourceBean, qui est une extension de NarayanaDataSourceBean. MyXaDataSource est spécifié comme argument, mais si le serveur PostgreSql, le serveur MySql et le serveur Oracle le prennent tous en charge, je pense qu'une interface qui couvre les éléments de configuration nécessaires sera préparée. Cette fois, je m'en fiche tant que je peux me connecter à PostgreSQL.

Placez jbossts-properties.xml sur la ressource afin que TwoPahseCommit soit activé

Il fonctionne tel quel et revient correctement, mais le paramètre initial de Narayana est OnePhaseCommit et certains flux de journaux WARN étranges.

WARN 4812 --- [           main] com.arjuna.ats.common                    : ARJUNA048002: Could not find configuration file, URL was: null

C'est le message si vous essayez de charger le fichier jbossts-properties.xml et qu'il n'est pas trouvé. Lors de l'activation de TwoPahseCommit, je vais stocker ce fichier sous la ressource.

jbossts-properties.xml


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <!-- (default is YES) -->
    <!-- (twoPhaseCommit is NO) -->
    <entry key="CoordinatorEnvironmentBean.commitOnePhase">NO</entry>
    
    <!-- (Must be unique across all Arjuna instances.) -->
    <!-- (default is 1) -->
    <entry key="CoreEnvironmentBean.nodeIdentifier">1</entry>

</properties>

Cela active la suppression du journal WARN et la validation en deux phases.

Jusqu'à présent, la transaction distribuée statique est terminée, mais que faire si elle est dynamique?

Avec l'implémentation jusqu'à présent, des transactions distribuées pour des sources de données statiques (telles que la base de données principale du propre système et la base de données du système lié) peuvent être réalisées. Mais supposons qu'un design fou ait été fait ici. De plus, cela ne peut pas aller à l'encontre du design. Le contenu de la conception est ** "Oui! Comme il existe de nombreuses branches, construisons un serveur pour chaque branche et avons une base de données!" ** En fait c'est impossible ... Je voulais penser que c'était impossible, mais j'ai rencontré un événement similaire, donc je n'ai pas d'autre choix que d'y faire face.

Abandonnez DI tant que la destination de la connexion est décidée dans la logique

Puisque la source de données à utiliser n'est pas décidée au moment où chaque contrôleur et chaque service est exécuté, la DI de Spring est abandonnée. Par conséquent, il est nécessaire de réaliser les fonctions suivantes absorbées par le cadre.

  1. Créez une SqlSession lorsque la source de données à utiliser est choisie
  2. Générez une instance Dao avec des informations de connexion injectées
  3. Collecte de session après la fin de la transaction

Implémenté 1 et 2 en se référant au site officiel de Mybatis. 3 est réalisé en utilisant la fonction d'AOP.

Créer dynamiquement une SqlSession pour créer une instance Dao

Fondamentalement, si vous lisez le document officiel Mybatis et que vous le mettez en œuvre docilement, il y a peu de points à prendre en compte. Cela devrait fonctionner correctement si vous gérez les informations de session créées pour chaque demande (pour chaque thread), ne créez pas la même session plusieurs fois et faites au moins attention à ces deux.

Cette fois, créez SqlSessionUtil pour créer SqlSession et SqlSessionManager pour gérer SqlSession. Les deux sont utilisés comme DI.

SqlSessionUtil.java


package com.example.common;

import com.example.config.MyDataBaseProperties;
import com.example.constance.DataBaseConst;
import java.util.Objects;
import javax.sql.DataSource;
import javax.sql.XADataSource;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.TransactionFactory;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class SqlSessionUtil {

  @Autowired
  MyDataBaseProperties properties;

  @Autowired
  MybatisProperties mybatisProperty;

  @Autowired
  SqlSessionManager manager;

  /**
   * create SqlSessionFactory by DataBase name.
   * 
   * @param name DataBase name
   * @return
   */
  public SqlSessionFactory getSqlSessionFactory(String name) {
    return getSqlSessionFactory(name, null);
  }

  /**
   * <p>
   * create SqlSessionFactory by DataBase name.
   * </p>
   * <p>
   * when no DataBase name on yml, create SqlSessionFactory by defBase.
   * </p>
   * 
   * @param name DataBase name
   * @param defBase use when yml dose not contain DataBaseName
   * @return
   */
  public SqlSessionFactory getSqlSessionFactory(String name, String defBase) {
    XADataSource dataSourceprop = this.properties.getProperty(name, defBase).getDetail();
    DataSource dataSource = DataSourceUtil.getDataSource(dataSourceprop);

    TransactionFactory transactionFactory = new JdbcTransactionFactory();
    Environment environment = new Environment("development", transactionFactory, dataSource);
    Configuration configuration = DataSourceUtil.fillNullByDefault(new Configuration(environment),
        this.mybatisProperty.getConfiguration());
    configuration.addMappers(DataBaseConst.DataSourceDefault.MAPPER);
    return new SqlSessionFactoryBuilder().build(configuration);
  }

  /**
   * Get SqlSession by name.
   * 
   * @param name DataBase name
   * @return
   */
  public SqlSession getSqlSession(String name) {
    return getSqlSession(name, null);
  }

  /**
   * <p>
   * create SqlSession by DataBase name.
   * </p>
   * <p>
   * when no DataBase name on yml, create SqlSession by defBase.
   * </p>
   * 
   * @param name DataBase name
   * @param defBase use when yml dose not contain DataBaseName
   * @return
   */
  public SqlSession getSqlSession(String name, String defBase) {
    SqlSession session = this.manager.get(name);
    if (!Objects.isNull(session)) {
      return session;
    }
    session = getSqlSessionFactory(name, defBase).openSession();
    this.manager.put(name, session);

    return session;
  }

}

En plus de la fonction de création d'une source de données à partir d'un fichier de paramètres dans SqlSession, une fonction de création d'informations de connexion à partir d'un modèle lorsqu'il n'y a pas d'informations de connexion est également ajoutée. Le SqlSessionManager, qui ressemble à une tonalité unique, a une variable ThreadLocal en interne, donc il n'est pas affecté par les autres threads. Il peut y avoir une méthode pour passer une instance de SqlSessionManager en tenant compte du cas d'une mise à jour par lots.

SqlSessionManager.java


package com.example.common;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.apache.ibatis.session.SqlSession;
import org.springframework.stereotype.Component;

@Component
public class SqlSessionManager {
  ThreadLocal<Map<String, SqlSession>> sessionMap = new ThreadLocal<>();

  /**
   * put SqlSession.
   * 
   * @param key key
   * @param session SqlSession
   */
  public void put(String key, SqlSession session) {
    init();
    if (Objects.isNull(this.sessionMap.get())) {
      this.sessionMap.set(new HashMap<String, SqlSession>());
    }
    this.sessionMap.get().put(key, session);
  }

  /**
   * get SqlSession by key.
   * 
   * @param key key
   * @return
   */
  public SqlSession get(String key) {
    init();
    return this.sessionMap.get().get(key);

  }

  /**
   * close all session.
   */
  public void close() {
    init();
    this.sessionMap.get().forEach((key, session) -> session.close());
    this.sessionMap.set(null);
  }

  private void init() {
    if (Objects.isNull(this.sessionMap.get())) {
      this.sessionMap.set(new HashMap<String, SqlSession>());
    }
  }

}

SqlSessionManagerd conserve la carte de la SqlSession créée. Moment de création d'instance! = Moment de début de thread, donc la vérification NULL est effectuée au début de chaque méthode en tant que contre-mesure NPE. Il aurait peut-être été bon de créer une instance de LocalThread au moment de la réception de la demande avec AOP.

Fermez la SqlSession créée dynamiquement une fois la transaction terminée

Il est bon de créer une SqlSession, mais si vous fermez la SqlSession créée après le démarrage de la transaction avant la fin de la transaction, elle ne fonctionnera pas correctement. Par conséquent, je fermerai la session une fois la transaction déclarative terminée dans AOP.

SessionCloseAspect.java


package com.example.aop;

import com.example.common.SqlSessionManager;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Aspect
@Component
@Order(Ordered.LOWEST_PRECEDENCE - 1)
@Transactional
public class SessionCloseAspect {

  @Autowired
  SqlSessionManager manager;

  @After("@within(org.springframework.transaction.annotation.Transactional)")
  public void closeSqlSession() {
    this.manager.close();
  }

}

L'ordre est spécifié à partir de celui avec le plus grand nombre. Personnellement, il a une priorité élevée = j'ai senti qu'il a été exécuté en premier, donc cela semble un peu étrange, mais s'il est exécuté dans cet ordre, ce serait le cas.

C'est OK si aucune exception ne se produit même si la transaction de déclaration est quittée après l'opération avec le SqlSession créé dynamiquement.

J'ai confirmé le fonctionnement normal pour le moment (problème à craindre)

No modifier information found for db. Connection will be closed immediately

Je crains que les journaux sortent toujours. Il semble que l'option de se déconnecter lors de la synchronisation avec d'autres connexions lors de la fin de la connexion est désactivée.

Je veux aussi joindre jbossts-properties.xml dans le fichier yaml autant que possible, mais cela semble inutile car le journal WARN apparaît.

référence

Narayana Spring Boot example
Échantillon officiel de Narayana. Comme il n'y a pas beaucoup d'informations en anglais, j'ai fait un essai et une erreur sur cette base.

jbossts-properties.xml
Quand j'ai obtenu Narayana de Maven, il n'y avait pas de fichier XML pour le réglage, donc je l'ai emprunté à partir d'ici

La source

Si vous voulez voir la source parce que l'explication est difficile à comprendre, ou si vous voulez quelque chose qui puisse être utilisé rapidement parce que le mandat est bon, veuillez cliquer ici. https://github.com/suimyakunosoko/narayana-spring-boot-mybatis-postgresql

Recommended Posts

Transaction distribuée avec SpringBoot + PostgreSql + mybatis + NarayanaJTA
Jusqu'à l'acquisition de données avec Spring Boot + MyBatis + PostgreSQL
Prend en charge le multi-port avec SpringBoot
J'ai écrit un test CRUD avec SpringBoot + MyBatis + DBUnit (Partie 1)