第二十三周_S-配置多数据源二之多数据源事务

131 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 13 天,点击查看活动详情

以前写过基于 AbstractRoutingDataSource 接口实现多数据源切换的。连接。这个没法保证事务。

简介

在看公司项目的时候,其中多数据源切换的实现,仔细看了下:

具体是实现 SqlSessionFactory 、DataSourceTransactionManager、SqlSessionTemplate Bean实例,而且还能保证跨数据源的事务。

配置

基于 Druid 管理数据源:master、slave 是自己命名的,加载事务管理器的时候会使用到。

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      master:
        url: jdbc:mysql://xxx:3306/ms_031_pm?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&zeroDateTimeBehavior=convertToNull
        username: root
        password: xxx
      slave:
        url: jdbc:mysql://xxx:3306/lottery?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false&zeroDateTimeBehavior=convertToNull
        username: root
        password: xxx

注入两个数据源

其中另外的主数据源的 Bean name 是 masterDataSource。并且在下面的配置上会多一个 @Primary 注解,表示是默认的数据源。

public DataSource dataSource() {
     return new DruidDataSource();
}
@Configuration
@MapperScan(basePackages = "com.practice.thinkindynamicsource.mapper.slave", sqlSessionTemplateRef = "slaveSqlSessionTemplate")
public class SlaveDataSourceConfiguration {

    @Bean("slaveDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.druid.slave")
    public DataSource dataSource() {
        return new DruidDataSource();
    }

    @Bean(name = "slaveSqlSessionFactory")
    @Primary
    public SqlSessionFactory sqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:slave/*.xml"));
        return bean.getObject();
    }

    @Bean(name = "slaveTransactionManager")
    @Primary
    public DataSourceTransactionManager transactionManager(@Qualifier("slaveDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "slaveSqlSessionTemplate")
    @Primary
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("slaveSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

}

数据源切换

到这里,其实已经实现了数据源的切换。如下所示的代码,就会执行不同的数据源。

public Map testDataSource() {
        Map slaveMap = slaveTest.slaveTest();
        Map masterMap = masterTest.masterTest();
        int i=1/0;
        Map map = new HashMap();
        map.put("masterMap", masterMap);
        map.put("slaveMap", slaveMap);
        return map;
    }

但是和事务回滚没一毛钱的关系。我们可以加上如下的代码,但是只对配置的数据源有效。

默认的 Transactional 注解不支持多个数据库事务。

@Transactional(transactionManager = "masterTransactionManager")

因此我们可以实现一个支持多个数据源的事务的注解 。

多数据事务注解

注解

就是一个注解:一个数组属性

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface MultiDataSourceTransactional {
    String[] transactionManagers();
}

切面实现

这里很巧妙的将 AOP 结合起来,手动声明事务管理器,然后关闭。

  1. 声明事务
  2. 执行业务代码
  3. 提交或者回退
@Component
@Aspect
public class MultiTransactionAop {

    /**
     * 1.使用 ThreadLocal 保证线程安全
     * 2.Stack 先进先出,事务的声明与提交/回滚对应
     */
    private static final ThreadLocal<Stack<Map<DataSourceTransactionManager, TransactionStatus>>> THREAD_LOCAL = new ThreadLocal<>();
    
    /**
     * Spring 上下文,用于获取事务管理器
     */
    @Autowired
    private ApplicationContext applicationContext;
    
    /**
     * 事务声明
     */
    private DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    {
        // 非只读模式
        def.setReadOnly(false);
        // 事务隔离级别:采用数据库的
        def.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
        // 事务传播行为
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    }

    /**
     * 切入点
     */
    @Pointcut("@annotation(com.practice.thinkindynamicsource.annotation.MultiDataSourceTransactional)")
    public void pointcut() {
    }

    /**
     * @Around 环绕
     * @param joinPoint
     * @param transactional
     */
    @Around("@annotation(transactional)")
    public void around(ProceedingJoinPoint joinPoint,MultiDataSourceTransactional transactional)  {
        String[] transactionManagerNames = transactional.transactionManagers();
        Stack<Map<DataSourceTransactionManager, TransactionStatus>> pairStack = new Stack<>();
         for (String transactionName : transactionManagerNames) {
            DataSourceTransactionManager manager = applicationContext.getBean(transactionName, DataSourceTransactionManager.class);
            TransactionStatus status = manager.getTransaction(def);
            Map<DataSourceTransactionManager, TransactionStatus> transactionMap = new HashMap<>();
            transactionMap.put(manager, status);
            pairStack.push(transactionMap);
        }
        THREAD_LOCAL.set(pairStack);

        try{
            joinPoint.proceed();
            while (!THREAD_LOCAL.get().isEmpty()) {
                Map<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
                pair.forEach((key, value) -> key.commit(value));
            }
        } catch (Throwable e) {
            while (!THREAD_LOCAL.get().isEmpty()) {
                Map<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
                pair.forEach((key, value) -> key.rollback(value));
            }
            throw new RuntimeException(e);
        } finally {
            THREAD_LOCAL.remove();
        }

    }
}

这里的 Around 注解,可以拆分为 Before/AfterReturning/AfterThrowing 三个注解实现。

可以参考我的AOP实战文章

注解使用

很简单,就是把定义的注解加在方法上即可

    @MultiDataSourceTransactional(transactionManagers = {"masterTransactionManager", "slaveTransactionManager"})
    public Map testDataSource1() {
        Map slaveMap = slaveTest.slaveTest();
        Map masterMap = masterTest.masterTest();
//        int i=1/0;
        Map map = new HashMap();
        map.put("masterMap", masterMap);
        map.put("slaveMap", slaveMap);
        return map;
    }

跨数据源事务总结

  • 这里使用栈,先进先出的结构,声明事务和提交事务/回滚事务的顺序应该相反的
  • 使用 ThreaLocal 可以保证线程安全,但是清除一定要放在 finally 保证一定执行,防止内存溢出

总结

多数据源事物的保证:其实就是手动管理事务,不交给 Spring 管理,只不过结合 AOP、注解 ,可以增加代码复用性。

现在有点感觉学习多种点、线的知识,怎么把它组装成一个面,也是质的一步!