Spring + Jta +JDBCTemplate 分布式事物实现方式

最近项目中需要用到多数据源管,数据访问层采用的是JDBCTemplate去做的,一开始是在数据源这块做了一个多数据源的操作类,通过拦截器注解去动态赋值指定的数据源操作,这种做法在查询中是没有问题的,但是DML操作时,会出现问题:事物中无法动态操作数据源,导致很多操作指针对第一个库。查询资料的时候发现:
DataSourceTransactionManager这个事物管理类只针对单个数据源进行事物控制.

解决的方案也有多种,这里只提及我实践过的两种:
  1. 开启多个数据源去进行操作,这种是可以自己去实现的.
  2. 利用JTA和Atomikos的多数据源分布事物管理

方案一:

思路:
- 自定义一个注解类,通过传递参数告诉这个业务需要用到哪几个数据源
- 然后仿照Spring中的@Transactional的实现模式,去构建一个集合来开启多个事物
- 然后通过拦截器去动态分配业务给这个集合告诉他,要开几个事物

代码:
applicationContext-datasource.xml


 <bean id="mvp" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
        <property name="driverClassName" value="${mvp.driverClassName}">property>
        <property name="url" value="${mvp.url}">property>
        <property name="username" value="${mvp.username}">property>
        <property name="password" value="${mvp.password}">property>
        <property name="filters" value="${mvp.filters}"/>
        
        <property name="initialSize" value="${mvp.initialSize}"/>
        <property name="minIdle" value="${mvp.minIdle}"/>
        <property name="maxActive" value="${mvp.maxActive}"/>
        
        <property name="maxWait" value="${mvp.maxWait}"/>
        
        <property name="timeBetweenEvictionRunsMillis" value="${mvp.timeBetweenEvictionRunsMillis}"/>
        
        <property name="minEvictableIdleTimeMillis" value="${mvp.minEvictableIdleTimeMillis}"/>
        <property name="testWhileIdle" value="${mvp.testWhileIdle}"/>
        <property name="testOnBorrow" value="${mvp.testOnBorrow}"/>
        <property name="testOnReturn" value="${mvp.testOnReturn}"/>
        
        <property name="poolPreparedStatements" value="${mvp.poolPreparedStatements}"/>
        <property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
        <property name="removeAbandoned" value="${mvp.removeAbandoned}" />
        <property name="removeAbandonedTimeout" value="${mvp.removeAbandonedTimeout}" />
        <property name="maxOpenPreparedStatements" value="${mvp.maxOpenPreparedStatements}" />
        <property name="logAbandoned" value="${mvp.logAbandoned}"/>
        <property name="queryTimeout" value="${mvp.querytimeout}"/>
        <property name="validationQuery" value="${mvp.validationQuery}"/>
    bean>

    
    <bean id="system" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
        <property name="driverClassName" value="${system.driverClassName}">property>
        <property name="url" value="${system.url}">property>
        <property name="username" value="${system.username}">property>
        <property name="password" value="${system.password}">property>
        <property name="filters" value="${system.filters}"/>
        
        <property name="initialSize" value="${system.initialSize}"/>
        <property name="minIdle" value="${system.minIdle}"/>
        <property name="maxActive" value="${system.maxActive}"/>
        
        <property name="maxWait" value="${system.maxWait}"/>
        
        <property name="timeBetweenEvictionRunsMillis" value="${system.timeBetweenEvictionRunsMillis}"/>
        
        <property name="minEvictableIdleTimeMillis" value="${system.minEvictableIdleTimeMillis}"/>
        <property name="testWhileIdle" value="${system.testWhileIdle}"/>
        <property name="testOnBorrow" value="${system.testOnBorrow}"/>
        <property name="testOnReturn" value="${system.testOnReturn}"/>
        
        <property name="poolPreparedStatements" value="${system.poolPreparedStatements}"/>
        <property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
        <property name="removeAbandoned" value="${system.removeAbandoned}" />
        <property name="removeAbandonedTimeout" value="${system.removeAbandonedTimeout}" />
        <property name="maxOpenPreparedStatements" value="${system.maxOpenPreparedStatements}" />
        <property name="logAbandoned" value="${system.logAbandoned}"/>
        <property name="queryTimeout" value="${system.querytimeout}"/>
        <property name="validationQuery" value="${system.validationQuery}"/>
    bean>

    
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="default"/>
    bean>

    <bean id="jdbcTemplate2" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="system"/>
    bean>

