Распределенная транзакция с использованием Spring и Spring jdbctemplate

У меня есть два источника данных с двумя схемами в oracle, я выполняю unittest, но он не работает. Я хочу, чтобы если вторая транзакция не удалась, она должна откатить первую транзакцию. Ниже мой код.

package com.test.db;

import static org.junit.Assert.assertEquals;

import java.util.Date;

import javax.sql.DataSource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.transaction.AfterTransaction;
import org.springframework.test.context.transaction.BeforeTransaction;
import org.springframework.transaction.annotation.Transactional;

//@EnableTransactionManagement
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/META-INF/spring/data-source-context.xml")
public class MultipleDatasourceTests {

    private JdbcTemplate jdbcTemplate;
    private JdbcTemplate otherJdbcTemplate;

    @Autowired
    public void setDataSources(@Qualifier("dataSource") DataSource dataSource,
            @Qualifier("otherDataSource") DataSource otherDataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.otherJdbcTemplate = new JdbcTemplate(otherDataSource);
    }

    @BeforeTransaction
    public void clearData() {
        jdbcTemplate.update("delete from T_ORACLE1");
        otherJdbcTemplate.update("delete from T_ORACLE1");
    }

    @AfterTransaction
    public void checkPostConditions() {

        int count = jdbcTemplate.queryForInt("select count(*) from T_ORACLE1");
        // This change was rolled back by the test framework
        assertEquals(0, count);

        count = otherJdbcTemplate.queryForInt("select count(*) from T_ORACLE1");
        // This rolls back as well if the connections are managed together
        assertEquals(0, count);

    }


    /**
     * Vanilla test case for two inserts into two data sources. Both should roll
     * back.
     * 
     * @throws Exception
     */
    @Transactional
    @Test
    public void testInsertIntoTwoDataSources() throws Exception {
        jdbcTemplate.update("delete from T_ORACLE1");
        otherJdbcTemplate.update("delete from T_ORACLE2");

        int count = jdbcTemplate.update(
                "INSERT into T_ORACLE1 (id,name,foo_date) values (?,?,null)", 0,
                "foo");
        assertEquals(1, count);

        count = otherJdbcTemplate
                .update(
                        "INSERT into T_ORACLE2 (id,operation,name,audit_date) values (?,?,?,?)",
                        1, "INSERT", "foo", new Date());
        assertEquals(1, count);

    }

    /**
     * Shows how to check the operation on the inner data source to see if it
     * has already been committed, and if it has do something different, instead
     * of just hitting a {@link DataIntegrityViolationException}.
     * 
     * @throws Exception
     */
    @Transactional
    @Test
    public void testInsertWithCheckForDuplicates() throws Exception {


        int count = jdbcTemplate.update(
                "INSERT into T_ORACLE1 (id,name,foo_date) values (?,?,null)", 0,
                "foo");
        assertEquals(1, count);

        count = otherJdbcTemplate.update(
                        "UPDATE T_ORACLE2 set operation=?, name=?, audit_date=? where id=?",
                        "UPDATE", "foo", new Date(), 0);

        if (count == 0) {
            count = otherJdbcTemplate.update(
                            "INSERT into T_ORACLE2 (id,operation,name,audit_date) values (?,?,?,?)",
                            0, "INSERT", "foo", new Date());
        }

        assertEquals(1, count);

    }
}

XML

