若依分离版——配置多数据源(mysql和oracle),实现一个方法操作多个数据源

若依分离版——配置多数据源(mysql和oracle),实现一个方法操作多个数据源

一、背景与问题

在分布式系统中,随着业务复杂度提升,单体应用往往需要同时操作多个数据库。若依框架作为主流的Java开发框架,其多数据源配置是常见的需求。例如:

  • 订单系统需要同时操作MySQL(业务库)和Oracle(风控库)
  • 微服务架构中,不同微服务需要连接不同数据库
  • 分库分表场景下,需要访问多个物理数据库

传统单数据源方案无法满足这种需求,需要引入多数据源支持。但直接使用Spring的多数据源功能存在诸多挑战:

  1. 数据源动态切换机制复杂
  2. 事务管理需要特殊处理
  3. 查询语句需要适配不同数据库
  4. 性能优化需要特别考虑

二、基本原理

1. 多数据源的核心概念

多数据源本质上是创建多个DataSource对象,并通过某种机制动态选择当前需要使用的数据源。Spring框架通过AbstractRoutingDataSource实现这一功能,其核心原理如下:

  • 通过继承AbstractRoutingDataSource,重写determineCurrentLookupKey方法
  • 该方法返回数据源标识(如"master"或"slave")
  • 根据标识选择对应的TargetDataSource
  • 支持读写分离、多租户等场景

2. 数据源切换的实现机制

在若依框架中,数据源切换通常通过以下方式实现:

  1. 自定义注解(如@DataSource)标记方法
  2. AOP拦截器处理注解并切换数据源
  3. 使用ThreadLocal保存当前数据源标识
  4. 在SQL执行前动态切换数据源

3. 事务管理的特殊处理

多数据源事务需要满足以下条件:

  • 所有操作必须在同一个事务中
  • 需要配置事务管理器(DataSourceTransactionManager)
  • 需要确保所有数据源都支持事务

三、环境准备

1. 开发环境要求

  • JDK 1.8+
  • Spring Boot 2.7+
  • MySQL 8.0+
  • Oracle 19c+
  • Maven 3.6+

2. 依赖配置(pom.xml)

<dependencies>
    <!-- 若依核心依赖 -->
    <dependency>
        <groupId>org.jeecg</groupId>
        <artifactId>jeecg-boot-starter</artifactId>
        <version>3.6.3</version>
    </dependency>

    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.28</version>
    </dependency>

    <!-- Oracle驱动 -->
    <dependency>
        <groupId>com.oracle.database.jdbc</groupId>
        <artifactId>ojdbc8</artifactId>
        <version>23.3.0.0</version>
    </dependency>

    <!-- 数据源配置 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
</dependencies>

四、核心实现

1. 数据源配置类(DataSourceConfig.java)

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.mysql")
    public DataSource mysqlDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.oracle")
    public DataSource oracleDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public AbstractRoutingDataSource routingDataSource(
        @Qualifier("mysqlDataSource") DataSource mysql,
        @Qualifier("oracleDataSource") DataSource oracle) {
        
        AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource();
        
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("mysql", mysql);
        targetDataSources.put("oracle", oracle);
        
        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.setDefaultTargetDataSource(mysql);
        return routingDataSource;
    }
}

2. 自定义数据源切换器(DataSourceContextHolder.java)

public class DataSourceContextHolder {
    
    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
    
    public static void setDataSource(String dataSource) {
        CONTEXT.set(dataSource);
    }
    
    public static String getDataSource() {
        return CONTEXT.get();
    }
    
    public static void clearDataSource() {
        CONTEXT.remove();
    }
}

3. 数据源切换拦截器(DataSourceAspect.java)

@Aspect
@Component
public class DataSourceAspect {
    
    @Autowired
    private DataSourceConfig dataSourceConfig;
    
    @Pointcut("@annotation(com.example.annotation.DataSource)")
    public void dataSourcePointCut() {}
    
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        DataSource dataSource = signature.getMethod().getAnnotation(DataSource.class);
        
