JPA多数据源分布式事务处理

前言

多数据源的事务处理是个老生常谈的话题,跨两个数据源的事务管理也算是分布式事务的范畴,在同一个JVM里处理多数据源的事务,比较经典的处理方案是JTA(基于XA协议建模的java标准事务抽象)+XA(XA事务协议),常见的JTA实现框架有Atomikos、Bitronix、Narayana,Spring对这些框架都有组件封装,基本可以做到开箱即用程度。本文除了分享XA事务方案外,提供了一种新的多数据源事务解决思路和视角。

问题背景

在解决mysql字段脱敏处理时,结合sharding-jdbc的脱敏组件功能,为了sql兼容和最小化应用改造,博主给出了一个多数据源融合的字段脱敏解决方案(只把包含脱敏字段表的操作走sharding-jdbc脱敏代理数据源)。这个方案解决了问题的同时,带来了一个新的问题,数据源的事务是独立的,正如我文中所述《JPA项目多数据源模式整合sharding-jdbc实现数据脱敏》,在spring上下文中,每个数据源对应一个独立的事务管理器,默认的事务管理器的数据源就用业务本身的数据源,所以需要加密的业务使用时,需要指定@Transactional注解里的事务管理器名称为脱敏对应的事务管理器名称。简单的业务场景这样用也就没有问题了,但是一般的业务场景总有一个事务覆盖两个数据源的操作,这个时候单指定哪个事务管理器都不行,so,这里需要一种多数据源的事务管理器。

XA事务方案

XA协议采用2PC(两阶段提交)的方式来管理分布式事务。XA接口提供资源管理器与事务管理器之间进行通信的标准接口。在JDBC的XA事务相关api抽象里,相关接口定义如下

XADataSource,XA协议数据源