applicationContext-service.xml 业务层配置文件




    <bean id="multiTransactionalAspect" class="com.elab.execute.utils.MultiTransactionalAspect"/>
     多数据Aop配置 
    <aop:config proxy-target-class="true">
        
        <aop:aspect ref="multiTransactionalAspect">
            <aop:pointcut id="pointUserMgr" expression="execution(* com.test.execute.services..*(..))"/>
            <aop:around method="around" pointcut-ref="pointUserMgr" />
        aop:aspect>
    aop:config>

注解类 :

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MultiTransactional {


    String[] values() default ""; 

    // TODO 当然你如果想做的更完善一点,可以参考@Transactional这个类,自己去做,什么传播机制啊,隔离级别啊 等等,思路是一样的
}

注解实现类 - 其实这里差不多都是沿用Spring的事物实现方式:

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;

import java.lang.reflect.Method;
import java.util.Stack;

/**
 * @Description: 多数据源动态管理
 * @Author: Liukx on 2017/7/28 - 16:41
 */
@Component("multiTransactionalAspect")
public class MultiTransactionalAspect {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private SpringUtils springUtils;

    /**
     * 切入点
     *
     * @param point
     * @return
     * @throws Throwable
     */
    public Object around(ProceedingJoinPoint point) throws Throwable {
        Stack dataSourceTransactionManagerStack = new Stack();
        Stack transactionStatuStack = new Stack();

        try {
            Object target = point.getTarget();
            String method = point.getSignature().getName();
            Class classz = target.getClass();
            Class[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
            Method m = classz.getMethod(method, parameterTypes);
            if (m != null && m.isAnnotationPresent(MultiTransactional.class)) {
                MultiTransactional multiTransactional = m.getAnnotation(MultiTransactional.class);
                if (!openTransaction(dataSourceTransactionManagerStack, transactionStatuStack, multiTransactional)) {
                    return null;
                }
                Object ret = point.proceed();
                commit(dataSourceTransactionManagerStack, transactionStatuStack);
                return ret;
            }
            Object ret = point.proceed();
            return ret;
        } catch (Exception e) {
            rollback(dataSourceTransactionManagerStack, transactionStatuStack);
            logger.error(String.format(
                    "MultiTransactionalAspect, method:%s-%s occors error:", point
                            .getTarget().getClass().getSimpleName(), point
                            .getSignature().getName()), e);
            throw e;
        }
    }

    /**
     * 打开一个事物方法
     *
     * @param dataSourceTransactionManagerStack
     * @param transactionStatuStack
     * @param multiTransactional
     * @return
     */
    private boolean openTransaction(
            Stack dataSourceTransactionManagerStack,
            Stack transactionStatuStack,
            MultiTransactional multiTransactional) {
        // 获取注解中要打开的事物类型
        String[] transactionMangerNames = multiTransactional.values();
        if (ArrayUtils.isEmpty(multiTransactional.values())) {
            return false;
        }

        for (String beanName : transactionMangerNames) {
            // 创建一个新的事物管理器,用来管理接下来要用到的事物
            DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
            // 根据注解中获取到的数据标识,从spring容器中去查找对应的数据源
            DruidDataSource dataSource = (DruidDataSource) springUtils.getBean(beanName);
            //然后交给事物管理器去管理
            dataSourceTransactionManager.setDataSource(dataSource);
            // 定义一个新的事物定义
            DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
            // 设置一个默认的事物传播机制,注意的是这里可以拓展注解中没有用到的属性
            // defaultTransactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
            TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(defaultTransactionDefinition);
            // 将这个事物的定义放入Stack中
            /**
             * 其中为什么要用Stack来保存TransactionManager和TransactionStatus呢?
             * 那是因为Spring的事务处理是按照LIFO/stack behavior的方式进行的。
             * 如若顺序有误,则会报错:
             */
            transactionStatuStack.push(transactionStatus);
            dataSourceTransactionManagerStack
                    .push(dataSourceTransactionManager);
        }
        return true;
    }

    /**
     * 提交事物方法实现
     *
     * @param dataSourceTransactionManagerStack
     * @param transactionStatuStack
     */
    private void commit(
            Stack dataSourceTransactionManagerStack,
            Stack transactionStatuStack) {
        while (!dataSourceTransactionManagerStack.isEmpty()) {
            dataSourceTransactionManagerStack.pop().commit(
                    transactionStatuStack.pop());
        }
    }

    /**
     * 回滚事物方法实现
     *
     * @param dataSourceTransactionManagerStack
     * @param transactionStatuStack
     */
    private void rollback(
            Stack dataSourceTransactionManagerStack,
            Stack transactionStatuStack) {
        while (!dataSourceTransactionManagerStack.isEmpty()) {
            dataSourceTransactionManagerStack.pop().rollback(
                    transactionStatuStack.pop());
        }
    }
}

service 1 - 2 :

/**
 * 一些实验性Demo接口
 *
 * @author Liukx
 * @create 2017-07-28 10:11
 * @email [email protected]
 **/
public interface IDemoService {

