若依分离版——配置多数据源(mysql和oracle),实现一个方法操作多个数据源
若依分离版——配置多数据源(mysql和oracle),实现一个方法操作多个数据源
一、背景与问题
在分布式系统中,随着业务复杂度提升,单体应用往往需要同时操作多个数据库。若依框架作为主流的Java开发框架,其多数据源配置是常见的需求。例如:
- 订单系统需要同时操作MySQL(业务库)和Oracle(风控库)
- 微服务架构中,不同微服务需要连接不同数据库
- 分库分表场景下,需要访问多个物理数据库
传统单数据源方案无法满足这种需求,需要引入多数据源支持。但直接使用Spring的多数据源功能存在诸多挑战:
- 数据源动态切换机制复杂
- 事务管理需要特殊处理
- 查询语句需要适配不同数据库
- 性能优化需要特别考虑
二、基本原理
1. 多数据源的核心概念
多数据源本质上是创建多个DataSource对象,并通过某种机制动态选择当前需要使用的数据源。Spring框架通过AbstractRoutingDataSource实现这一功能,其核心原理如下:
- 通过继承AbstractRoutingDataSource,重写determineCurrentLookupKey方法
- 该方法返回数据源标识(如"master"或"slave")
- 根据标识选择对应的TargetDataSource
- 支持读写分离、多租户等场景
2. 数据源切换的实现机制
在若依框架中,数据源切换通常通过以下方式实现:
- 自定义注解(如@DataSource)标记方法
- AOP拦截器处理注解并切换数据源
- 使用ThreadLocal保存当前数据源标识
- 在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.OracleDriver2. 自定义注解(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方法时:
- AOP拦截器获取到
@DataSource("mysql")注解 - 调用
DataSourceContextHolder.setDataSource("mysql") - 在SQL执行时,从
AbstractRoutingDataSource获取对应的MySQL数据源 - 执行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. 性能优化策略
连接池配置:使用HikariCP并配置适当参数
spring.datasource.mysql.hikari.maximum-pool-size=10 spring.datasource.mysql.hikari.minimum-idle=5分页查询优化:使用
LIMIT和OFFSET结合索引SELECT * FROM orders ORDER BY id DESC LIMIT 10 OFFSET 100;缓存机制:对常用查询结果进行缓存
@Cacheable("orders") public List<Order> getOrders() { return jdbcTemplate.query("SELECT * FROM orders", new OrderRowMapper()); }
2. 安全风险分析
SQL注入风险:务必使用预编译语句
String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; jdbcTemplate.query(sql, username, password, new UserRowMapper());数据库方言差异: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. 常见陷阱
- 未清空数据源上下文:在异步任务中忘记调用
clearDataSource()可能导致数据源污染 - 事务传播问题:跨数据源的事务需要使用
Propagation.REQUIRED传播机制 - 连接池配置不当:未配置最大连接数可能导致连接池耗尽
3. 典型错误案例
// 错误示例:未正确配置数据源
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url("jdbc:mysql://localhost:3306/mysql_db")
.username("root")
.password("root")
.build();
}十、最佳实践
1. 推荐使用场景
- 微服务架构:每个微服务连接独立数据库
- 分库分表:按业务划分数据源
- 多租户系统:按租户标识动态切换数据源
- 混合数据库系统:同时操作MySQL和Oracle
2. 避免使用场景
- 单体应用:增加复杂度且收益有限
- 简单CRUD系统:使用单一数据源更简单
- 事务需求不明确:避免不必要的复杂性
- 数据一致性要求不高:使用最终一致性方案更合适
3. 优化建议
- 使用连接池:配置HikariCP等高性能连接池
- SQL封装:使用MyBatis或Hibernate进行SQL封装
- 缓存机制:对热点数据进行缓存
- 监控机制:监控数据源使用情况
- 日志记录:记录关键操作日志
十一、总结
多数据源配置是复杂但非常重要的技术点,需要深入理解其原理和实现机制。在实际开发中,需要根据业务场景合理选择配置方式,同时注意事务管理、性能优化和安全风险。通过合理使用数据源切换机制,可以有效支持复杂的业务需求。但也要注意避免在不需要的场景下过度使用,保持系统的简洁性和可维护性。通过本文的深入解析和实践案例,相信读者能够更好地理解和应用多数据源技术,在实际项目中取得更好的效果。
评论已关闭