        if (dataSource != null) {
            DataSourceContextHolder.setDataSource(dataSource.value().trim());
        }
        
        try {
            return point.proceed();
        } finally {
            DataSourceContextHolder.clearDataSource();
        }
    }
}

五、完整案例

1. 数据源配置(application.yml)

spring:
  datasource:
    mysql:
      url: jdbc:mysql://localhost:3306/mysql_db?useSSL=false&serverTimezone=UTC
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
    oracle:
      url: jdbc:oracle:thin:@localhost:1521:orcl
      username: sys
      password: oracle
      driver-class-name: oracle.jdbc.OracleDriver

2. 自定义注解(DataSource.java)

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value() default "mysql";
}

3. 业务服务类(OrderService.java)

@Service
public class OrderService {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    @DataSource("mysql")
    public void createOrder(String orderNo, BigDecimal amount) {
        String sql = "INSERT INTO orders(order_no, amount) VALUES(?, ?)";
        jdbcTemplate.update(sql, orderNo, amount);
    }
    
    @DataSource("oracle")
    public void updateInventory(String productId, Integer quantity) {
        String sql = "UPDATE inventory SET stock = stock - ? WHERE product_id = ?";
        jdbcTemplate.update(sql, quantity, productId);
    }
    
    @DataSource("mysql")
    public void transferOrder(String fromNo, String toNo) {
        String sql = "UPDATE orders SET status = 'TRANSFERRED' WHERE order_no = ?";
        jdbcTemplate.update(sql, fromNo);
        
        jdbcTemplate.update("UPDATE orders SET status = 'TRANSFERRED' WHERE order_no = ?", toNo);
    }
}

4. 控制器类(OrderController.java)

@RestController
@RequestMapping("/orders")
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    @PostMapping("/create")
    public ResponseEntity<String> createOrder(@RequestParam String orderNo, 
                                              @RequestParam BigDecimal amount) {
        orderService.createOrder(orderNo, amount);
        return ResponseEntity.ok("Order created");
    }
    
    @PostMapping("/transfer")
    public ResponseEntity<String> transferOrder(@RequestParam String fromNo, 
                                               @RequestParam String toNo) {
        orderService.transferOrder(fromNo, toNo);
        return ResponseEntity.ok("Order transferred");
    }
}

六、源码解析

1. 数据源切换流程

当执行createOrder方法时:

  1. AOP拦截器获取到@DataSource("mysql")注解
  2. 调用DataSourceContextHolder.setDataSource("mysql")
  3. 在SQL执行时,从AbstractRoutingDataSource获取对应的MySQL数据源
  4. 执行SQL语句并返回结果

2. 事务管理机制

Spring的事务管理器(DataSourceTransactionManager)会:

  • 在方法执行前获取当前数据源
  • 创建事务对象并绑定到当前线程
  • 在方法执行过程中处理SQL执行
  • 在方法执行完成后提交或回滚事务

3. 事务传播机制

在transferOrder方法中,两个SQL操作共享同一个事务:

@Transactional
public void transferOrder(String fromNo, String toNo) {
    jdbcTemplate.update("UPDATE orders..."); // MySQL
    jdbcTemplate.update("UPDATE orders..."); // MySQL
}

Spring会确保这两个SQL操作在同一个事务中执行,如果任一操作失败,整个事务回滚。

七、进阶使用

1. 动态数据源选择

@DataSource("mysql")
public void createOrder(String orderNo, BigDecimal amount) {
    String sql = "INSERT INTO orders(order_no, amount) VALUES(?, ?)";
    jdbcTemplate.update(sql, orderNo, amount);
}

2. 多数据源事务管理

@Transactional(propagation = Propagation.REQUIRED)
public void transferOrder(String fromNo, String toNo) {
    jdbcTemplate.update("UPDATE orders..."); // MySQL
    jdbcTemplate.update("UPDATE inventory..."); // Oracle
}

3. 数据源优先级配置

