最小化样例演示:JPA多数据源+分布式事务+SpringBoot+Web

13 篇文章 1 订阅
1 篇文章 1 订阅

概述

本文基于一个最小化Springboot web应用例子演示如何使用分布式事务,并设计了一个含有多数据源写入操作的事务,用于演示以下几种场景:

  1. 整个过程没有异常,预期事务在所有数据源上正常提交;
  2. 所有数据源写入完成后,提交前遇到会导致事务回滚的异常,预期事务在所有数据源上完全回滚;
  3. 所有数据源写入完成后,提交前遇到不会导致事务回滚的异常,预期事务在所有数据源上正常提交;

例子基本介绍

本例子应用主要使用以下工具或组件 :

  • maven
  • Spring boot 1.5.9 RELEASE
    • Spring web MVC 4.3.13 RELEASE
  • tomcat 8.5.23
  • JPA 1.11.9.RELEASE
  • Hibernate 5.0.12 Final
  • Atomikos 3.9.3
  • MySQL 5.6.28

为了实现对上述三种场景的模拟,本例子使用了两个数据源,数据库结构设计如下 :

  1. 采用两个MySQL数据库db_admindb_user,分别对应两个数据源中的一个;
  2. db_admin数据库中用一个表admin来保存管理员Admin账号信息;
  3. db_user数据库中用另外一张表user来保存用户User账号信息。

并且这样设计了业务逻辑 :

三个@Transactional注解的业务逻辑层方法,用于表示该方法的一次执行使用一个事务;

  • 方法1 : 往表adminuser表里面分别执行一条插入记录动作,然后方法正常结束;
    • 演示预期 : 该方法执行完成后,两条新记录出现在数据库表内,表示事务在所有数据源上均被成功提交;
  • 方法2 : 往表adminuser表里面分别执行一条插入记录动作,然后抛出一个异常,该异常被设计成发生时需要回滚事务;
    • 演示预期 : 该方法执行完成后,两个数据库表保持和该方法执行前一样,没有新记录被插入,表示事务在所有数据源上都因为异常被回滚;
  • 方法3 : 往表adminuser表里面分别执行一条插入记录动作,然后抛出一个异常,该异常被设计成发生时不需要回滚事务;
    • 演示预期 : 该方法执行完成后,两条新记录出现在数据库表内,表示事务在所有数据源上均被成功提交;

完成这样一个例子,需要以下几个步骤 :

  1. 准备数据库
  2. 引入数据库访问依赖
  3. 引入分布式事务依赖
  4. 配置多数据源和分布式事务处理组件
  5. 定义领域实体Entity
  6. 定义领域实体存储库Repository
  7. 实现业务服务逻辑
  8. 实现用于演示的Web控制器方法

具体实现

1 准备数据库

本例子在本机缺省端口上的MySQL数据库上创建了需要用到的两个数据库实例,用于对应两个数据源:

CREATE SCHEMA `db_admin` DEFAULT CHARACTER SET utf8 ;
CREATE SCHEMA `db_user` DEFAULT CHARACTER SET utf8 ;

2 引入数据库访问依赖

本例子基于MySQL 和 JPA,所以加入以下依赖包,缺省情况下,以下依赖包会隐含导入hibernate的依赖包。

        <!-- jpa support -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- MySQL 连接驱动依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

3 引入分布式事务依赖

        <!--基于atomikos的分布式事务支持-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>       

本例子应用也使用到了其他一些工具包,在maven项目的pom.xml文件中,对它们的依赖引入如下 :

        <!--一个工具插件,可以通过注解生成get/set/equals/toString等方法-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.10</version>
        </dependency>
        <!--apache commons lang 工具-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.1</version>
        </dependency>

4 配置多数据源和分布式事务处理组件

数据库参数配置

resources/application.yml 中,添加如下配置信息 :

