springboot整合jta事务管理

797 阅读8分钟

小知识,大挑战!本文正在参与“ 程序员必备小知识 ”创作活动

0.java中的事务介绍

Java事务的类型有三种:JDBC事务、JTA(Java Transaction API)事务、容器事务。 常见的容器事务如Spring事务,容器事务主要是J2EE应用服务器提供的,容器事务大多是基于JTA完成,这是一个基于JNDI的,相当复杂的API实现。所以本文暂不讨论容器事务。本文主要介绍J2EE开发中两个比较基本的事务:JDBC事务和JTA事务,重点介绍JTA的使用。

  • JDBC事务

JDBC的一切行为包括事务是基于一个Connection的,在JDBC中是通过Connection对象进行事务管理。在JDBC中,常用的和事务相关的方法是: setAutoCommitcommitrollback等。

图片.png

下面看一个简单的JDBC事务代码:

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为使用Java进行数据库的事务操作提供了最基本的支持。通过JDBC事务,我们可以将多个SQL语句放到同一个事务中,保证其ACID特性。JDBC事务的主要优点就是API比较简单,可以实现最基本的事务操作,性能也相对较好。 但是,JDBC事务有一个局限:一个 JDBC 事务不能跨越多个数据库!!!所以,如果涉及到多数据库的操作或者分布式场景,JDBC事务就无能为力了。

  • 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 规范编译的资源分配器。

1.JTA定义

Java事务API(Java Transaction API,简称JTA ) 是一个Java企业版 的应用程序接口,在Java环境中,允许完成跨越多个XA资源的分布式事务。

图片.png JTA和它的同胞Java事务服务(JTS;Java TransactionService),为J2EE平台提供了分布式事务服务。不过JTA只是提供了一个接口,并没有提供具体的实现,而是由j2ee服务器提供商 根据JTS规范提供的,常见的JTA实现有以下几种:

  • 1.J2EE容器所提供的JTA实现(JBoss)
  • 2.独立的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规范,请看我的另外一篇文章中有相关介绍。这里,提一句,目前主流的数据库都支持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的方式获取UserTransactionConnection

2.springboot引入jta

2.1依赖引入

 <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jta-atomikos</artifactId>
 </dependency>

2.2全局事务配置

  • XML配置 首先要在配置类中引入注解:
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;

@Configuration
@ImportResource({ "classpath:transaction.xml" })
public class TxtManagentConfiguration {
    
}

然后配置XML:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="txManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" ></property>
    </bean>
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="add*" propagation="REQUIRED"/>
            <tx:method name="save*" propagation="REQUIRED"/>
            <tx:method name="del*" propagation="REQUIRED"/>
            <tx:method name="mod*" propagation="REQUIRED"/>
            <tx:method name="update*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
    <aop:config>
        <!-- 切入点 -->
        <aop:pointcut id="newServicesPointcut"
                      expression="execution(* com.xxx.xxx.*.*.service..*.*(..))  or execution (* com.xxx.xxx.*.*.*.service..*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="newServicesPointcut"/>
    </aop:config>
</beans>

注:切入点一定要和自己项目中的目录结构对应,execution(* com.xxx.xxx...service...(..)),.*代表一层目录。

  • java类配置
@Configuration
public class ApplicationContextTransactional {
 
    //事务方法超时时间设置
    private static final int TX_METHOD_TIMEOUT=10;
 
    //AOP切面的切点表达式
    private static final String AOP_POINTCUT_EXPRESSION = "execution(* com.xxx.xxx.*.*.service..*.*(..))  or execution (* com.xxx.xxx.*.*.*.service..*.*(..))";
 
    //注入事务管理器
    @Autowired
    private PlatformTransactionManager transactionManager;
 
    /**
     * 增强(事务)的属性的配置
     *       isolation:DEFAULT        :事务的隔离级别.
     *       propagation              :事务的传播行为.
     *       read-only                :false.不是只读
     *       timeout                  :-1
     *       no-rollback-for          :发生哪些异常不回滚
     *       rollback-for             :发生哪些异常回滚事务
     * @return
     */
    @Bean
    public TransactionInterceptor txAdvice() {
           /*增强(事务)的属性的配置
            * <tx:attributes>
            * */
        NameMatchTransactionAttributeSource txAttributeS = new NameMatchTransactionAttributeSource();
           /*propagation="REQUIRED" , timeout=5 ;rollback-for=".. , .."配置*/
        RuleBasedTransactionAttribute requiredAttr = new RuleBasedTransactionAttribute();
        requiredAttr.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        requiredAttr.setTimeout(TX_METHOD_TIMEOUT);
        requiredAttr.setRollbackRules(Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
           /*propagation="SUPPORTS" , readOnly="true"配置*/
        RuleBasedTransactionAttribute supportsAttr = new RuleBasedTransactionAttribute();
        supportsAttr.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS);
        supportsAttr.setReadOnly(true);
           /*
            注意:方法名称来自类匹配的到方法【save*, “*”表示匹配任意個字符】
             <tx:method .../>
            */
        Map<String , TransactionAttribute> txMethod = new HashMap<String , TransactionAttribute>();
        txMethod.put("add*" , requiredAttr);
        txMethod.put("save*" , requiredAttr);
        txMethod.put("del*" , requiredAttr);
        txMethod.put("mod*" , requiredAttr);
        txMethod.put("update*" , requiredAttr);
        txAttributeS.setNameMap(txMethod);
        TransactionInterceptor txAdvice = new TransactionInterceptor(transactionManager , txAttributeS);
        return txAdvice;
    }
 
    /**
     * AOP配置定义切面和切点的信息
     * @return
     */
    @Bean
    public Advisor txAdviceAdvisor() {
        AspectJExpressionPointcut pointcut= new AspectJExpressionPointcut();
        pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
        return new DefaultPointcutAdvisor(pointcut , txAdvice());
    }
 
}

2.3事务超时时间自定义

jta默认配置在bitronix.tm.Configuration类中,其中超时时间设定的为60秒

defaultTransactionTimeout = getInt(properties, "bitronix.tm.timer.defaultTransactionTimeout", 60);

如果需要自定义超时时间,可以在全局设定超时时间

-- 全局设定时,在application.xml配置如下,单位:s
spring.jta.bitronix.properties.default-transaction-timeout: xx

如果不使用方式2中的全局设置方法,也可以单独设置某一个事务的超时时间,在当前事务所在的servvice上面加入如下注解(单位s):

@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 600)

注意:在@Transactional注解的timeout属性中有如下说明,只有在新启动的事务中才会生效

	/**
	 * The timeout for this transaction (in seconds).
	 * <p>Defaults to the default timeout of the underlying transaction system.
	 * <p>Exclusively designed for use with {@link Propagation#REQUIRED} or
	 * {@link Propagation#REQUIRES_NEW} since it only applies to newly started
	 * transactions.
	 * @see org.springframework.transaction.interceptor.TransactionAttribute#getTimeout()
	 */
	int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;