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方法即可,主要还是设计思想。

相关推荐
qq_4419960515 分钟前
Mybatis官方生成器使用示例
java·mybatis
巨大八爪鱼21 分钟前
XP系统下用mod_jk 1.2.40整合apache2.2.16和tomcat 6.0.29,让apache可以同时访问php和jsp页面
java·tomcat·apache·mod_jk
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
计算机-秋大田2 小时前
基于微信小程序的养老院管理系统的设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
魔道不误砍柴功4 小时前
简单叙述 Spring Boot 启动过程
java·数据库·spring boot
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
枫叶_v4 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
wclass-zhengge4 小时前
SpringCloud篇(配置中心 - Nacos)
java·spring·spring cloud
路在脚下@4 小时前
Springboot 的Servlet Web 应用、响应式 Web 应用(Reactive)以及非 Web 应用(None)的特点和适用场景
java·spring boot·servlet
黑马师兄4 小时前
SpringBoot
java·spring