1public interface XADataSource extends CommonDataSource { 2 /** 3 * 尝试建立物理数据库连接,使用给定的用户名和密码。返回的连接可以在分布式事务中使用 4 */ 5 XAConnection getXAConnection() throws SQLException; 6 //省略getLogWriter等非关键方法 7 } 8

XAConnection

1public interface XAConnection extends PooledConnection { 2 3 /** 4 * 检索一个{@code XAResource}对象,事务管理器将使用该对象管理该{@code XAConnection}对象在分布式事务中的事务行为 5 */ 6 javax.transaction.xa.XAResource getXAResource() throws SQLException; 7} 8

XAResource

1public interface XAResource { 2 /** 3 * 提交xid指定的全局事务 4 */ 5 void commit(Xid xid, boolean onePhase) throws XAException; 6 7 /** 8 * 结束代表事务分支执行的工作。资源管理器从指定的事务分支中分离XA资源,并让事务完成。 9 */ 10 void end(Xid xid, int flags) throws XAException; 11 12 /** 13 * 通知事务管理器忽略此xid事务分支 14 */ 15 void forget(Xid xid) throws XAException; 16 17 /** 18 * 判断是否同一个资源管理器 19 */ 20 boolean isSameRM(XAResource xares) throws XAException; 21 22 /** 23 * 指定xid事务准备阶段 24 */ 25 int prepare(Xid xid) throws XAException; 26 27 /** 28 * 从资源管理器获取准备好的事务分支的列表。事务管理器在恢复期间调用此方法, 29 * 以获取当前处于准备状态或初步完成状态的事务分支的列表。 30 */ 31 Xid[] recover(int flag) throws XAException; 32 33 /** 34 * 通知资源管理器回滚代表事务分支完成的工作。 35 */ 36 void rollback(Xid xid) throws XAException; 37 38 /** 39 * 代表xid中指定的事务分支开始工作。 40 */ 41 void start(Xid xid, int flags) throws XAException; 42 43 //省略非关键方法 44} 45

相比较普通的事务管理,JDBC的XA协议管理多了一个XAResource资源管理器,XA事务相关的行为(开启、准备、提交、回滚、结束)都由这个资源管理器来控制,这些都是框架内部的行为,体现在开发层面提供的数据源也变成了XADataSource。而JTA的抽象里,定义了UserTransaction、TransactionManager。想要使用JTA事务,必须先实现这两个接口。所以,如果我们要使用JTA+XA控制多数据源的事务,在sprign boot里以Atomikos为例,

引入Atomikos依赖

1 <dependency> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-jta-atomikos</artifactId> 4 </dependency> 5

spring boot已经帮我们把XA事务管理器自动装载类定义好了,如:

创建JTA事务管理器

1@Configuration(proxyBeanMethods = false) 2@EnableConfigurationProperties({ AtomikosProperties.class, JtaProperties.class }) 3@ConditionalOnClass({ JtaTransactionManager.class, UserTransactionManager.class }) 4@ConditionalOnMissingBean(PlatformTransactionManager.class) 5class AtomikosJtaConfiguration { 6 7 @Bean(initMethod = "init", destroyMethod = "shutdownWait") 8 @ConditionalOnMissingBean(UserTransactionService.class) 9 UserTransactionServiceImp userTransactionService(AtomikosProperties atomikosProperties, 10 JtaProperties jtaProperties) { 11 Properties properties = new Properties(); 12 if (StringUtils.hasText(jtaProperties.getTransactionManagerId())) { 13 properties.setProperty("com.atomikos.icatch.tm_unique_name", jtaProperties.getTransactionManagerId()); 14 } 15 properties.setProperty("com.atomikos.icatch.log_base_dir", getLogBaseDir(jtaProperties)); 16 properties.putAll(atomikosProperties.asProperties()); 17 return new UserTransactionServiceImp(properties); 18 } 19 @Bean(initMethod = "init", destroyMethod = "close") 20 @ConditionalOnMissingBean(TransactionManager.class) 21 UserTransactionManager atomikosTransactionManager(UserTransactionService userTransactionService) throws Exception { 22 UserTransactionManager manager = new UserTransactionManager(); 23 manager.setStartupTransactionService(false); 24 manager.setForceShutdown(true); 25 return manager; 26 } 27 @Bean 28 @ConditionalOnMissingBean(XADataSourceWrapper.class) 29 AtomikosXADataSourceWrapper xaDataSourceWrapper() { 30 return new AtomikosXADataSourceWrapper(); 31 } 32 @Bean 33 JtaTransactionManager transactionManager(UserTransaction userTransaction, TransactionManager transactionManager, 34 ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) { 35 JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(userTransaction, transactionManager); 36 transactionManagerCustomizers.ifAvailable((customizers) -> customizers.customize(jtaTransactionManager)); 37 return jtaTransactionManager; 38 } 39 、、、、、、、、、、 40} 41

显然,想要使用XA事务,除了需要提供UserTransaction、TransactionManager的实现。还必须要有一个XADataSource,而sharding-jdbc代理的数据源是DataSource的,我们需要将XADataSource包装成普通的DataSource,spring已经提供了一个AtomikosXADataSourceWrapper的XA数据源包装器,而且在AtomikosJtaConfiguration里已经注册到Spring上下文中,所以我们在自定义数据源时可以直接注入包装器实例,然后,因为是JPA环境,所以在创建EntityManagerFactory实例时,需要指定JPA的事务管理类型为JTA,综上,普通的业务默认数据源配置如下:

1/** 2 * @author: kl @kailing.pub 3 * @date: 2020/5/18 4 */ 5@Configuration 6@EnableConfigurationProperties({JpaProperties.class, DataSourceProperties.class}) 7public class DataSourceConfiguration{ 8 9 @Primary 10 @Bean 11 public DataSource dataSource(AtomikosXADataSourceWrapper wrapper, DataSourceProperties dataSourceProperties) throws Exception { 12 MysqlXADataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(MysqlXADataSource.class).build(); 13 return wrapper.wrapDataSource(dataSource); 14 } 15 16 @Primary 17 @Bean(initMethod = "afterPropertiesSet") 18 public LocalContainerEntityManagerFactoryBean entityManagerFactory(JpaProperties jpaProperties, DataSource dataSource, EntityManagerFactoryBuilder factoryBuilder) { 19 return factoryBuilder.dataSource(dataSource) 20 .packages(Constants.BASE_PACKAGES) 21 .properties(jpaProperties.getProperties()) 22 .persistenceUnit("default") 23 .jta(true) 24 .build(); 25 } 26 27 @Bean 28 @Primary 29 public EntityManager entityManager(EntityManagerFactory entityManagerFactory){ 30 //必须使用SharedEntityManagerCreator创建SharedEntityManager实例,否则SimpleJpaRepository中的事务不生效 31 return SharedEntityManagerCreator.createSharedEntityManager(entityManagerFactory); 32 } 33} 34 35

sharding-jdbc加密数据源和普通业务数据源其实是同一个数据源,只是走加解密逻辑的数据源需要被sharding-jdbc的加密组件代理一层,加上了加解密的处理逻辑。所以配置如下:

1/** 2 * @author: kl @kailing.pub 3 * @date: 2020/5/18 4 */ 5@Configuration 6@EnableConfigurationProperties({JpaProperties.class,SpringBootEncryptRuleConfigurationProperties.class, SpringBootPropertiesConfigurationProperties.class}) 7public class EncryptDataSourceConfiguration { 8 9 @Bean 10 public DataSource encryptDataSource(DataSource dataSource,SpringBootPropertiesConfigurationProperties props,SpringBootEncryptRuleConfigurationProperties encryptRule) throws SQLException { 11 return EncryptDataSourceFactory.createDataSource(dataSource, new EncryptRuleConfigurationYamlSwapper().swap(encryptRule), props.getProps()); 12 } 13 14 @Bean(initMethod = "afterPropertiesSet") 15 public LocalContainerEntityManagerFactoryBean encryptEntityManagerFactory(@Qualifier("encryptDataSource") DataSource dataSource,JpaProperties jpaProperties, EntityManagerFactoryBuilder factoryBuilder) throws SQLException { 16 return factoryBuilder.dataSource(dataSource) 17 .packages(Constants.BASE_PACKAGES) 18 .properties(jpaProperties.getProperties()) 19 .persistenceUnit("encryptPersistenceUnit") 20 .jta(true) 21 .build(); 22 } 23 24 @Bean 25 public EntityManager encryptEntityManager(@Qualifier("encryptEntityManagerFactory") EntityManagerFactory entityManagerFactory){ 26 //必须使用SharedEntityManagerCreator创建SharedEntityManager实例,否则SimpleJpaRepository中的事务不生效 27 return SharedEntityManagerCreator.createSharedEntityManager(entityManagerFactory); 28 } 29} 30
  • 遇到问题1、:Connection pool exhausted - try increasing 'maxPoolSize' and/or 'borrowConnectionTimeout' on the DataSourceBean.

  • 解决问题:默认AtomikosXADataSourceWrapper包装器初始化的数据源连接池最大为1,所以需要添加配置参数如:

    spring.jta.atomikos.datasource.max-pool-size=20

  • 遇到问题2、: XAER_INVAL: Invalid arguments (or unsupported command)

  • 解决问题:这个是mysql实现XA的bug,仅当您在同一事务中多次访问同一MySQL数据库时,才会发生此问题,在mysql连接url加上如下参数即可,如:

    spring.datasource.url = jdbc:mysql://127.0.0.1:3306/xxx?pinGlobalTxToPhysicalConnection=true

Mysql XA事务行为

在这个场景中,虽然是多数据源,但是底层链接的是同一个mysql数据库,所以XA事务行为为,从第一个执行的sql开始(并不是JTA事务begin阶段),生成xid并XA START事务,然后XA END。第二个数据源的sql执行时会判断是否同一个mysql资源,如果是同一个则用刚生成的xid重新XA START RESUME,然后XA END,最终虽然在应用层是两个DataSource,其实最后只会调用XA COMMIT一次。mysql驱动实现的XAResource的start如下:

1 public void start(Xid xid, int flags) throws XAException { 2 StringBuilder commandBuf = new StringBuilder(MAX_COMMAND_LENGTH); 3 commandBuf.append("XA START "); 4 appendXid(commandBuf, xid); 5 6 switch (flags) { 7 case TMJOIN: 8 commandBuf.append(" JOIN"); 9 break; 10 case TMRESUME: 11 commandBuf.append(" RESUME"); 12 break; 13 case TMNOFLAGS: 14 // no-op 15 break; 16 default: 17 throw new XAException(XAException.XAER_INVAL); 18 } 19 dispatchCommand(commandBuf.toString()); 20 this.underlyingConnection.setInGlobalTx(true); 21 } 22

第一次sql执行时,flags=0,走的TMNOFLAGS逻辑,第二次sql执行时,flags=134217728,走的TMRESUME,重新开启事务的逻辑。以上是Mysql XA的真实事务逻辑,但是博主研究下来发现,msyql xa并不支持XA START RESUME这种语句,而且有很多限制《Mysql XA交易限制》,所以在mysql数据库使用XA事务时,最好了解下mysql xa的缺陷

链式事务方案

链式事务不是我首创的叫法,在spring-data-common项目的Transaction包下,已经有一个默认实现ChainedTransactionManager,前文中《深入理解spring的@Transactional工作原理》已经分析了Spring的事务抽象,由PlatformTransactionManager(事务管理器)、TransactionStatus(事务状态)、TransactionDefinition(事务定义)等形态组成,ChainedTransactionManager也是实现了PlatformTransactionManager和TransactionStatus。实现原理也很简单,在ChainedTransactionManager内部维护了事务管理器的集合,通过代理编排真实的事务管理器,在事务开启、提交、回滚时,都分别操作集合里的事务。以达到对多个事务的统一管理。这个方案比较简陋,而且有缺陷,在提交阶段,如果异常不是发生在第一个数据源,那么会存在之前的提交不会回滚,所以在使用ChainedTransactionManager时,尽量把出问题可能性比较大的事务管理器放链的后面(开启事务、提交事务顺序相反)。这里只是抛出了一种新的多数据源事务管理的思路,能用XA尽量用XA管理。

普通的业务默认数据源配置如下:

1/** 2 * @author: kl @kailing.pub 3 * @date: 2020/5/18 4 */ 5@Configuration 6@EnableConfigurationProperties({JpaProperties.class, DataSourceProperties.class}) 7public class DataSourceConfiguration{ 8 9 @Primary 10 @Bean 11 public DataSource dataSource(DataSourceProperties dataSourceProperties){ 12 return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build(); 13 } 14 15 @Primary 16 @Bean(initMethod = "afterPropertiesSet") 17 public LocalContainerEntityManagerFactoryBean entityManagerFactory(JpaProperties jpaProperties, DataSource dataSource, EntityManagerFactoryBuilder factoryBuilder) { 18 return factoryBuilder.dataSource(dataSource) 19 .packages(Constants.BASE_PACKAGES) 20 .properties(jpaProperties.getProperties()) 21 .persistenceUnit("default") 22 .build(); 23 } 24 25 @Bean 26 @Primary 27 public EntityManager entityManager(EntityManagerFactory entityManagerFactory){ 28 //必须使用SharedEntityManagerCreator创建SharedEntityManager实例,否则SimpleJpaRepository中的事务不生效 29 return SharedEntityManagerCreator.createSharedEntityManager(entityManagerFactory); 30 } 31 32 @Primary 33 @Bean 34 public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ 35 JpaTransactionManager txManager = new JpaTransactionManager(); 36 txManager.setEntityManagerFactory(entityManagerFactory); 37 return txManager; 38 } 39} 40

sharding-jdbc加密数据源配置如下:

1/** 2 * @author: kl @kailing.pub 3 * @date: 2020/5/18 4 */ 5@Configuration 6@EnableConfigurationProperties({JpaProperties.class,SpringBootEncryptRuleConfigurationProperties.class, SpringBootPropertiesConfigurationProperties.class}) 7public class EncryptDataSourceConfiguration { 8 9 @Bean 10 public DataSource encryptDataSource(DataSource dataSource,SpringBootPropertiesConfigurationProperties props,SpringBootEncryptRuleConfigurationProperties encryptRule) throws SQLException { 11 return EncryptDataSourceFactory.createDataSource(dataSource, new EncryptRuleConfigurationYamlSwapper().swap(encryptRule), props.getProps()); 12 } 13 14 @Bean(initMethod = "afterPropertiesSet") 15 public LocalContainerEntityManagerFactoryBean encryptEntityManagerFactory(@Qualifier("encryptDataSource") DataSource dataSource,JpaProperties jpaProperties, EntityManagerFactoryBuilder factoryBuilder) throws SQLException { 16 return factoryBuilder.dataSource(dataSource) 17 .packages(Constants.BASE_PACKAGES) 18 .properties(jpaProperties.getProperties()) 19 .persistenceUnit("encryptPersistenceUnit") 20 .build(); 21 } 22 23 @Bean 24 public EntityManager encryptEntityManager(@Qualifier("encryptEntityManagerFactory") EntityManagerFactory entityManagerFactory){ 25 //必须使用SharedEntityManagerCreator创建SharedEntityManager实例,否则SimpleJpaRepository中的事务不生效 26 return SharedEntityManagerCreator.createSharedEntityManager(entityManagerFactory); 27 } 28 29 @Bean 30 public PlatformTransactionManager chainedTransactionManager(PlatformTransactionManager transactionManager) throws SQLException { 31 JpaTransactionManager encryptTransactionManager = new JpaTransactionManager(); 32 encryptTransactionManager.setEntityManagerFactory(encryptEntityManagerFactory()); 33 //使用链式事务管理器包装真正的transactionManager、txManager事务 34 ChainedTransactionManager chainedTransactionManager = new ChainedTransactionManager(encryptTransactionManager,transactionManager); 35 return chainedTransactionManager; 36 } 37} 38 39

使用这种方案,在涉及到多数据源的业务时,需要指定使用哪个事务管理器,如:

1 @PersistenceContext(unitName = "encryptPersistenceUnit") 2 private EntityManager entityManager; 3 4 @PersistenceContext 5 private EntityManager manager; 6 7 @Transactional(transactionManager = "chainedTransactionManager") 8 public AccountModel save(AccountDTO dto){ 9 AccountModel accountModel = AccountMapper.INSTANCE.dtoTo(dto); 10 11 entityManager.persist(accountModel); 12 entityManager.flush(); 13 AccountModel accountMode2 = AccountMapper.INSTANCE.dtoTo(dto); 14 15 manager.persist(accountMode2); 16 manager.flush(); 17 18 return accountModel; 19 } 20

结语

综上,对于JPA的多数据源分布式事务处理,JTA的事务管理器经过spring boot的封装已经可以开箱即用了。重点在JPA环境下,需要指定EntityManagerFactory的事务使用JTA事务。另本文分享了一种链式事务编排的方式也可以应用在这种场景,但是特殊的场景下不能保证事务的完整性,所以博主推荐使用JtaTransactionManager,有符合的场景也可以试试ChainedTransactionManager。

作者简介:

陈凯玲,2016年5月加入凯京科技。现任凯京科技研发中心架构组经理,救火队队长。独立博客KL博客(http://www.kailing.pub)博主。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )