概述
本文基于一个最小化Springboot web
应用例子演示如何使用分布式事务,并设计了一个含有多数据源写入操作的事务,用于演示以下几种场景:
- 整个过程没有异常,预期事务在所有数据源上正常提交;
- 所有数据源写入完成后,提交前遇到会导致事务回滚的异常,预期事务在所有数据源上完全回滚;
- 所有数据源写入完成后,提交前遇到不会导致事务回滚的异常,预期事务在所有数据源上正常提交;
例子基本介绍
本例子应用主要使用以下工具或组件 :
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
为了实现对上述三种场景的模拟,本例子使用了两个数据源,数据库结构设计如下 :
- 采用两个
MySQL
数据库db_admin
和db_user
,分别对应两个数据源中的一个; - 在
db_admin
数据库中用一个表admin
来保存管理员Admin
账号信息; - 在
db_user
数据库中用另外一张表user
来保存用户User
账号信息。
并且这样设计了业务逻辑 :
三个@Transactional
注解的业务逻辑层方法,用于表示该方法的一次执行使用一个事务;
- 方法1 : 往表
admin
和user
表里面分别执行一条插入记录动作,然后方法正常结束;- 演示预期 : 该方法执行完成后,两条新记录出现在数据库表内,表示事务在所有数据源上均被成功提交;
- 方法2 : 往表
admin
和user
表里面分别执行一条插入记录动作,然后抛出一个异常,该异常被设计成发生时需要回滚事务;- 演示预期 : 该方法执行完成后,两个数据库表保持和该方法执行前一样,没有新记录被插入,表示事务在所有数据源上都因为异常被回滚;
- 方法3 : 往表
admin
和user
表里面分别执行一条插入记录动作,然后抛出一个异常,该异常被设计成发生时不需要回滚事务;- 演示预期 : 该方法执行完成后,两条新记录出现在数据库表内,表示事务在所有数据源上均被成功提交;
完成这样一个例子,需要以下几个步骤 :
- 准备数据库
- 引入数据库访问依赖
- 引入分布式事务依赖
- 配置多数据源和分布式事务处理组件
- 定义领域实体
Entity
- 定义领域实体存储库
Repository
- 实现业务服务逻辑
- 实现用于演示的
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
数据库变化 : 有
虽然有异常发生,但是该异常标记成不需要回滚事务,所以两个数据源的数据表内会分别出现一条新增的记录。