spring:
  ################### DataSource Configuration ##########################
  datasource :
    db_user:
      unique-resource-name: db_user # The unique name used to identify the resource during recovery.
      max-pool-size: 5 # The maximum size of the pool.
      min-pool-size: 1  # The minimum size of the pool.
      max-life-time: 20000 # The time, in seconds, that a connection can be pooled for before being destroyed. 0 denotes no limit.
      max-idle-time : 60 # The time, in seconds, after which connections are cleaned up from the pool.
      maintenance-interval : 60 # The time, in seconds, between runs of the pool's maintenance thread.
      borrow-connection-timeout: 10000  # Timeout, in seconds, for borrowing connections from the pool.
      reap-timeout : 0 # The reap timeout, in seconds, for borrowed connections. 0 denotes no limit.
      test-query : SELECT 1 # SQL query or statement used to validate a connection before returning it.
      xa-data-source-class-name: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
      xa-properties:
        user: root
        password: your_password
        URL: jdbc:mysql://localhost:3306/db_user?useUnicode=true&characterEncoding=utf-8
        pinGlobalTxToPhysicalConnection : true #MySQL_XA_bug

    db_admin:
      unique-resource-name: db_admin # The unique name used to identify the resource during recovery.
      max-pool-size: 5 # The maximum size of the pool.
      min-pool-size: 1  # The minimum size of the pool.
      max-life-time: 20000 # The time, in seconds, that a connection can be pooled for before being destroyed. 0 denotes no limit.
      max-idle-time : 60 # The time, in seconds, after which connections are cleaned up from the pool.
      maintenance-interval : 60 # The time, in seconds, between runs of the pool's maintenance thread.
      borrow-connection-timeout: 10000  # Timeout, in seconds, for borrowing connections from the pool.
      reap-timeout : 0 # The reap timeout, in seconds, for borrowed connections. 0 denotes no limit.
      test-query : SELECT 1 # SQL query or statement used to validate a connection before returning it.
      xa-data-source-class-name: com.mysql.jdbc.jdbc2.optional.MysqlXADataSource
      xa-properties:
        user: root
        password: your_password
        URL: jdbc:mysql://localhost:3306/db_admin?useUnicode=true&characterEncoding=utf-8
        pinGlobalTxToPhysicalConnection : true #MySQL_XA_bug

resources目录下增加文件 jta.properties , 内容如下 :

com.atomikos.icatch.service=com.atomikos.icatch.standalone.UserTransactionServiceFactory
# https://www.atomikos.com/Documentation/KnownProblems#MySQL_XA_bug
# raised -5: invalid arguments were given for the XA operation
com.atomikos.icatch.serial_jta_transactions=false

定义支持分布式事务的事务管理器和用户事务Bean

package andy.tut.springboot.zero.config.xa;

import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.hibernate.engine.transaction.jta.platform.internal.AbstractJtaPlatform;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.jta.JtaTransactionManager;

import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;

/**
 * 参考来源 :
 * http://fabiomaffioletti.me/blog/2014/04/15/distributed-transactions-multiple-databases-spring-boot-spring-data-jpa-atomikos/
 * <p>
 * Created by Andy Zhang on 2017/12/20.
 */
@Configuration
@ComponentScan
@EnableTransactionManagement
public class XATransactionConfig {

    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(true);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        return hibernateJpaVendorAdapter;
    }

    @Bean(name = "userTransaction")
    public UserTransaction userTransaction() throws Throwable {
        UserTransactionImp userTransactionImp = new UserTransactionImp();
        userTransactionImp.setTransactionTimeout(10000);
        return userTransactionImp;
    }

    @Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
    public TransactionManager atomikosTransactionManager() throws Throwable {
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        userTransactionManager.setForceShutdown(false);

        AtomikosJtaPlatform.transactionManager = userTransactionManager;

        return userTransactionManager;
    }

    @Bean(name = "transactionManager")
    @DependsOn({"userTransaction", "atomikosTransactionManager"})
    public PlatformTransactionManager transactionManager() throws Throwable {
        UserTransaction userTransaction = userTransaction();

        AtomikosJtaPlatform.transaction = userTransaction;

        TransactionManager atomikosTransactionManager = atomikosTransactionManager();
        return new JtaTransactionManager(userTransaction, atomikosTransactionManager);
    }

    public static class AtomikosJtaPlatform extends AbstractJtaPlatform {

        private static final long serialVersionUID = 20171220L;

        static TransactionManager transactionManager;
        static UserTransaction transaction;

        @Override
        protected TransactionManager locateTransactionManager() {
            return transactionManager;
        }

        @Override
        protected UserTransaction locateUserTransaction() {
            return transaction;
        }
    }
}

