同一个服务连接多个数据库下的动态数据源切换

前言

本篇文章讨论单个服务链接多个数据库的情况,实际上本次问题其实发生在特殊的过渡场景下。
原则上,笔者建议,微服务应当遵循:单一职责、高内聚低耦合、同时以降低系统维护复杂度为准则,即大多数场景下,单一服务【建议】【最好】只连接一个数据库,避免诸如:多个不同的数据模型、多个不同的业务逻辑、跨数据库的事务处理、数据一致性考量等等一系列复杂问题考量,同时还有运维成本的增加、对新手开发者不友好、不利于团队快速无脑垒业务代码。
本篇文章在单个服务链接多个数据库的场景下,讨论如下内容:

  • 基于AOP实现动态数据源
  • 基于注解简化代码
  • @Transactional注解的注意事项

提示:以下是本篇文章正文内容,下面案例可供参考

一、不使用注解的动态数据源例子

假设我们配置了如下两个数据源

代码如下(示例):

@Configuration
public class DataSourceConfig {
   @Bean
   public DataSource dataSource1() {
       //XXXXXX数据源配置代码
       return dataSource;
   }
   @Bean
   public DataSource dataSource2() {
       //XXXXXX数据源配置代码
       return dataSource;
   }
}

定义事务管理器

代码如下(示例):

@Configuration
public class TransactionConfig {

    @Autowired
    private DataSource dataSource1;

    @Autowired
    private DataSource dataSource2;

    @Bean
    public PlatformTransactionManager transactionManager1() {
        return new DataSourceTransactionManager(dataSource1);
    }

    @Bean
    public PlatformTransactionManager transactionManager2() {
        return new DataSourceTransactionManager(dataSource2);
    }
}

使用动态数据源

代码如下(示例):

@Configuration
public class DynamicDataSourceConfig {

    @Autowired
    private DataSource dataSource1;

    @Autowired
    private DataSource dataSource2;

    @Bean
    public DynamicDataSource dynamicDataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("db1", dataSource1);
        targetDataSources.put("db2", dataSource2);

        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(dataSource1); // 默认数据源

        return dynamicDataSource;
    }

    @Bean
    public DataSource dataSource() {
        return dynamicDataSource();
    }
}

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setDataSource(String dataSourceName) {
        contextHolder.set(dataSourceName);
    }

    public static String getDataSource() {
        return (String) contextHolder.get();
    }

    public static void clearDataSource() {
        contextHolder.remove();
    }
}

配置事务代理

代码如下(示例):

@Configuration
@EnableTransactionManagement
public class TransactionProxyConfig {

    @Autowired
    private DynamicDataSource dynamicDataSource;

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource);
    }
}

具体在业务代码中怎么使用

代码如下(示例):

@Service
public class UserService {

    @Autowired
    private UserDAO userDAO;

    @Autowired
    private OrderDAO orderDAO;

    @Transactional
    public void createUserAndOrder() {
        DataSourceContextHolder.setDataSource("db1");
        userDAO.createUser();

        DataSourceContextHolder.setDataSource("db2");
        orderDAO.createOrder();

        // 故意抛出异常以测试事务回滚
        if (true) {
            throw new RuntimeException("Simulating an error");
        }

        DataSourceContextHolder.clearDataSource();
    }
}

二、基于AOP和注解优化代码

数据源配置、事务管理器、设置动态数据源 同上,不再赘述

自定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestDataSource {
    String value();
}

AOP切面

@Aspect
@Component
public class DataSourceAspect {

    @Before("@annotation(testDataSource)")
    public void setDataSource(JoinPoint joinPoint, TestDataSource testDataSource) {
        DataSourceContextHolder.setDataSource(useDataSource.value());
    }

    @AfterReturning(pointcut = "execution(* com.example.service..*.*(..))")
    public void clearDataSource(JoinPoint joinPoint) {
        DataSourceContextHolder.clearDataSource();
    }
}

如何使用注解

@Service
public class UserService {

    @Autowired
    private UserDAO userDAO;

    @Autowired
    private OrderDAO orderDAO;

    @Transactional
    @UseDataSource("db1")
    public void createUser() {
        userDAO.createUser();
    }

    @Transactional
    @UseDataSource("db2")
    public void createOrder() {
        orderDAO.createOrder();
    }

    @Transactional
    public void createUserAndOrder() {
        createUser();
        createOrder();
    }
}

三、@Transactional注意事项

读完了以上内容,大家可能有一个疑问,能不能在一个方法中同时链接并操作多个数据库?
答案是:不建议😂,非常不建议;首先大家要知道Transactional注解,本身的名字就是本地事务!当然如果非要实现以上说法也可以做,请看下面的例子

方法一:手动管理事务(即显式的管理事务)

@Service
public class UserService {

    @Autowired
    private UserDAO userDAO;

    @Autowired
    private OrderDAO orderDAO;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void createUserAndOrder() {
        DataSourceContextHolder.setDataSource("db1");
        try {
            // 开始事务
            TransactionStatus status1 = transactionManager1.getTransaction(new DefaultTransactionDefinition());

            userDAO.createUser();

            DataSourceContextHolder.setDataSource("db2");
            // 开始事务
            TransactionStatus status2 = transactionManager2.getTransaction(new DefaultTransactionDefinition());

            orderDAO.createOrder();

            // 提交事务
            transactionManager2.commit(status2);
            transactionManager1.commit(status1);
        } catch (Exception e) {
            // 回滚事务
            transactionManager2.rollback(status2);
            transactionManager1.rollback(status1);
            throw e; // 或者处理异常
        } finally {
            DataSourceContextHolder.clearDataSource();
        }
    }
}

方法二:真心不建议

方案二,不想写下去了,通常谁会在同一个基础服务中的一个方法内真的直接操作两个数据库呢,如果我是你的TL,在方案评审或者CR过程中,没有指出这个问题,请在内心鄙视下你自己和你的TL

方法三:替代方案

实际垒代码的时候,如果遇到分布式的场景,建议考虑支持分布式事务的中间件来完成你的业务代码逻辑,同时看下是不是可以不做强一致性方案,转而倾向最终一致性方案


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值