    public void mvpManage() throws Exception;

}

/**
 * 一些实验性Demo接口
 *
 * @author Liukx
 * @create 2017-07-28 10:11
 * @email [email protected]
 **/
public interface IDemoService2 {

    public void beidouMange() throws CoreException;

}

service实现类:


/**
 * 实现性Demo接口实现
 *
 * @author Liukx
 * @create 2017-07-28 10:12
 * @email [email protected]
 **/
@Service("demoService")
public class DemoServiceImpl implements IDemoService {

    @Autowired
    @Qualifier("demoTestDao")
    private IDemoTestDao testDao;

    @Autowired
    private IDemoService2 demoService2;

//    @Autowired
//    private DataSourceTransactionManager transactionManager;

    //TODO 这个注解很关键 - 必须是这个注解才能被拦截器拦截到
    @MultiTransactional(values = {DataSource.mvp, DataSource.system})
    public void mvpManage() throws CoreException {
        System.out.println("=====================开始处理MVP");
        testDao.insert();
        List> select = testDao.select();
        System.out.println("===============>>>>>>>>>>>>>>>>>" + select.size());
        System.out.println("=====================结束处理MVP");

        demoService2.beidouMange();

//        System.out.println("业务处理完成,开始报错");
        int i = 1 / 0;
    }

}

service 2实现

/**
 * @author Liukx
 * @create 2017-07-28 11:07
 * @email [email protected]
 **/
@Service("demo2")
public class DemoServiceImpl2 implements IDemoService2 {
    @Autowired
    @Qualifier("beidouTestDao")
    private IDemoTestDao testDao;

    @MultiTransactional(values = DataSource.system)
    public void beidouMange() throws CoreException {
        System.out.println("=====================开始处理北斗");
        testDao.insert();
        List> select = testDao.select();
        System.out.println("========================================>" + select.size());
        System.out.println("=====================结束处理北斗");
    }
}

dao实现: 这里就只列入实现类

@Repository("beidouTestDao")
public class BeiDouTestDaoImpl extends BaseDao implements IDemoTestDao {


    /**
     * 我们这块目前是有一些封装的,不过你不用太关注;
     * 你只要把对应的数据源指定好就行了
     */
    @Autowired
    @Qualifier("jdbcTemplate2")
    public JdbcTemplate jdbcTemplate;

    public int insert() throws CoreException {
        LinkedHashMap params = new LinkedHashMap<>();
        params.put("name", "某某某" + RandomUtils.randomNum(3));
        params.put("created", new Date());
        return executeInsert("test.insert", params);
    }