<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns = "http://www.springframework.org/schema/beans"
    xmlns:tx = "http://www.springframework.org/schema/tx" xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context = "http://www.springframework.org/schema/context"
    xsi:schemaLocation = "http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
    http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-2.5.xsd">

    <context:annotation-config />
    <context:component-scan base-package = "com.test.*"/>

    <bean id = "dataSource"
        class = "org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name = "driverClassName" value = "oracle.jdbc.driver.OracleDriver" />
        <property name = "url" value = "jdbc:oracle:thin:@//localhost:1521/TESTDB1" />
        <property name = "username" value = "ORACLE1"/>
        <property name = "password" value = "ORACLE1"/>
    </bean>

    <bean id = "otherDataSource"
        class = "org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name = "driverClassName" value = "oracle.jdbc.driver.OracleDriver" />
        <property name = "url" value = "jdbc:oracle:thin:@//localhost:1521/TESTDB2" />
        <property name = "username" value = "ORACLE2"/>
        <property name = "password" value = "ORACLE2"/>
    </bean>

    <bean id = "transactionManager" class = "com.test.db.MultiTransactionManager">
        <property name = "transactionManagers">
            <list>
                <bean
                    class = "org.springframework.jdbc.datasource.DataSourceTransactionManager">
                    <property name = "dataSource" ref = "dataSource" />
                </bean>
                <bean
                    class = "org.springframework.jdbc.datasource.DataSourceTransactionManager">
                    <property name = "dataSource" ref = "otherDataSource" />
                </bean>
            </list>
        </property>
    </bean>

</beans>


package com.test.db;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
import org.springframework.transaction.support.DefaultTransactionStatus;


public class MultiTransactionManager extends
        AbstractPlatformTransactionManager {

    private List<PlatformTransactionManager> transactionManagers = new ArrayList<PlatformTransactionManager>();
    private ArrayList<PlatformTransactionManager> reversed;

    public void setTransactionManagers(
            List<PlatformTransactionManager> transactionManagers) {
        this.transactionManagers = transactionManagers;
        reversed = new ArrayList<PlatformTransactionManager>(
                transactionManagers);
        Collections.reverse(reversed);
    }

    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition)
            throws TransactionException {
        @SuppressWarnings("unchecked")
        List<DefaultTransactionStatus> list = (List<DefaultTransactionStatus>) transaction;
        for (PlatformTransactionManager transactionManager : transactionManagers) {
            DefaultTransactionStatus element = (DefaultTransactionStatus) transactionManager
                    .getTransaction(definition);
            list.add(0, element);
        }
    }

    @Override
    protected void doCommit(DefaultTransactionStatus status)
            throws TransactionException {
        @SuppressWarnings("unchecked")
        List<DefaultTransactionStatus> list = (List<DefaultTransactionStatus>) status
                .getTransaction();
        int i = 0;
        for (PlatformTransactionManager transactionManager : reversed) {
            TransactionStatus local = list.get(i++);
            try {
                transactionManager.commit(local);
            } catch (TransactionException e) {
                logger.error("Error in commit", e);
                // Rollback will ensue as long as rollbackOnCommitFailure=true
                throw e;
            }
        }
    }

    @Override
    protected Object doGetTransaction() throws TransactionException {
        return new ArrayList<DefaultTransactionStatus>();
    }

    @Override
    protected void doRollback(DefaultTransactionStatus status)
            throws TransactionException {
        @SuppressWarnings("unchecked")
        List<DefaultTransactionStatus> list = (List<DefaultTransactionStatus>) status
                .getTransaction();
        int i = 0;
        TransactionException lastException = null;
        for (PlatformTransactionManager transactionManager : reversed) {
            TransactionStatus local = list.get(i++);
            try {
                transactionManager.rollback(local);
            } catch (TransactionException e) {
                // Log exception and try to complete rollback 
                lastException = e;
                logger.error("Error in rollback", e);
            }
        }
        if (lastException!=null) {
            throw lastException;
        }
    }

}

Мой pom.xml

