Spring Boot 使用jta管理多数据源分布式事务
spring boot应用,通常我们在进行数据管理时,只操作一个数据源的表,需要开启事务管理,只需要在服务启动类增加@EnableTransactionManagement注解,在需要事务控制的方法增加@Transactional注解即可。
即便是多数据源切换情况下,在需要事务控制的方法只操作一个数据源,也可以满足。
但如果在需要事务控制的方法里,同时操作多个数据源的表,这时候仅仅是上述两个注解,不能保证事务的一致性(这是分布式事务的范畴)。比如,我要往A数据源的表插如一条记录,同时更新B数据源的表,并且需要进行事务控制,该怎么处理?
首先我们要理解,什么是分布式事务。分布式事务,是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上,分布式事务需要保证这些节点操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
针对上面的问题,我们使用atomikos+jta实现分布式事务统一管理,这在spring boot应用里非常简单,直接看示例代码:
1.首先引入atomikos依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jta-atomikos</artifactId> </dependency>
2.dao层我们使用spring jdbc来实现,所以还需要引入spring-jdbc依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <exclusions> <exclusion> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> </exclusion> </exclusions> </dependency>
注意,这里使用atomikos,所以不需要HikariCP连接池。
3.加入mysql的jdbc驱动:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
4.增加数据源的java配置:
import java.sql.SQLException; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import com.atomikos.jdbc.AtomikosDataSourceBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource; @Configuration public class DatasourceConfiguration { public static final String XA_DS1 = "xaDS01"; public static final String XA_DS2 = "xaDS02"; public static final String ATOMIKOS_DS1 = "atomikosDS01"; public static final String ATOMIKOS_DS2 = "atomikosDS02"; public static final String JDBC_TEMPLATE_DS1 = "jdbcTemplateDS01"; public static final String JDBC_TEMPLATE_DS2 = "jdbcTemplateDS02"; @Bean(name=XA_DS1) @ConfigurationProperties(prefix = "hzwei.datasource.ds1.xa") public MysqlXADataSource xaDS01() throws SQLException { MysqlXADataSource xaDataSource = new MysqlXADataSource(); xaDataSource.setPinGlobalTxToPhysicalConnection(true); xaDataSource.setPinGlobalTxToPhysicalConnection(true); return xaDataSource; } @Bean(name=XA_DS2) @ConfigurationProperties(prefix = "hzwei.datasource.ds2.xa") public MysqlXADataSource xaDS02() throws SQLException { MysqlXADataSource xaDataSource = new MysqlXADataSource(); xaDataSource.setPinGlobalTxToPhysicalConnection(true); xaDataSource.setPinGlobalTxToPhysicalConnection(true); return xaDataSource; } @Bean(name=ATOMIKOS_DS1) @ConfigurationProperties(prefix = "hzwei.datasource.xa") public AtomikosDataSourceBean atomikosDS01(@Qualifier(XA_DS1) MysqlXADataSource dataSource) throws SQLException { AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean(); atomikosDataSourceBean.setXaDataSource(dataSource); atomikosDataSourceBean.setUniqueResourceName(ATOMIKOS_DS1); return atomikosDataSourceBean; } @Bean(name=ATOMIKOS_DS2) @ConfigurationProperties(prefix = "hzwei.datasource.xa") public AtomikosDataSourceBean atomikosDS02(@Qualifier(XA_DS2) MysqlXADataSource dataSource) throws SQLException { AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean(); atomikosDataSourceBean.setXaDataSource(dataSource); atomikosDataSourceBean.setUniqueResourceName(ATOMIKOS_DS2); return atomikosDataSourceBean; } @Bean(name = JDBC_TEMPLATE_DS1) public JdbcTemplate jdbcTemplateDS01(@Qualifier(ATOMIKOS_DS1) DataSource dataSource) { return new JdbcTemplate(dataSource); } @Bean(name = JDBC_TEMPLATE_DS2) public JdbcTemplate jdbcTemplateDS02(@Qualifier(ATOMIKOS_DS2) DataSource dataSource) { return new JdbcTemplate(dataSource); } }
5.application.properties增加数据源配置项:
hzwei.datasource.ds1.xa.url=jdbc:mysql://localhost:3306/test_01?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull hzwei.datasource.ds1.xa.user=root hzwei.datasource.ds1.xa.password=root123 hzwei.datasource.ds2.xa.url=jdbc:mysql://localhost:3306/test_02?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true hzwei.datasource.ds2.xa.user=root hzwei.datasource.ds2.xa.password=root123 hzwei.datasource.xa.min-pool-size=5 hzwei.datasource.xa.max-pool-size=25 hzwei.datasource.xa.max-lifetime=20000 hzwei.datasource.xa.borrow-connection-timeout=30 hzwei.datasource.xa.login-timeout=30 hzwei.datasource.xa.maintenance-interval=60 hzwei.datasource.xa.max-idle-time=60
6.dao层:
@Repository("userDao1") public class UserDao1 implements IUserDao{ @Resource(name = DatasourceConfiguration.JDBC_TEMPLATE_DS1) private JdbcTemplate jdbcTemplate; @Override public void saveUser(User user){ //....saveUser... } } ***************************************************** @Repository("userDao2") public class UserDao2 implements IUserDao{ @Resource(name = DatasourceConfiguration.JDBC_TEMPLATE_DS2) private JdbcTemplate jdbcTemplate; @Override public void saveUser(User user){ //....saveUser... } }
7.service层:
@Service public class UserServiceImpl implements IUserService{ @Resource(name = "userDao1") private IUserDao userDao1; @Resource(name = "userDao2") private IUserDao userDao2; @Override @Transactional public void saveUser(User user){ userDao1.saveuser(user); userDao2.saveuser(user); //throw new RuntimeException("test rollback"); } }
PS:最后要注意一点,由于我们这里配置的是分布式数据源,没有配置默认数据源,所以需要在启动程序禁止DataSourceAutoConfiguration的初始化:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
当然,你也可以指定一个数据源增加@Primary注解(仅能注解一个数据源),这样不需要exclude = {DataSourceAutoConfiguration.class}。
否则,应用启动会报错。