AOP+自定义注解实现的多数据库事务管理

场景与方案

当一个服务,连接两个数据库,一次接口请求需要向两个数据库同时写入数据,我们使用 @Transactional 注解方式加入事务,此时A数据库写入成功,B数据库写入失败,这时可能数据库A写入的数据不能回滚,原因是 @Transactional 注解中 transactionManager 的参数决定的,只能指定一个事务管理。

A数据源配置代码

java 复制代码
@Configuration
@MapperScan(basePackages = "com.p.test.testadb.mapper",
       annotationClass = Repository.class,
       sqlSessionFactoryRef = DataSourceConfig.SQL_SESSION_FACTORY_NAME)
public class DataSourceAConfig {

   static final String SQL_SESSION_FACTORY_NAME = "aSessionFactory";
   public static final String A_TX_MANAGER = "aTxManager";


   @Bean(name = "aDataSource")
   @ConfigurationProperties(prefix = "spring.datasource")
   @Primary
   public DataSource dataSource() {
       return DataSourceBuilder.create().build();
   }

   @Bean(name = A_TX_MANAGER)
   @Primary
   public PlatformTransactionManager txManager() {
       return new DataSourceTransactionManager(dataSource());
   }

   @Bean(name = SQL_SESSION_FACTORY_NAME)
   @Primary
   public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
       SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
       sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
               .getResources("classpath*:mapper-testadb/*.xml"));
       sqlSessionFactoryBean.setDataSource(dataSource());

       org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
       configuration.setCacheEnabled(false);
       sqlSessionFactoryBean.setConfiguration(configuration);

       return sqlSessionFactoryBean.getObject();
   }

}

B数据源配置代码

java 复制代码
@Configuration
@MapperScan(basePackages = "com.p.test.testbdb.mapper",
        annotationClass = Repository.class,
        sqlSessionFactoryRef = DataSourceConfig.SQL_SESSION_FACTORY_NAME)
public class DataSourceBConfig {

    static final String SQL_SESSION_FACTORY_NAME = "bSessionFactory";
    public static final String B_TX_MANAGER = "bTxManager";


    @Bean(name = "bDataSource")
    @ConfigurationProperties(prefix = "spring.datasource")
    @Primary
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = B_TX_MANAGER)
    @Primary
    public PlatformTransactionManager txManager() {
        return new DataSourceTransactionManager(dataSource());
    }

    @Bean(name = SQL_SESSION_FACTORY_NAME)
    @Primary
    public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath*:mapper-testbdb/*.xml"));
        sqlSessionFactoryBean.setDataSource(dataSource());

        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setCacheEnabled(false);
        sqlSessionFactoryBean.setConfiguration(configuration);

        return sqlSessionFactoryBean.getObject();
    }

}

最原始的办法

csharp 复制代码
public interface TestService {

    int insertAAndB();

}
less 复制代码
@Service
public class TestServiceImpl implements TestService {

    @Autowired
    private Test1Service test1Service;

    @Override
    @Transactional(DataSourceAConfig.A_TX_MANAGER)
    public int insertAAndBUser() {
        return test1Service.insert();
    }
}
csharp 复制代码
public interface Test1Service {

    int insert();

}
less 复制代码
@Service
public class Test1ServiceImpl implements Test1Service {


    @Override
    @Transactional(transactionManager = DataSourceBConfig.B_TX_MANAGER)
    public int insert() {
        //a写入库
        a.insert();
        //b写入库
        b.insert();
        return 1;
    }
}

这种方式如果我们一次操作10个数据库,我们就要手写嵌套10层方法,简直是让人头疼

最终解决方案

AOP+自定义注解,下面直接开始撸码

java 复制代码
/**
 * @Author: pp
 * @DateTime: 2022/3/29 14:33
 * @Description: 定义多事务注解
 */

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiTransactional {

    String[] value() default {};

}

下面是AOP相关代码,通过AOP技术拦截所有使用了 @MultiTransactional 注解的方法

java 复制代码
/**
 * @Author: pp
 * @DateTime: 2022/3/29 14:36
 * @Description: AOP拦截MultiTransactional注解方法
 */
@Aspect
@Component
public class MultiTransactionAop {


    private final ComboTransaction comboTransaction;

    @Autowired
    public MultiTransactionAop(ComboTransaction comboTransaction) {
        this.comboTransaction = comboTransaction;
    }

    @Pointcut("@annotation(com.modian.chain.config.datasource.annotation.MultiTransactional)")
    public void pointCut() {
    }