    @Override
    public List> select() throws CoreException {
        return findList("test.findAll", new LinkedHashMap());
    }
}
@Repository("demoTestDao")
public class DemoTestDaoImpl extends BaseDao implements IDemoTestDao {
    /**
     * 我们这块目前是有一些封装的,不过你不用太关注;
     * 你只要把对应的数据源指定好就行了
     */
    @Autowired
    @Qualifier("jdbcTemplate")
    public JdbcTemplate jdbcTemplate;

    public int insert() throws CoreException {
        LinkedHashMap params = new LinkedHashMap<>();
        params.put("name", "某某某" + RandomUtils.randomNum(2));
        params.put("created", new Date());
        return executeInsert("test.insert", params);
    }

    public List> select() throws CoreException {
        return findList("test.findAll", new LinkedHashMap());
    }
}

另外有一个动态获取bean的工具类:

@Component
public class SpringUtils implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public Object getBean(String beanId) {
        return applicationContext.getBean(beanId);
    }
}

PS : 这个方案会存在一定的问题 -> 就是如果你下一个另一个数据库操作的业务serivce方法中如果定义了@Transactional它将会又开启一个事物去执行…

方案2 : 利用JTA+Atomikios实现事物管理,业务层代码和上面差不多,主要是配置文件这块.

applicationContext-datasource.xml


<bean id="abstractXADataSource" class="com.atomikos.jdbc.AtomikosDataSourceBean" init-method="init"
          destroy-method="close" abstract="true">
        <property name="xaDataSourceClassName"
                  value="com.alibaba.druid.pool.xa.DruidXADataSource"/>  
        <property name="poolSize" value="10"/>
        <property name="minPoolSize" value="10"/>
        <property name="maxPoolSize" value="30"/>
        <property name="borrowConnectionTimeout" value="60"/>
        <property name="reapTimeout" value="20"/>
        <property name="maxIdleTime" value="60"/>
        <property name="maintenanceInterval" value="60"/>
        <property name="loginTimeout" value="60"/>
        <property name="testQuery" value="${default.validationQuery}"/>
    bean>

    
    <bean id="masterDataSource" parent="abstractXADataSource">
        <property name="uniqueResourceName" value="masterDB"/>
        <property name="xaProperties">
            <props>
                <prop key="driverClassName">${default.driverClassName}prop>
                <prop key="url">${default.url}prop>
                <prop key="password">${default.password}prop>
                 
                <prop key="username">${default.username}prop>   
                <prop key="initialSize">0prop>
                <prop key="maxActive">20prop> 
                <prop key="minIdle">0prop>
                <prop key="maxWait">60000prop>
                <prop key="validationQuery">${default.validationQuery}prop>
                <prop key="testOnBorrow">falseprop>
                <prop key="testOnReturn">falseprop>
                <prop key="testWhileIdle">trueprop>
                <prop key="removeAbandoned">trueprop>
                <prop key="removeAbandonedTimeout">1800prop>
                <prop key="logAbandoned">trueprop>
                <prop key="filters">mergeStatprop>
            props>
        property>
    bean>

    <bean id="slaveDataSource" parent="abstractXADataSource">
        <property name="uniqueResourceName" value="slaveDB"/>
        <property name="xaProperties">
            <props>
                <prop key="driverClassName">${system.driverClassName}prop>
                <prop key="url">${system.url}prop>
                <prop key="password">${system.password}prop>
                
                <prop key="username">${system.username}prop>
                <prop key="initialSize">0prop>
                <prop key="maxActive">20prop>
                <prop key="minIdle">0prop>
                <prop key="maxWait">60000prop>
                <prop key="validationQuery">${system.validationQuery}prop>
                <prop key="testOnBorrow">falseprop>
                <prop key="testOnReturn">falseprop>
                <prop key="testWhileIdle">trueprop>
                <prop key="removeAbandoned">trueprop>
                <prop key="removeAbandonedTimeout">1800prop>
                <prop key="logAbandoned">trueprop>
                <prop key="filters">mergeStatprop>
            props>
        property>
    bean>

    
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="masterDataSource"/>
    bean>

    <bean id="jdbcTemplate2" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="slaveDataSource"/>
    bean>

