[JAVA] Verteilte Transaktion mit SpringBoot + PostgreSql + mybatis + NarayanaJTA

Ich habe versucht, eine verteilte Transaktion mit SpringBoot + PostgreSql + mybatis + NarayanaJTA auszuführen

Unterstützt mehrere Datenbanken, was sehr ärgerlich ist

Zuvor Ich habe versucht, mehrere Datenbanken mit DynamicAbstractRoutingDataSource zu unterstützen. Als ich später nachgesehen habe, hat das Rollback nicht gut funktioniert, daher habe ich eine andere Methode zur Unterstützung mehrerer Datenquellen ausprobiert.

Wenn Sie etwas möchten, das zuerst funktioniert, lesen Sie bitte den Quelllink unten auf der Seite.

Warum nicht Atomikos Transaction Manager verwenden?

In Einführungsbüchern zu Spring Boot wurden verteilte Transaktionen mit Atomikos vorgestellt, und es gab viele Beispiele mit Atomicos, um Informationen im Internet zu erhalten. Da die Behandlung von XID, dem eindeutigen Schlüssel jeder Datenquelle, verdächtig war, als die Verbindungsziel-DB variabel war (denken Sie daran), werden wir dieses Mal mehrere Datenbanken mit Narayana JTA unterstützen.

Umgebung

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>

Realisierungsziel

Lassen Sie uns vorerst mit Narayana eine Verbindung zu PostgreSql herstellen

Die Zusammenarbeit zwischen Spring Boot und Narayana selbst kann durch Registrieren der von Spring bereitgestellten Narayana DataSource Bean als DataSource Bean realisiert werden.

Sie können beispielsweise mit Narayana eine Verbindung für verteilte Transaktionen herstellen, indem Sie sich wie folgt in der Bean registrieren.

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 ruft die Datenquelleninformationen ab, indem der Eigenschaftsname aus der Informationsliste der in der yaml-Datei beschriebenen Datenquelle angegeben wird. In DataSourceUtil # getDataSource wird NarayanaDataSourceBean basierend auf der erworbenen Eigenschaft erstellt.

Da diesmal Mybatis als ORM verwendet wird, wird dieser Klasse auch eine SqlSessionFactoryBean-Generierungsmethode für Mybatis hinzugefügt.

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;
}

Sie müssen lediglich die Konfiguration von Mybatis lesen und eine Sitzung mit den Datenquelleninformationen erstellen. Diese Datenquelleneinstellung wird verwendet, wenn das DAO, das den in der Klasse deklarierten MapperScan-Basispaketen entspricht, DI ist.

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

Wenn entschieden wird, zu welcher Datenbank eine Verbindung hergestellt werden soll, und diese Klasse um die Anzahl der darauf basierenden Datenbanken erhöht wird, können verteilte Transaktionen auch bei deklarativen Transaktionen problemlos ausgeführt werden.

Aktivieren Sie alle Connection Manager-Funktionen in Narayana

Verteilte Transaktionen sind mit der von Spring bereitgestellten Narayana DataSource Bean durchaus möglich. Wie Sie jedoch aus dem folgenden Auszug sehen können, sind nur wenige Funktionen verfügbar.

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"));

~~~ weggelassen ~~~
}

Da beim Erstellen einer Verbindung nur XADataSource als Eigenschaft übergeben wird, können weder die Einstellung für die Verwendung des Verbindungspools noch die maximale Anzahl von Verbindungen festgelegt werden. Andere Eigenschaften werden nur verwendet, um zu bestätigen, dass die Datenquellen identisch sind, sodass keine großen Auswirkungen auftreten. Wenn Sie jedoch 10 oder mehr Datenquellen verwenden, sollte dies nicht der Fall sein. Darüber hinaus legen dynamicClass, poolConnections und maxConnections Elemente fest, die in der PGXADataSource von PostgreSql nicht vorhanden sind.

Erstellen Sie daher eine Klasse, die NarayanaDataSourceBean erweitert, und eine Klasse, die PGXADataSource erweitert. Die Erweiterung von PGXADataSource ist möglicherweise nicht erforderlich, wenn sie durch Übergeben des Eigenschaftswerts von außen implementiert wird, aber dennoch erstellt wird.

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());
  }

}