@Bean
public AbstractRoutingDataSource routingDataSource(...) {
    Map<Object, Object> targetDataSources = new HashMap<>();
    targetDataSources.put("mysql", mysql);
    targetDataSources.put("oracle", oracle);
    
    AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource();
    routingDataSource.setTargetDataSources(targetDataSources);
    routingDataSource.setDefaultTargetDataSource(mysql); // 默认数据源
    return routingDataSource;
}

八、性能与工程实践

1. 性能优化策略

  1. 连接池配置:使用HikariCP并配置适当参数

    spring.datasource.mysql.hikari.maximum-pool-size=10
    spring.datasource.mysql.hikari.minimum-idle=5
  2. 分页查询优化:使用LIMIT和OFFSET结合索引

    SELECT * FROM orders ORDER BY id DESC LIMIT 10 OFFSET 100;
  3. 缓存机制:对常用查询结果进行缓存

    @Cacheable("orders")
    public List<Order> getOrders() {
        return jdbcTemplate.query("SELECT * FROM orders", new OrderRowMapper());
    }

2. 安全风险分析

  1. SQL注入风险:务必使用预编译语句

    String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
    jdbcTemplate.query(sql, username, password, new UserRowMapper());
  2. 数据库方言差异:Oracle和MySQL的SQL语法差异

    • 使用Hibernate方言适配
    • 对特殊语法进行封装
    • 建议使用MyBatis框架进行SQL封装

3. 异常处理机制

try {
    orderService.transferOrder(fromNo, toNo);
} catch (DataAccessException e) {
    logger.error("数据源操作失败", e);
    throw new CustomException("数据源操作失败");
}

九、常见问题与踩坑

1. 常见错误及解决办法

问题错误示例解决方案
数据源未正确切换NullPointerException确保DataSourceContextHolder正确设置
事务管理失败TransactionRequiredException确保使用@Transactional注解
SQL语法错误SyntaxErrorException使用Hibernate方言适配
性能问题SQL查询慢使用索引和分页查询

2. 常见陷阱

  1. 未清空数据源上下文:在异步任务中忘记调用clearDataSource()可能导致数据源污染
  2. 事务传播问题:跨数据源的事务需要使用Propagation.REQUIRED传播机制
  3. 连接池配置不当:未配置最大连接数可能导致连接池耗尽

3. 典型错误案例

// 错误示例:未正确配置数据源
@Bean
public DataSource dataSource() {
    return DataSourceBuilder.create()
        .url("jdbc:mysql://localhost:3306/mysql_db")
        .username("root")
        .password("root")
        .build();
}

十、最佳实践

1. 推荐使用场景

  1. 微服务架构:每个微服务连接独立数据库
  2. 分库分表:按业务划分数据源
  3. 多租户系统:按租户标识动态切换数据源
  4. 混合数据库系统:同时操作MySQL和Oracle

2. 避免使用场景

  1. 单体应用:增加复杂度且收益有限
  2. 简单CRUD系统:使用单一数据源更简单
  3. 事务需求不明确:避免不必要的复杂性
  4. 数据一致性要求不高:使用最终一致性方案更合适

3. 优化建议

  1. 使用连接池:配置HikariCP等高性能连接池
  2. SQL封装:使用MyBatis或Hibernate进行SQL封装
  3. 缓存机制:对热点数据进行缓存
  4. 监控机制:监控数据源使用情况
  5. 日志记录:记录关键操作日志

十一、总结

多数据源配置是复杂但非常重要的技术点,需要深入理解其原理和实现机制。在实际开发中,需要根据业务场景合理选择配置方式,同时注意事务管理、性能优化和安全风险。通过合理使用数据源切换机制,可以有效支持复杂的业务需求。但也要注意避免在不需要的场景下过度使用,保持系统的简洁性和可维护性。通过本文的深入解析和实践案例,相信读者能够更好地理解和应用多数据源技术,在实际项目中取得更好的效果。

最后修改于:2026年09月15日 06:09

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日