定义db_user数据源和相应的实体管理器Bean

package andy.tut.springboot.zero.config.xa;

import com.atomikos.jdbc.AtomikosDataSourceBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy;
import org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;

import javax.sql.DataSource;
import java.util.HashMap;

/**
 * Created by Andy Zhang on 2017/12/20.
 */
@Configuration
@EnableJpaRepositories(
        entityManagerFactoryRef = "userEntityManager",//实体管理引用
        transactionManagerRef = "transactionManager",//事务管理引用
        basePackages = {"andy.tut.springboot.zero.repository.user"}) //设置为目标JpaRepository所在包
public class UserDataSourceConfigurer {

    @Autowired
    private JpaVendorAdapter jpaVendorAdapter;

    //定义数据源
    @Bean(name = "userDataSource", initMethod = "init", destroyMethod = "close")
    @ConfigurationProperties(prefix = "spring.datasource.db_user")//application.yml文件内配置数据源的前缀
    public DataSource dataSource() {
        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        return xaDataSource;
    }

    //定义访问用户数据库的entityManager
    @Bean(name = "userEntityManager")
    @DependsOn("transactionManager")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws Throwable {

        HashMap<String, Object> properties = new HashMap<String, Object>();
        properties.put("hibernate.transaction.jta.platform", XATransactionConfig.AtomikosJtaPlatform.class.getName());
        properties.put("javax.persistence.transactionType", "JTA");
        properties.put("hibernate.physical_naming_strategy", SpringPhysicalNamingStrategy.class.getName());
        properties.put("hibernate.implicit_naming_strategy", SpringImplicitNamingStrategy.class.getName());

        LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
        entityManager.setJtaDataSource(dataSource());
        entityManager.setJpaVendorAdapter(jpaVendorAdapter);
        // 设置为该EntityManager所要管理的Entity类所在的包
        entityManager.setPackagesToScan("andy.tut.springboot.zero.domain.user");
        entityManager.setPersistenceUnitName("userPersistenceUnit");
        entityManager.setJpaPropertyMap(properties);
        return entityManager;
    }
}

定义db_admin数据源和相应的实体管理器Bean

package andy.tut.springboot.zero.config.xa;

import com.atomikos.jdbc.AtomikosDataSourceBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy;
import org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;

import javax.sql.DataSource;
import java.util.HashMap;

/**
 * Created by Andy Zhang on 2017/12/20.
 */
@Configuration
@EnableJpaRepositories(
        entityManagerFactoryRef = "adminEntityManager",//实体管理引用
        transactionManagerRef = "transactionManager",//事务管理引用
        basePackages = {"andy.tut.springboot.zero.repository.admin"}) //设置为目标JpaRepository所在包
public class AdminDataSourceConfigurer {

    @Autowired
    private JpaVendorAdapter jpaVendorAdapter;

    //定义数据源
    @Bean(name = "adminDataSource", initMethod = "init", destroyMethod = "close")
    @ConfigurationProperties(prefix = "spring.datasource.db_admin")//application.yml文件内配置数据源的前缀
    public DataSource dataSource() {
        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        return xaDataSource;
    }

    //定义访问管理员数据库的entityManager
    @Bean(name = "adminEntityManager")
    @DependsOn("transactionManager")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws Throwable {

        HashMap<String, Object> properties = new HashMap<String, Object>();
        properties.put("hibernate.transaction.jta.platform", XATransactionConfig.AtomikosJtaPlatform.class.getName());
        properties.put("javax.persistence.transactionType", "JTA");
        properties.put("hibernate.physical_naming_strategy", SpringPhysicalNamingStrategy.class.getName());
        properties.put("hibernate.implicit_naming_strategy", SpringImplicitNamingStrategy.class.getName());

        LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean();
        entityManager.setJtaDataSource(dataSource());
        entityManager.setJpaVendorAdapter(jpaVendorAdapter);
        // 设置为该EntityManager所要管理的Entity类所在的包
        entityManager.setPackagesToScan("andy.tut.springboot.zero.domain.admin");
        entityManager.setPersistenceUnitName("adminPersistenceUnit");
        entityManager.setJpaPropertyMap(properties);
        return entityManager;
    }
}