Fügen Sie die Eigenschaften poolConnections und maxConnections hinzu, die in MyXaDataSource, einer Erweiterung von PGXADataSource, fehlten. Passen Sie den Anfangswert an den Verbindungsmanager an. Da getDynamicClass nur für die Datenquellenprüfung verwendet wird, übergeben Sie den Klassennamen entsprechend.

Die Überschreibung der Gleichheit erfolgt auch mithilfe der Gleichheitsmethode der XADataSource, wenn der Verbindungsmanager das Vorhandensein der Datenquelle überprüft. Wenn Sie die Standardeinstellung beibehalten, wird ein Vergleich zwischen Hash-Werten durchgeführt, sodass ich ihn überschreiben werde. Da die URL von PGXADataSource den Benutzernamen und das Passwort enthält, ist dies nicht erforderlich, aber ich werde es hier hinzufügen.

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);
  }

}

Ändern Sie diese Option, um Elemente festzulegen, die nicht in MyNarayanaDataSourceBean festgelegt wurden. Dies ist eine Erweiterung von NarayanaDataSourceBean. MyXaDataSource wird als Argument angegeben, aber wenn der PostgreSql-Server, der MySql-Server und der Oracle-Server dies alle unterstützen, wird meiner Meinung nach eine Schnittstelle vorbereitet, die die erforderlichen Einstellungselemente abdeckt. Diesmal ist es mir egal, solange ich mich mit PostgreSQL verbinden kann.

Platzieren Sie jbossts-properties.xml in der Ressource, sodass TwoPahseCommit aktiviert ist

Es funktioniert wie es ist und rollt ordnungsgemäß zurück, aber Narayanas Anfangseinstellung ist OnePhaseCommit und einige seltsame WARN-Protokollflüsse.

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

Dies ist die Meldung, wenn Sie versuchen, die Datei jbossts-properties.xml zu laden, die jedoch nicht gefunden wird. Während ich TwoPahseCommit aktiviere, werde ich diese Datei unter der Ressource speichern.

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>

Dies aktiviert die WARN-Protokollunterdrückung und das Zwei-Phasen-Commit.

Bisher ist die statische verteilte Transaktion abgeschlossen, aber was ist, wenn sie dynamisch ist?

Mit der bisherigen Implementierung können verteilte Transaktionen für statische Datenquellen (wie die Hauptdatenbank des eigenen Systems und die Datenbank des verknüpften Systems) realisiert werden. Aber nehmen wir an, das verrückte Design wurde hier gemacht. Darüber hinaus kann es nicht gegen das Design verstoßen. Der Inhalt des Entwurfs lautet ** "Ja! Da es viele Zweige gibt, erstellen wir für jeden Zweig einen Server und haben eine Datenbank!" ** Eigentlich ist es unmöglich ... Ich wollte denken, dass es unmöglich ist, aber ich bin auf ein ähnliches Ereignis gestoßen, also habe ich keine andere Wahl, als mich damit zu befassen.

Geben Sie DI auf, solange das Verbindungsziel in der Logik festgelegt ist

Da die zu verwendende Datenquelle nicht zu dem Zeitpunkt festgelegt wird, zu dem jeder Controller und jeder Dienst ausgeführt wird, wird der DI von Spring aufgegeben. Daher ist es notwendig, die folgenden Funktionen zu realisieren, die vom Framework absorbiert werden.

  1. Erstellen Sie eine SqlSession, wenn die zu verwendende Datenquelle festgelegt ist
  2. Generieren Sie eine Dao-Instanz mit eingespeisten Verbindungsinformationen
  3. Sitzungserfassung nach Abschluss der Transaktion

Implementiert 1 und 2 unter Bezugnahme auf die offizielle Website von Mybatis. 3 wird unter Verwendung der Funktion von AOP realisiert.

Erstellen Sie dynamisch eine SqlSession, um eine Dao-Instanz zu erstellen

