Spring如何支持分布式事务
一、分布式事务介绍
名词解释
XA :XA规范的目的是允许多个资源(如数据库,应用服务器,消息队列,等等)在同一事务中访问,这样可以使ACID属性跨越应用程序而保持有效。XA使用两阶段提交来保证所有资源同时提交或回滚任何特定的事务。
JTA: Java事务API(Java Transaction API,简称JTA ) 是一个Java企业版 的应用程序接口,在Java环境中,允许完成跨越多个XA资源的分布式事务。
分布式事务要解决的问题
把不同支援放到一个事物中,实现ACID。举例说明,一个方法中需要操作两个数据库 db1,db2, 本地事物是基于connection ,所以无法保证两个库的事物,这是后需要用到分布式事物。
分布式事务原理
两阶段提交。简单来说,引入事务管理器(txManager)的概念,开启事务前,txManager 创建一个 tx ,txId 是全局事务的唯一标示, 方法中db1操作完成后,告知tx db1操作成功,但db1没有真的提交,而是block住了。db2 继续执行,执行完了自己block住,然后告知txManager,这个事物可以提交了,然后txManager 通知db1 db2 你们可以真正提交了,事物结束。
db block 住属于第一阶段, 真正提交或者回滚属于第二阶段,这就是两阶段提交。
二、java事务类型
Java事务的类型有三种:JDBC事务、JTA(Java Transaction API)事务、容器事务。 常见的容器事务如Spring事务,容器事务主要是J2EE应用服务器提供的,容器事务大多是基于JTA完成,这是一个基于JNDI的,相当复杂的API实现。本人不推荐使用容器事务。
JDBC事务
JDBC的一切行为包括事务是基于一个Connection的,在JDBC中是通过Connection对象进行事务管理。在JDBC中,常用的和事务相关的方法是: setAutoCommit、commit、rollback等。
事务案例
public void JdbcTransfer() { java.sql.Connection conn = null; try{ conn = conn =DriverManager.getConnection("jdbc:oracle:thin:@host:1521:SID","username","userpwd"); // 将自动提交设置为 false, //若设置为 true 则数据库将会把每一次数据更新认定为一个事务并自动提交 conn.setAutoCommit(false); stmt = conn.createStatement(); // 将 A 账户中的金额减少 500 stmt.execute("\ update t_account set amount = amount - 500 where account_id = 'A'"); // 将 B 账户中的金额增加 500 stmt.execute("\ update t_account set amount = amount + 500 where account_id = 'B'"); // 提交事务 conn.commit(); // 事务提交:转账的两步操作同时成功 } catch(SQLException sqle){ try{ // 发生异常,回滚在本事务中的操做 conn.rollback(); // 事务回滚:转账的两步操作完全撤销 stmt.close(); conn.close(); }catch(Exception ignore){ } sqle.printStackTrace(); } }
JDBC事务的优缺点
JDBC为使用Java进行数据库的事务操作提供了最基本的支持。通过JDBC事务,我们可以将多个SQL语句放到同一个事务中,保证其ACID特性。JDBC事务的主要优点就是API比较简单,可以实现最基本的事务操作,性能也相对较好。
但是,JDBC事务有一个局限:一个 JDBC 事务不能跨越多个数据库!!!所以,如果涉及到多数据库的操作或者分布式场景,JDBC事务就无能为力了。
容器事务
容器事务也是基于jndi实现的。
jndi(java naming directory interface),可以把JNDI看成一个全局的目录服务接口,实现了这个接口的类可以提供你想要的东西,不管这个东西是什么,只要注册到了目录中就可以被找到并且返回给你。有点像webservbice。
Spring配置JNDI和通过JNDI获取DataSource。
SpringJNDI数据源配置信息
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <value>java:comp/env/myDataSourceJNDI</value> </property> </bean>
上面<value>中myDataSourceJNDI是tomcat或者其他应用服务器配置的JNDI。
关于JNDI的配置(tomcat中)
修改tomcat目录conf/context.xml文件:
<Resource name="myDataSourceJNDI" auth="Container" type="javax.sql.DataSource" maxActive="100" maxIdle="30" maxWait="10000" username="root" password="root" driverClassName="oracle.jdbc.driver.OracleDriver" url="jdbc:oracle:thin:@127.0.0.1:1521:TEST"/>
通过JNDI获取DataSource
Context context = new InitialContext(); DataSource ds = (DataSource)context.lookup("java:comp/env/myDataSourceJNDI");
它和jdbc的区别是,jdbc是java去找数据库驱动,jndi是通过你的服务器配置(如Tomcat)的配置文件context来找数据库驱动。
正如上面所见,致命缺陷,由于jndi要访问容器组件(tomcat)的配置,所以耦合比较严重,不喜欢用。
JTA事务
为什么需要JTA
通常,JDBC事务就可以解决数据的一致性等问题,鉴于他用法相对简单,所以很多人关于Java中的事务只知道有JDBC事务,或者有人知道框架中的事务(比如Hibernate、Spring)等。但是,由于JDBC无法实现分布式事务,而如今的分布式场景越来越多,所以,JTA事务就应运而生。
如果,你在工作中没有遇到JDBC事务无法解决的场景,那么只能说你做的项目还都太小。拿电商网站来说,我们一般把一个电商网站横向拆分成商品模块、订单模块、购物车模块、消息模块、支付模块等。然后我们把不同的模块部署到不同的机器上,各个模块之间通过远程服务调用(RPC)等方式进行通信。以一个分布式的系统对外提供服务。
一个支付流程就要和多个模块进行交互,每个模块都部署在不同的机器中,并且每个模块操作的数据库都不一致,这时候就无法使用JDBC来管理事务。我们看一段代码:
/** 支付订单处理 **/ @Transactional(rollbackFor = Exception.class) public void completeOrder() { orderDao.update(); // 订单服务本地更新订单状态 accountService.update(); // 调用资金账户服务给资金帐户加款 pointService.update(); // 调用积分服务给积分帐户增加积分 accountingService.insert(); // 调用会计服务向会计系统写入会计原始凭证 merchantNotifyService.notify(); // 调用商户通知服务向商户发送支付结果通知 }
上面的代码是一个简单的支付流程的操作,其中调用了五个服务,这五个服务都通过RPC的方式调用,请问使用JDBC如何保证事务一致性?我在方法中增加了@Transactional注解,但是由于采用调用了分布式服务,该事务并不能达到ACID的效果。
JTA事务比JDBC事务更强大。一个JTA事务可以有多个参与者,而一个JDBC事务则被限定在一个单一的数据库连接。下列任一个Java平台的组件都可以参与到一个JTA事务中:JDBC连接、JDO PersistenceManager 对象、JMS 队列、JMS 主题、企业JavaBeans(EJB)、一个用J2EE Connector Architecture 规范编译的资源分配器。
JTA的定义
Java事务API(Java Transaction API,简称JTA ) 是一个Java企业版 的应用程序接口,在Java环境中,允许完成跨越多个XA资源的分布式事务。
JTA和它的同胞Java事务服务(JTS;Java TransactionService),为J2EE平台提供了分布式事务服务。不过JTA只是提供了一个接口,并没有提供具体的实现,而是由j2ee服务器提供商根据JTS规范提供的,常见的JTA实现有以下几种:
- J2EE容器所提供的JTA实现(JBoss)
- 独立的JTA实现:如JOTM,Atomikos.这些实现可以应用在那些不使用J2EE应用服务器的环境里用以提供分布事事务保证。如Tomcat,Jetty以及普通的java应用。
JTA里面提供了 java.transaction.UserTransaction ,里面定义了下面几个方法
begin:开启一个事务
commit:提交当前事务
rollback:回滚当前事务
setRollbackOnly:把当前事务标记为回滚
setTransactionTimeout:设置事务的事件,超过这个事件,就抛出异常,回滚事务
这里,值得注意的是,不是使用了UserTransaction就能把普通的JDBC操作直接转成JTA操作,JTA对DataSource、Connection和Resource 都是有要求的,只有符合XA规范,并且实现了XA规范的相关接口的类才能参与到JTA事务中来。目前主流的数据库都支持XA规范。
要想使用用 JTA 事务,那么就需要有一个实现 javax.sql.XADataSource 、javax.sql.XAConnection 和 javax.sql.XAResource 接口的 JDBC 驱动程序。一个实现了这些接口的驱动程序将可以参与 JTA 事务。一个 XADataSource 对象就是一个 XAConnection 对象的工厂。XAConnection 是参与 JTA 事务的 JDBC 连接。
要使用JTA事务,必须使用XADataSource来产生数据库连接,产生的连接为一个XA连接。
XA连接(javax.sql.XAConnection)和非XA(java.sql.Connection)连接的区别在于:XA可以参与JTA的事务,而且不支持自动提交。
public void JtaTransfer() { javax.transaction.UserTransaction tx = null; java.sql.Connection conn = null; try{ tx = (javax.transaction.UserTransaction) context.lookup("java:comp/UserTransaction"); //取得JTA事务,本例中是由Jboss容器管理 javax.sql.DataSource ds = (javax.sql.DataSource) context.lookup("java:/XAOracleDS"); //取得数据库连接池,必须有支持XA的数据库、驱动程序 tx.begin(); conn = ds.getConnection(); // 将自动提交设置为 false, //若设置为 true 则数据库将会把每一次数据更新认定为一个事务并自动提交 conn.setAutoCommit(false); stmt = conn.createStatement(); // 将 A 账户中的金额减少 500 stmt.execute("\ update t_account set amount = amount - 500 where account_id = 'A'"); // 将 B 账户中的金额增加 500 stmt.execute("\ update t_account set amount = amount + 500 where account_id = 'B'"); // 提交事务 tx.commit(); // 事务提交:转账的两步操作同时成功 } catch(SQLException sqle){ try{ // 发生异常,回滚在本事务中的操做 tx.rollback(); // 事务回滚:转账的两步操作完全撤销 stmt.close(); conn.close(); }catch(Exception ignore){ } sqle.printStackTrace(); } }
上面的例子就是一个使用JTA事务的转账操作,该操作相对依赖于J2EE容器,并且需要通过JNDI的方式获取UserTransaction和Connection。
标准的分布式事务
一个分布式事务(Distributed Transaction)包括一个事务管理器(transaction manager)和一个或多个资源管理器(resource manager)。一个资源管理器(resource manager)是任意类型的持久化数据存储。事务管理器(transaction manager)承担着所有事务参与单元者的相互通讯的责任。
JTA的实现方式也是基于以上这些分布式事务参与者实现的,具体的关于JTA的实现细节不是本文的重点,感兴趣的同学可以阅读JTA 深度历险 – 原理与实现
看上面关于分布式事务的介绍是不是和2PC中的事务管理比较像?的却,2PC其实就是符合XA规范的事务管理器协调多个资源管理器的一种实现方式。 我之前有几篇文章关于2PC和3PC的,那几篇文章中介绍过分布式事务中的事务管理器是如何协调多个事务的统一提交或回滚的,后面我还会有几篇文章详细的介绍一下和分布式事务相关的内容,包括但不限于全局事务、DTP模型、柔性事务等。
JTA的优缺点
JTA的优点很明显,就是提供了分布式事务的解决方案,严格的ACID。但是,标准的JTA方式的事务管理在日常开发中并不常用,因为他有很多缺点:
- 实现复杂,通常情况下,JTA UserTransaction需要从JNDI获取。这意味着,如果我们使用JTA,就需要同时使用JTA和JNDI。
- JTA本身就是个笨重的API
- 通常JTA只能在应用服务器环境下使用,因此使用JTA会限制代码的复用性。
在spring中使用JTA分布式事务
spring的org.springframework.transaction.jta.JtaTransactionManager,提供了分布式事务支持。如果使用WAS的JTA支持,把它的属性改为WebSphere对应TransactionManager。
在tomcat下,是没有分布式事务的,不过可以借助于第三方软件jotm(Java Open Transaction Manager )和AtomikosTransactionsEssentials实现,在spring中分布式事务是通过jta(jotm,atomikos)来进行实现。
三、分布式事务实现方案
提示:多数据源的切换一般可以根据项目结构划分(即哪些包路径是使用哪个数据源,在一开始就初始化所有关系。)
Atomikos的JTA事务实现
引入依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.1.16.RELEASE</version> </dependency> <!--atomikos是分布式事务使用,jta是spring整合分布式事务使用 --> <dependency> <groupId>com.atomikos</groupId> <artifactId>transactions-jdbc</artifactId> <version>5.0.8</version> </dependency> <dependency> <groupId>javax.transaction</groupId> <artifactId>jta</artifactId> <version>1.1</version> </dependency> <!-- druidDataSource, 支持XA规范 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.3</version> </dependency> <!-- mysql, 它提供了MysqlXADataSource支持XA规范 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency>
数据源配置
db.properties
# 数据源1 ps.datasource.one.uniqueResourceName=first_unique_db ps.datasource.one.driverClassName=com.mysql.jdbc.Driver ps.datasource.one.jdbcUrl=jdbc:mysql://localhost:3305/spring?useTimezone=true&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useUnicode=true&characterEncoding=utf-8&tcpRcvBuf=1024000&useOldAliasMetadataBehavior=true&useSSL=false&rewriteBatchedStatements=true&useAffectedRows=true ps.datasource.one.username=root ps.datasource.one.password=123456 ps.datasource.one.poolSize=2 ps.datasource.one.minPoolSize=1 ps.datasource.one.maxPoolSize=5 #获取连接失败重新获等待最大时间,在这个时间内如果有可用连接,将返回 ps.datasource.one.borrowConnectionTimeout=60 #最大获取数据时间,如果不设置这个值,Atomikos使用默认的5分钟,那么在处理大批量数据读取的时候,一旦超过5分钟,就会抛出类似 Resultset is close 的错误 ps.datasource.one.reapTimeout=20 #最大闲置时间,超过最小连接池连接的连接将关闭 ps.datasource.one.maxIdleTime=20 #连接回收时间 ps.datasource.one.maintenanceInterval=20 #java数据库连接池,最大可等待获取dataSource的时间 ps.datasource.one.loginTimeout=60 ps.datasource.one.testQuery=select 1 # 数据源2 ps.datasource.two.uniqueResourceName=second_unique_db ps.datasource.two.driverClassName=com.mysql.jdbc.Driver ps.datasource.two.jdbcUrl=jdbc:mysql://localhost:3305/mos_sp?useTimezone=true&serverTimezone=GMT%2B8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useUnicode=true&characterEncoding=utf-8&tcpRcvBuf=1024000&useOldAliasMetadataBehavior=true&useSSL=false&rewriteBatchedStatements=true&useAffectedRows=true ps.datasource.two.username=root ps.datasource.two.password=123456 ps.datasource.two.poolSize=2 ps.datasource.two.minPoolSize=1 ps.datasource.two.maxPoolSize=5 #获取连接失败重新获等待最大时间,在这个时间内如果有可用连接,将返回 ps.datasource.two.borrowConnectionTimeout=60 #最大获取数据时间,如果不设置这个值,Atomikos使用默认的5分钟,那么在处理大批量数据读取的时候,一旦超过5分钟,就会抛出类似 Resultset is close 的错误 ps.datasource.two.reapTimeout=20 #最大闲置时间,超过最小连接池连接的连接将关闭 ps.datasource.two.maxIdleTime=20 #连接回收时间 ps.datasource.two.maintenanceInterval=20 #java数据库连接池,最大可等待获取dataSource的时间 ps.datasource.two.loginTimeout=60 ps.datasource.two.testQuery=select 1
数据源1
@Component @PropertySource("classpath:db.properties") public class FirstDbConfigBean { @Value("${ps.datasource.one.uniqueResourceName}") private String uniqueResourceName; @Value("${ps.datasource.one.driverClassName}") private String driverClassName; @Value("${ps.datasource.one.jdbcUrl}") private String jdbcUrl; @Value("${ps.datasource.one.username}") private String username; @Value("${ps.datasource.one.password}") private String password; @Value("${ps.datasource.one.poolSize}") private int poolSize; @Value("${ps.datasource.one.minPoolSize}") private int minPoolSize; @Value("${ps.datasource.one.maxPoolSize}") private int maxPoolSize; @Value("${ps.datasource.one.borrowConnectionTimeout}") private int borrowConnectionTimeout; @Value("${ps.datasource.one.reapTimeout}") private int reapTimeout; @Value("${ps.datasource.one.maxIdleTime}") private int maxIdleTime; @Value("${ps.datasource.one.maintenanceInterval}") private int maintenanceInterval; @Value("${ps.datasource.one.loginTimeout}") private int loginTimeout; @Value("${ps.datasource.one.testQuery}") private String testQuery; public String getUniqueResourceName() { return uniqueResourceName; } public void setUniqueResourceName(String uniqueResourceName) { this.uniqueResourceName = uniqueResourceName; } public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } public String getJdbcUrl() { return jdbcUrl; } public void setJdbcUrl(String jdbcUrl) { this.jdbcUrl = jdbcUrl; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getPoolSize() { return poolSize; } public void setPoolSize(int poolSize) { this.poolSize = poolSize; } public int getMinPoolSize() { return minPoolSize; } public void setMinPoolSize(int minPoolSize) { this.minPoolSize = minPoolSize; } public int getMaxPoolSize() { return maxPoolSize; } public void setMaxPoolSize(int maxPoolSize) { this.maxPoolSize = maxPoolSize; } public int getBorrowConnectionTimeout() { return borrowConnectionTimeout; } public void setBorrowConnectionTimeout(int borrowConnectionTimeout) { this.borrowConnectionTimeout = borrowConnectionTimeout; } public int getReapTimeout() { return reapTimeout; } public void setReapTimeout(int reapTimeout) { this.reapTimeout = reapTimeout; } public int getMaxIdleTime() { return maxIdleTime; } public void setMaxIdleTime(int maxIdleTime) { this.maxIdleTime = maxIdleTime; } public int getMaintenanceInterval() { return maintenanceInterval; } public void setMaintenanceInterval(int maintenanceInterval) { this.maintenanceInterval = maintenanceInterval; } public int getLoginTimeout() { return loginTimeout; } public void setLoginTimeout(int loginTimeout) { this.loginTimeout = loginTimeout; } public String getTestQuery() { return testQuery; } public void setTestQuery(String testQuery) { this.testQuery = testQuery; } }
数据源2
@Component @PropertySource("classpath:db.properties") public class SecondDbConfigBean { @Value("${ps.datasource.two.uniqueResourceName}") private String uniqueResourceName; @Value("${ps.datasource.two.driverClassName}") private String driverClassName; @Value("${ps.datasource.two.jdbcUrl}") private String jdbcUrl; @Value("${ps.datasource.two.username}") private String username; @Value("${ps.datasource.two.password}") private String password; @Value("${ps.datasource.two.poolSize}") private int poolSize; @Value("${ps.datasource.two.minPoolSize}") private int minPoolSize; @Value("${ps.datasource.two.maxPoolSize}") private int maxPoolSize; @Value("${ps.datasource.two.borrowConnectionTimeout}") private int borrowConnectionTimeout; @Value("${ps.datasource.two.reapTimeout}") private int reapTimeout; @Value("${ps.datasource.two.maxIdleTime}") private int maxIdleTime; @Value("${ps.datasource.two.maintenanceInterval}") private int maintenanceInterval; @Value("${ps.datasource.two.loginTimeout}") private int loginTimeout; @Value("${ps.datasource.two.testQuery}") private String testQuery; public String getUniqueResourceName() { return uniqueResourceName; } public void setUniqueResourceName(String uniqueResourceName) { this.uniqueResourceName = uniqueResourceName; } public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } public String getJdbcUrl() { return jdbcUrl; } public void setJdbcUrl(String jdbcUrl) { this.jdbcUrl = jdbcUrl; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getPoolSize() { return poolSize; } public void setPoolSize(int poolSize) { this.poolSize = poolSize; } public int getMinPoolSize() { return minPoolSize; } public void setMinPoolSize(int minPoolSize) { this.minPoolSize = minPoolSize; } public int getMaxPoolSize() { return maxPoolSize; } public void setMaxPoolSize(int maxPoolSize) { this.maxPoolSize = maxPoolSize; } public int getBorrowConnectionTimeout() { return borrowConnectionTimeout; } public void setBorrowConnectionTimeout(int borrowConnectionTimeout) { this.borrowConnectionTimeout = borrowConnectionTimeout; } public int getReapTimeout() { return reapTimeout; } public void setReapTimeout(int reapTimeout) { this.reapTimeout = reapTimeout; } public int getMaxIdleTime() { return maxIdleTime; } public void setMaxIdleTime(int maxIdleTime) { this.maxIdleTime = maxIdleTime; } public int getMaintenanceInterval() { return maintenanceInterval; } public void setMaintenanceInterval(int maintenanceInterval) { this.maintenanceInterval = maintenanceInterval; } public int getLoginTimeout() { return loginTimeout; } public void setLoginTimeout(int loginTimeout) { this.loginTimeout = loginTimeout; } public String getTestQuery() { return testQuery; } public void setTestQuery(String testQuery) { this.testQuery = testQuery; } }
数据源及JTA事务配置
/** * 多数据源jta事务支持 */ @Configuration @EnableTransactionManagement public class AtomikosJtaDataSource { /** * @MethodName firstDataSource * @Description 数据源1 */ @Bean("firstDataSource") public DataSource firstDataSource(FirstDbConfigBean configBean) throws Exception { AtomikosDataSourceBean firstDataSourceBean = new AtomikosDataSourceBean(); // 不能为空,否则报错:Property 'uniqueResourceName' cannot be null firstDataSourceBean.setUniqueResourceName(configBean.getUniqueResourceName()); DruidXADataSource xaDataSource = new DruidXADataSource(); xaDataSource.setUrl(configBean.getJdbcUrl()); xaDataSource.setDriverClassName(configBean.getDriverClassName()); xaDataSource.setUsername(configBean.getUsername()); xaDataSource.setPassword(configBean.getPassword()); firstDataSourceBean.setXaDataSource(xaDataSource); firstDataSourceBean.setMaxIdleTime(configBean.getMaxIdleTime()); firstDataSourceBean.setMinPoolSize(configBean.getMinPoolSize()); firstDataSourceBean.setPoolSize(configBean.getPoolSize()); firstDataSourceBean.setMaxPoolSize(configBean.getMaxPoolSize()); firstDataSourceBean.setTestQuery(configBean.getTestQuery()); firstDataSourceBean.setReapTimeout(configBean.getReapTimeout()); firstDataSourceBean.setMaintenanceInterval(configBean.getMaintenanceInterval()); firstDataSourceBean.setLoginTimeout(configBean.getLoginTimeout()); firstDataSourceBean.setBorrowConnectionTimeout(configBean.getBorrowConnectionTimeout()); return firstDataSourceBean; } /** * @MethodName secondDataSource * @Description 数据源2 */ @Bean("secondDataSource") public DataSource secondDataSource(SecondDbConfigBean configBean) throws Exception { AtomikosDataSourceBean secondDataSourceBean = new AtomikosDataSourceBean(); // 不能为空,否则报错:Property 'uniqueResourceName' cannot be null secondDataSourceBean.setUniqueResourceName(configBean.getUniqueResourceName()); DruidXADataSource xaDataSource = new DruidXADataSource(); xaDataSource.setUrl(configBean.getJdbcUrl()); xaDataSource.setDriverClassName(configBean.getDriverClassName()); xaDataSource.setUsername(configBean.getUsername()); xaDataSource.setPassword(configBean.getPassword()); secondDataSourceBean.setXaDataSource(xaDataSource); secondDataSourceBean.setMaxIdleTime(configBean.getMaxIdleTime()); secondDataSourceBean.setMinPoolSize(configBean.getMinPoolSize()); secondDataSourceBean.setPoolSize(configBean.getPoolSize()); secondDataSourceBean.setMaxPoolSize(configBean.getMaxPoolSize()); secondDataSourceBean.setTestQuery(configBean.getTestQuery()); secondDataSourceBean.setReapTimeout(configBean.getReapTimeout()); secondDataSourceBean.setMaintenanceInterval(configBean.getMaintenanceInterval()); secondDataSourceBean.setLoginTimeout(configBean.getLoginTimeout()); secondDataSourceBean.setBorrowConnectionTimeout(configBean.getBorrowConnectionTimeout()); return secondDataSourceBean; } @Bean("firstJdbcTemplate") public JdbcTemplate firstJdbcTemplate(DataSource firstDataSource){ JdbcTemplate jdbcTemplate = new JdbcTemplate(); jdbcTemplate.setDataSource(firstDataSource); return jdbcTemplate; } @Bean("secondJdbcTemplate") public JdbcTemplate secondJdbcTemplate(DataSource secondDataSource){ JdbcTemplate jdbcTemplate = new JdbcTemplate(); jdbcTemplate.setDataSource(secondDataSource); return jdbcTemplate; } /** * @MethodName transactionManager * @Description 事务管理器,包装了atomikos事务 */ @Bean public JtaTransactionManager transactionManager() throws Exception { JtaTransactionManager transactionManager = new JtaTransactionManager(); UserTransactionManager userTransactionManager = new UserTransactionManager(); userTransactionManager.setForceShutdown(true); userTransactionManager.setTransactionTimeout(3000); transactionManager.setUserTransaction(userTransactionManager); transactionManager.setAllowCustomIsolationLevels(true); return transactionManager; } /** * @MethodName transactionTemplate * @Description spring 事务模板,包装了atomikos事务, 主要用于编程式事务 */ @Bean public TransactionTemplate transactionTemplate() throws Exception { TransactionTemplate transactionTemplate = new TransactionTemplate(); JtaTransactionManager transactionManager = new JtaTransactionManager(); UserTransactionManager userTransactionManager = new UserTransactionManager(); userTransactionManager.setForceShutdown(true); userTransactionManager.setTransactionTimeout(3000); transactionManager.setUserTransaction(userTransactionManager); transactionManager.setAllowCustomIsolationLevels(true); transactionTemplate.setTransactionManager(transactionManager); return transactionTemplate; } }
注意:需要在SpringConfig.java中添加@Import(value = {AtomikosJtaDataSource.class})。
测试
AtomikosService.java
public interface AtomikosService { void saveEntity(); }
AtomikosServiceImpl.java
@Service public class AtomikosServiceImpl implements AtomikosService { @Resource(name = "firstJdbcTemplate") private JdbcTemplate firstJdbcTemplate; @Resource(name = "secondJdbcTemplate") private JdbcTemplate secondJdbcTemplate; @Transactional @Override public void saveEntity() { String userCode = UUID.randomUUID().toString(); String sql_1 = "insert into os_user(user_name, age) values(?, ?)"; firstJdbcTemplate.update(sql_1, userCode, 23); String sql_2 = "insert into os_order(order_id, user_code) values(?, ?)"; secondJdbcTemplate.update(sql_2, UUID.randomUUID().toString(), userCode); //int a = 1/0; } }
@RunWith(SpringRunner.class) @WebAppConfiguration @ContextHierarchy({ @ContextConfiguration(classes = SpringConfig.class), @ContextConfiguration(classes = SpringMVCConfig.class) }) public class JtaTest { @Autowired private AtomikosService atomikosService; @Test public void testAtomikos() { atomikosService.saveEntity(); } }
可能大家也发现了,上面的代码中注入了多个JdbcTemplate实例,麻烦地很。我们可以这样解决:使用Map<String, JdbcTemplate> jdbcTemplateMap = applicationContext.getBeansOfType(JdbcTemplate.class);将所有的实例起来,用的时候根据beanName取下。这是最简单粗暴的方式。
消息事务+最终一致性
所谓的消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性。
基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。
缺点:
- 不是严格一致而是最终一致
- 存在一定的风险,如上如果A执行成功,B始终执行不成功,那就完蛋了。
优点:
- 性能大幅度提升。