5 定义领域实体Entity

package andy.tut.springboot.zero.domain.admin;

import lombok.Data;
import lombok.EqualsAndHashCode;

import javax.persistence.*;

/**
 * Created by Andy Zhang on 2017/12/20.
 */

@Entity
@Table(name = "admin")
@Data
@EqualsAndHashCode(exclude = {"id"})
public class Admin {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(name = "name", nullable = false)
    private String name;

    @Column(name = "age", nullable = false)
    private Integer age;

}

package andy.tut.springboot.zero.domain.user;

import lombok.Data;
import lombok.EqualsAndHashCode;

import javax.persistence.*;

/**
 * Created by Andy Zhang on 2017/12/20.
 */
@Entity
@Table(name = "user")
@Data
@EqualsAndHashCode(exclude = {"id"})
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(name = "name", nullable = false)
    private String name;

    @Column(name = "age", nullable = false)
    private Integer age;

}
```java
## 6 定义领域实体存储库`Repository`
```java
package andy.tut.springboot.zero.repository.admin;


import andy.tut.springboot.zero.domain.admin.Admin;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by Andy Zhang on 2017/12/20.
 */
public interface AdminRepository extends JpaRepository<Admin, Integer> {

}

package andy.tut.springboot.zero.repository.user;


import andy.tut.springboot.zero.domain.user.User;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by Andy Zhang on 2017/12/20.
 */
public interface UserRepository extends JpaRepository<User, Integer> {

}

7 实现业务服务逻辑

package andy.tut.springboot.zero.service;

import andy.tut.springboot.zero.domain.admin.Admin;
import andy.tut.springboot.zero.repository.admin.AdminRepository;
import andy.tut.springboot.zero.exception.NoRollbackException;
import andy.tut.springboot.zero.exception.RollbackException;
import andy.tut.springboot.zero.domain.user.User;
import andy.tut.springboot.zero.repository.user.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Created by Andy Zhang on 2017/12/20.
 */
@Service
public class XATestService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private AdminRepository adminRepository;

    /**
     * 模拟场景 : 使用一个事务,往两个数据源中分别写入记录,没有异常发生,记录分别顺利提交
     *
     * @param user
     * @param admin
     */
    @Transactional
    public void store(User user, Admin admin) {
        userRepository.save(user);
        adminRepository.save(admin);
    }

    /**
     * 模拟场景 : 往两个数据源中分别写入记录,方法结束时出现会导致数据库回滚操作的异常,导致两个数据源中的数据写入都被回滚
     *
     * @param user  要添加的用户记录
     * @param admin 要添加的管理员记录
     * @throws RollbackException
     */
    // 使用事务,并且遇到RollbackException异常时回滚数据库操作
    @Transactional(rollbackFor = RollbackException.class)
    public void storeWithRollbackException(User user, Admin admin) throws RollbackException {
        userRepository.save(user);
        adminRepository.save(admin);
        // 抛出一个会导致数据库事务回滚的异常,用于模拟实际环境中出现某种异常,事务需要回滚的情况
        throw new RollbackException();
    }

    /**
     * 模拟场景 : 往两个数据源中分别写入记录,方法结束时出现不会导致数据库回滚操作的异常,最终两个数据源中的数据写入都被提交
     *
     * @param user  要添加的用户记录
     * @param admin 要添加的管理员记录
     * @throws NoRollbackException
     */
    // 使用事务,并且遇到RollbackException异常时回滚数据库操作,遇到NoRollbackException异常时不回滚数据库操作
    @Transactional(noRollbackFor = NoRollbackException.class, rollbackFor = RollbackException.class)
    public void storeWithNoRollbackException(User user, Admin admin) throws NoRollbackException {
        userRepository.save(user);
        adminRepository.save(admin);
        // 抛出一个不会导致数据库事务回滚的异常,用于模拟实际环境中即使出现某种异常,事务也需要正常提交的情况
        throw new NoRollbackException();
    }

}