Wenn Sie das offizielle Mybatis-Dokument lesen und gehorsam implementieren, sollten Sie grundsätzlich einige Punkte beachten. Es sollte ordnungsgemäß funktionieren, wenn Sie die erstellten Sitzungsinformationen für jede Anforderung (für jeden Thread) verwalten, nicht mehrmals dieselbe Sitzung erstellen und zumindest diese beiden Dinge beachten.

Erstellen Sie dieses Mal SqlSessionUtil zum Erstellen von SqlSession und SqlSessionManager zum Verwalten von SqlSession. Beide werden als DI verwendet.

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;
  }

}

Zusätzlich zu der Funktion zum Erstellen einer Datenquelle aus einer Einstellungsdatei in SqlSession wird eine Funktion zum Erstellen von Verbindungsinformationen aus einer Vorlage hinzugefügt, wenn keine Verbindungsinformationen vorhanden sind. Der SqlSessionManager, der wie ein einzelner Ton aussieht, verfügt intern über eine ThreadLocal-Variable, sodass er nicht von anderen Threads beeinflusst wird. Möglicherweise gibt es eine Methode zum Übergeben einer Instanz von SqlSessionManager unter Berücksichtigung des Falls der Stapelaktualisierung.

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 behält die Karte der erstellten SqlSession bei. Instanzerstellungszeitpunkt! = Thread-Startzeitpunkt, daher wird zu Beginn jeder Methode eine NULL-Prüfung als NPE-Gegenmaßnahme durchgeführt. Es könnte sinnvoll gewesen sein, zum Zeitpunkt des Empfangsempfangs mit AOP eine Instanz von LocalThread zu erstellen.

Schließen Sie die dynamisch erstellte SqlSession, nachdem die Transaktion abgeschlossen ist

Es ist gut, eine SqlSession zu erstellen. Wenn Sie jedoch die SqlSession schließen, die nach dem Start der Transaktion vor dem Ende der Transaktion erstellt wurde, funktioniert sie nicht ordnungsgemäß. Daher werde ich die Sitzung schließen, nachdem die deklarative Transaktion in AOP abgeschlossen wurde.

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();
  }

}

Die Reihenfolge wird von der mit der größten Nummer angegeben. Persönlich hat es eine hohe Priorität = Ich hatte das Gefühl, dass es zuerst ausgeführt wurde, daher fühlt es sich etwas seltsam an, aber wenn es in dieser Reihenfolge ausgeführt wird, wäre dies der Fall.

Es ist in Ordnung, wenn keine Ausnahme auftritt, auch wenn die Deklarationstransaktion nach der Operation mit der dynamisch erstellten SqlSession beendet wird.

Ich habe vorerst den normalen Betrieb bestätigt (Problem, über das ich mir Sorgen machen muss)

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

Ich mache mir Sorgen, dass immer Protokolle herauskommen. Es scheint, dass die Option zum Trennen der Verbindung während der Synchronisierung mit anderen Verbindungen beim Beenden der Verbindung deaktiviert ist.

Ich möchte auch jbossts-properties.xml so weit wie möglich in die yaml-Datei einfügen, aber es scheint nutzlos zu sein, da das WARN-Protokoll angezeigt wird.

Referenz

Narayana Spring Boot example
Narayana offizielle Probe. Da es auf Englisch nicht viele Informationen gibt, habe ich darauf basierend einen Versuch und Irrtum gemacht.

jbossts-properties.xml
Als ich Narayana von Maven bekam, gab es keine XML-Datei zum Einstellen, also habe ich sie von hier ausgeliehen

Quelle

Wenn Sie die Quelle sehen möchten, weil die Erklärung schwer zu verstehen ist, oder wenn Sie etwas möchten, das schnell verwendet werden kann, weil die Provision gut ist, klicken Sie bitte hier. https://github.com/suimyakunosoko/narayana-spring-boot-mybatis-postgresql

Recommended Posts

Verteilte Transaktion mit SpringBoot + PostgreSql + mybatis + NarayanaJTA
Bis zur Datenerfassung mit Spring Boot + MyBatis + PostgreSQL
Unterstützt Multi-Port mit SpringBoot
Ich habe einen CRUD-Test mit SpringBoot + MyBatis + DBUnit geschrieben (Teil 1)