<project xmlns = "http://maven.apache.org/POM/4.0.0" xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.springsource.open</groupId>
    <artifactId>spring-best-db-db</artifactId>
    <version>2.0.0.CI-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>Spring Best Efforts DB-DB</name>
    <description><![CDATA[Sample project showing multi DataSource transaction 
processing with Spring using best efforts 1PC.
]]> </description>
    <properties>
        <maven.test.failure.ignore>true</maven.test.failure.ignore>
        <spring.framework.version>4.1.4.RELEASE</spring.framework.version>
    </properties>
    <profiles>
        <profile>
            <id>strict</id>
            <properties>
                <maven.test.failure.ignore>false</maven.test.failure.ignore>
            </properties>
        </profile>
        <profile>
            <id>fast</id>
            <properties>
                <maven.test.skip>true</maven.test.skip>
            </properties>
        </profile>
    </profiles>
    <dependencies>
        <dependency>
            <groupId>hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
            <version>1.8.0.7</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1</version>
            <exclusions>
                <exclusion>
                    <groupId>avalon-framework</groupId>
                    <artifactId>avalon-framework</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>logkit</groupId>
                    <artifactId>logkit</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>javax.servlet</groupId>
                    <artifactId>servlet-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.derby</groupId>
            <artifactId>derby</artifactId>
            <version>10.2.1.6</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.9</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.easymock</groupId>
            <artifactId>easymock</artifactId>
            <version>2.4</version>
            <scope>test</scope>
        </dependency>
        <!-- Spring Dependencies -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.framework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.framework.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>${spring.framework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>${spring.framework.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.framework.version}</version>
        </dependency>

        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>ojdbc8</artifactId>
            <version>12.1.0.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.github.marcus-nl.btm/btm-spring -->
        <dependency>
            <groupId>com.github.marcus-nl.btm</groupId>
            <artifactId>btm-spring</artifactId>
            <version>3.0.0-mk1</version>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>6.0</version>
        </dependency>
    </dependencies>

</project>

TABLE IN ORACLE1 database
create table T_ORACLE1 (
    id integer not null primary key,
    name varchar(80),
    foo_date timestamp
);

Таблица в базе данных ORACLE2

create table T_ORACLE2 (
    id integer not null primary key,
    operation varchar(20),
    name varchar(80),
    audit_date timestamp
);

Я погуглил, чтобы использовать BitronixTransactionManager, но не понял, как настроить для двух источников данных.

Кажется, мы можем использовать спящий режим, но я не хочу использовать спящий режим. Я хочу либо jdbctemplate с запросом sql, либо простой jdbc.

Я получаю ошибку

java.lang.IllegalStateException: не удается активировать синхронизацию транзакций - уже активна в org.springframework.transaction.support.TransactionSynchronizationManager.initSynchronization (TransactionSynchronizationManager.java:270)

Я новичок в распределенной транзакции. Не могли бы вы мне помочь? Можно ли использовать два источника данных, но один менеджер транзакций?

Вы не можете избежать распределенной транзакции? См. stackoverflow.com/a/887426/479159

Andrei Damian-Fekete 28.08.2018 11:32
2
1
684
1

Ответы 1

Вы должны использовать источники данных XA, если хотите достичь распределенных транзакций, и менеджер транзакций также должен поддерживать транзакции XA.

Использование диспетчера транзакций Bitronix должно сделать это, но вы также должны использовать источник данных XA: похоже, что реализация на основе Oracle доступна в драйвере Oracle JDBC (см. https://docs.oracle.com/cd/E17904_01/web.1111/e13731/thirdpartytx.htm#WLJTA266).

Вы можете найти пример конфигурации Spring для Bitronix здесь: https://www.snip2code.com/Snippet/652599/Example-distributed-XA-transaction-confi/, просто не забудьте настроить свойства источников данных для использования oracle.jdbc.xa.client.OracleXADataSource вместо PostgreSQL.

Однако обратите внимание, что XA / распределенные транзакции не являются серебряной пулей и не смогут справиться с некоторыми классами проблем (например, сбои сети); вам действительно стоит подумать о возможных альтернативах, прежде чем идти по этому пути.

Я знаю, что мы можем использовать XAdatasouce, но я не хочу использовать jpa, я хочу использовать только JDBC или springtemplate.

Passionate developer 28.08.2018 18:24

Вы можете просто игнорировать использование части, связанной с Hibernate, в указанном файле конфигурации Spring (просто выберите элементы конфигурации источника данных и диспетчера транзакций).

Guillaume G. 29.08.2018 10:03

Другие вопросы по теме