以上业务逻辑层的实现使用到了两个自定义的异常,其实现如下 :

package andy.tut.springboot.zero.exception;

/**
 * Created by Andy Zhang on 2017/12/20.
 */
public class NoRollbackException extends Exception {

    private static final long serialVersionUID = 20171220L;


}
package andy.tut.springboot.zero.exception;

/**
 * Created by Andy Zhang on 2017/12/20.
 */
public class RollbackException extends Exception {

    private static final long serialVersionUID = 20171220L;

}

8 实现用于演示的Web控制器方法

package andy.tut.springboot.zero.web;

import andy.tut.springboot.zero.domain.admin.Admin;
import andy.tut.springboot.zero.domain.user.User;
import andy.tut.springboot.zero.exception.NoRollbackException;
import andy.tut.springboot.zero.exception.RollbackException;
import andy.tut.springboot.zero.service.XATestService;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Random;

/**
 * Created by Andy Zhang on 2017/12/20.
 */
@RestController
public class XATestController {
    @Autowired
    XATestService testService;

    @RequestMapping("/store")
    public String store() {
        Admin admin = new Admin();
        admin.setName(RandomStringUtils.randomAlphabetic(6));
        admin.setAge(new Random().nextInt(100));

        User user = new User();
        user.setName(RandomStringUtils.randomAlphabetic(6));
        user.setAge(new Random().nextInt(100));
        testService.store(user, admin);

        return "success";
    }

    /**
     * @return
     */
    @RequestMapping("/store-with-rollback-exception")
    public String storeWithRollbackException() {
        Admin admin = new Admin();
        admin.setName(RandomStringUtils.randomAlphabetic(6));
        admin.setAge(new Random().nextInt(100));

        User user = new User();
        user.setName(RandomStringUtils.randomAlphabetic(6));
        user.setAge(new Random().nextInt(100));
        try {
            testService.storeWithRollbackException(user, admin);
        } catch (RollbackException e) {
            return e.getClass().getSimpleName();
        }

        return "not expected";
    }

    /**
     * @return
     */
    @RequestMapping("/store-with-no-rollback-exception")
    public String storeWithNoRollbackException() {
        Admin admin = new Admin();
        admin.setName(RandomStringUtils.randomAlphabetic(6));
        admin.setAge(new Random().nextInt(100));

        User user = new User();
        user.setName(RandomStringUtils.randomAlphabetic(6));
        user.setAge(new Random().nextInt(100));
        try {
            testService.storeWithNoRollbackException(user, admin);
        } catch (NoRollbackException e) {
            return e.getClass().getSimpleName();
        }

        return "not expected";
    }
}

场景演示

启动该应用,缺省情况下,他应该启动在本机8080端口上。

场景1

访问如下地址 :

http://localhost:8080/store

预期服务器响应内容 :

success

数据库变化 : 有

一切正常,所调用的方法中的记录插入动作正常执行并且所在事务成功提交,所以两个数据源的数据表内会分别出现一条新增的记录。

场景2

http://localhost:8080/store-with-rollback-exception

预期服务器响应内容 :

RollbackException

数据库变化 : 无

有异常发生,并且该异常被标记成发生时需要回滚事务,所以两个数据源的数据表会保持和访问该功能前一样,
不会有最终的数据记录插入发生,也就是说,相应的业务逻辑层中的数据记录插入动作,因为事务回滚被取消了。

场景3

访问如下地址 :

http://localhost:8080/store-with-no-rollback-exception

预期服务器响应内容 :

NoRollbackException

数据库变化 : 有

虽然有异常发生,但是该异常标记成不需要回滚事务,所以两个数据源的数据表内会分别出现一条新增的记录。

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值