    @Around("pointCut() && @annotation(multiTransactional)")
    public Object inMultiTransaction(ProceedingJoinPoint pjp, MultiTransactional multiTransactional) {

        return comboTransaction.inCombinedTx(() -> {
            try {
                return pjp.proceed();
            } catch (Throwable throwable) {
                if (throwable instanceof RuntimeException) {
                    throw (RuntimeException) throwable;
                }
                throw new RuntimeException(throwable);
            }
        }, multiTransactional.value());

    }


}

下面实现一个公共事务方法,这代码很简单,将注解中的参数带过来,循环参数中的事务,判断是哪个事务就执行哪个事务对应的的方法

php 复制代码
/**
 * @Author: pp
 * @DateTime: 2022/3/29 14:38
 * @Description: 公共事务实现
 */
@Component
public class ComboTransaction {


    @Autowired
    private ATxBroker aTxBroker;
    @Autowired
    private BTxBroker bTxBroker;

    public <V> V inCombinedTx(Callable<V> callable, String[] transactions) {
        if (callable == null) {
            return null;
        }

        Callable<V> combined = Stream
                .of(transactions)
                .filter(ele -> !StringUtils.isEmpty(ele))
                .distinct()
                .reduce(callable, (r, tx) -> {
                    switch (tx) {
                        case DataSourceBusinessConfig.A_TX_MANAGER:
                            return () -> aTxBroker.inTransaction(r);
                        case DataSourceChainConfig.B_TX_MANAGER:
                            return () -> aTxBroker.inTransaction(r);
                        default:
                            return null;
                    }
                }, (r1, r2) -> r2);
        try {
            return combined.call();
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

}

ATxBroker和BTxBroker的实现,下面这个代码应该很眼熟吧,和上面我说的很low的实现思想是一样的,只不过这里我们统一封装了而已

php 复制代码
/**
 * @Author: pp
 * @DateTime: 2022/3/29 14:39
 * @Description: 单个事务实现
 */
@Component
public class ATxBroker {


    @Transactional(DataSourceAConfig.A_TX_MANAGER)
    public <V> V inTransaction(Callable<V> callable){
        try {
            return callable.call();
        }catch (RuntimeException e){
            throw e;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }


}

/**
 * @Author: pp
 * @DateTime: 2022/3/29 14:39
 * @Description: 单个事务实现
 */
@Component
public class BTxBroker {


    @Transactional(DataSourceBConfig.B_TX_MANAGER)
    public <V> V inTransaction(Callable<V> callable){
        try {
            return callable.call();
        }catch (RuntimeException e){
            throw e;
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }


}

使用方式如下

csharp 复制代码
    @Component
    public class ABService{


        @MultiTransactional(value = {DataSourceBusinessConfig.A_TX_MANAGER, DataSourceChainConfig.B_TX_MANAGER})
        public void add(){
            //A库写入数据
            a.add();
            //B库写入数据
            b.add();
        }
         
    }

就此优雅的解决了这个问题,从技术上来说没有任何实现难度,核心思想就是我们需要在一个方法上添加两个或者多个 @Transactional(transactionManager = "你需要加的transactionManager")注解,但是原有Spring事务不支持一个方法上添加多个事务注解,我们可以通过AOP方式拦截我们自己定义的注解,然后植入多个方法,将方法上添加对应的@Transactional(transactionManager = "你需要加的transactionManager")注解,实际底层 我们只需要知道 @Transactional 注解的使用方式,知道AOP原理 ,知道Callable 可以接收返回值,并且可以抛异常,知道Stream的reduce方法即可,主要还是设计思想。

相关推荐
安之若素^5 分钟前
启用不安全的HTTP方法
java·开发语言
ruanjiananquan9912 分钟前
c,c++语言的栈内存、堆内存及任意读写内存
java·c语言·c++
chuanauc39 分钟前
Kubernets K8s 学习
java·学习·kubernetes
一头生产的驴1 小时前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf
YuTaoShao1 小时前
【LeetCode 热题 100】73. 矩阵置零——(解法二)空间复杂度 O(1)
java·算法·leetcode·矩阵
zzywxc7871 小时前
AI 正在深度重构软件开发的底层逻辑和全生命周期,从技术演进、流程重构和未来趋势三个维度进行系统性分析
java·大数据·开发语言·人工智能·spring
YuTaoShao3 小时前
【LeetCode 热题 100】56. 合并区间——排序+遍历
java·算法·leetcode·职场和发展
程序员张33 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端
llwszx6 小时前
深入理解Java锁原理(一):偏向锁的设计原理与性能优化
java·spring··偏向锁
云泽野7 小时前
【Java|集合类】list遍历的6种方式
java·python·list