applicationContext-atomikos.xml


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-4.0.xsd
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-4.0.xsd" default-lazy-init="true">

    <description>配置事物description>
    
    <bean id="atomikosTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager" init-method="init"
          destroy-method="close">
        <property name="forceShutdown">
            <value>truevalue>
        property>
    bean>

    <bean id="atomikosUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp">
        <property name="transactionTimeout" value="300"/>
    bean>
    
    <bean id="springTransactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
        <property name="transactionManager" ref="atomikosTransactionManager"/>
        <property name="userTransaction" ref="atomikosUserTransaction"/>
        
        <property name="allowCustomIsolationLevels" value="true"/>
    bean>

    <aop:config proxy-target-class="true">
        <aop:pointcut id="pointUserMgr" expression="execution(* com.elab.execute.services..*(..))"/>
        <aop:advisor pointcut-ref="pointUserMgr" advice-ref="txAdvice"/>
    aop:config>
    
    <tx:advice id="txAdvice" transaction-manager="springTransactionManager">
        <tx:attributes>
            <tx:method name="get*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="find*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="has*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="locate*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="mvp*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
            <tx:method name="beidou*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
        tx:attributes>
    tx:advice>
    
    
beans>

pom.xml


        <dependency>
            <groupId>javax.transactiongroupId>
            <artifactId>jtaartifactId>
            <version>1.1version>
        dependency>
        <dependency>
            <groupId>com.atomikosgroupId>
            <artifactId>atomikos-utilartifactId>
            <version>4.0.2version>
        dependency>
        <dependency>
            <groupId>com.atomikosgroupId>
            <artifactId>transactionsartifactId>
            <version>4.0.2version>
        dependency>
        <dependency>
            <groupId>com.atomikosgroupId>
            <artifactId>transactions-jtaartifactId>
            <version>4.0.2version>
        dependency>
        <dependency>
            <groupId>com.atomikosgroupId>
            <artifactId>transactions-jdbcartifactId>
            <version>4.0.2version>
        dependency>
        <dependency>
            <groupId>com.atomikosgroupId>
            <artifactId>transactions-apiartifactId>
            <version>4.0.2version>
        dependency>
        <dependency>
            <groupId>cglibgroupId>
            <artifactId>cglib-nodepartifactId>
            <version>3.2.2version>
        dependency>

业务层代码和方案一的差不多,看了一下大概的思路:
1. 在调用被拦截器匹配的方法时,开启一个新的事物
2. 当执行到DML操作时,会获取他对应的数据源,并且会和当前线程的事物管理器中的数据源进行匹配,如果不存在,则到连接池中获取一个新的连接,并把这个连接放入当前线程的事物管理器中进行管理
3. 当所有业务执行完毕,并且没有报错的时候,会执行一个两阶段提交的方式

    PREPARE TRANSACTION transaction_id PREPARE TRANSACTION 为当前事务的两阶段提交做准备。 在命令之后,事务就不再和当前会话关联了;它的状态完全保存在磁盘上, 它提交成功有非常高的可能性,即使是在请求提交之前数据库发生了崩溃也如此。这条命令必须在一个用BEGIN显式开始的事务块里面使用。  
    COMMIT PREPARED transaction_id 提交已进入准备阶段的ID为transaction_id的事务  
    ROLLBACK PREPARED transaction_id 回滚已进入准备阶段的ID为transaction_id的事务

推荐使用第二种方式,因为它有比较好的连接池以及相对完善的机制,第一种考虑的情况比较少,会出现问题,当然,你如果愿意折腾自己写一套,可以参考一下..

以上为个人学习参考,有什么不足的地方欢迎指正.

参考博客:
Mybatis + JTA http://blog.csdn.net/zmx729618/article/details/54344296
分布式事物提交以及JTA的概念 : http://www.jasongj.com/big_data/two_phase_commit/
JTA一些实现原理:https://www.ibm.com/developerworks/cn/java/j-lo-jta/

你可能感兴趣的:(后端,spring,JTA,分布式事物,多数据源,Atomikos)