1. 项目背景与需求分析
最近在开发一个企业级应急管理系统时,遇到了一个典型的多数据源场景。系统原本使用PostgreSQL作为主数据库存储核心配置和元数据,但随着业务扩展,需要接入外部推送的业务数据,这些数据存储在MySQL中。这就带来了一个技术挑战:如何在同一个Java后端服务中同时操作两种不同类型的数据库?
这种需求在实际开发中非常常见,比如:
- 新老系统迁移过渡期间需要同时访问新旧数据库
- 不同业务模块使用不同类型的数据库(如关系型+图数据库)
- 需要同时访问本地数据库和云端数据库
我们的核心需求可以总结为:
- 保持现有PostgreSQL数据操作不受影响
- 新增MySQL数据源支持
- 确保两类数据源的操作完全隔离,避免SQL语法混淆
- 提供清晰的事务管理机制
2. 技术选型与架构设计
2.1 主流多数据源方案对比
在Java生态中,实现多数据源主要有以下几种方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| AbstractRoutingDataSource | 轻量级,配置简单 | 动态切换不够直观 | 简单多数据源 |
| 多套MyBatis配置 | 隔离彻底,易于维护 | 配置稍复杂 | 异构数据库 |
| JPA多EntityManager | 面向对象程度高 | 学习曲线陡峭 | JPA项目 |
| 中间件方案(ShardingSphere) | 功能强大 | 重量级 | 分库分表 |
考虑到我们使用的是MyBatis-Plus,且需要操作两种完全不同类型的数据库,最终选择了"多套MyBatis配置"方案。这种方案虽然配置工作量稍大,但隔离性最好,能够彻底避免SQL语法混淆的问题。
2.2 整体架构设计
系统架构分为以下几个关键层次:
- 数据源层:通过Spring Boot的配置机制,初始化PostgreSQL和MySQL两个独立的数据源
- ORM层:为每个数据源配置独立的MyBatis SqlSessionFactory和Mapper扫描路径
- 事务层:为每个数据源配置独立的事务管理器
- 业务层:通过包路径隔离不同数据源的操作,Service根据需要注入对应的Mapper
这种分层设计确保了各层职责清晰,且扩展性强。如果需要新增第三个数据源,只需要按照相同模式增加配置即可。
3. 详细实现步骤
3.1 数据源配置
首先在application.yml中配置两个数据源的连接信息:
yaml复制spring:
datasource:
postgres:
driver-class-name: org.postgresql.Driver
jdbc-url: jdbc:postgresql://localhost:5432/core_db
username: postgres
password: postgres123
hikari:
maximum-pool-size: 50
minimum-idle: 5
idle-timeout: 30000
connection-timeout: 120000
mysql:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/biz_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: mysql123
hikari:
maximum-pool-size: 20
minimum-idle: 2
idle-timeout: 30000
connection-timeout: 60000
这里有几个关键点需要注意:
- PostgreSQL和MySQL的JDBC驱动类名不同
- MySQL连接URL需要指定时区等参数
- 连接池配置根据业务特点做了区分(核心库连接数更多)
3.2 数据源Java配置
创建DataSourceConfig类将配置转换为Spring Bean:
java复制@Configuration
public class DataSourceConfig {
@Bean(name = "postgresDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.postgres")
public DataSource postgresDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean(name = "mysqlDataSource")
@ConfigurationProperties(prefix = "spring.datasource.mysql")
public DataSource mysqlDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean(name = "postgresTransactionManager")
@Primary
public PlatformTransactionManager postgresTransactionManager(
@Qualifier("postgresDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "mysqlTransactionManager")
public PlatformTransactionManager mysqlTransactionManager(
@Qualifier("mysqlDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
关键点说明:
- PostgreSQL数据源标记为@Primary,作为默认数据源
- 每个数据源都有独立的事务管理器
- 使用HikariCP作为连接池实现
3.3 MyBatis独立配置
为每个数据源创建独立的MyBatis配置类:
PostgreSQL配置:
java复制@Configuration
@MapperScan(
basePackages = "com.example.mapper.postgres",
sqlSessionTemplateRef = "postgresSqlSessionTemplate"
)
public class PostgresMyBatisConfig {
@Resource
private DataSource postgresDataSource;
@Bean(name = "postgresSqlSessionFactory")
public SqlSessionFactory postgresSqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setDataSource(postgresDataSource);
factoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/postgres/*.xml"));
factoryBean.setTypeAliasesPackage("com.example.entity.postgres");
return factoryBean.getObject();
}
@Bean(name = "postgresSqlSessionTemplate")
public SqlSessionTemplate postgresSqlSessionTemplate(
@Qualifier("postgresSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
MySQL配置类似,主要区别在于:
- 扫描的Mapper包路径不同(com.example.mapper.mysql)
- XML映射文件存放路径不同(mapper/mysql)
- 实体类包路径不同(com.example.entity.mysql)
3.4 项目结构规范
为了保持代码清晰,项目结构按照以下方式组织:
code复制src/main/java
├── com.example
│ ├── config
│ │ ├── DataSourceConfig.java
│ │ ├── PostgresMyBatisConfig.java
│ │ └── MysqlMyBatisConfig.java
│ ├── controller
│ │ ├── postgres
│ │ └── mysql
│ ├── service
│ │ ├── impl
│ │ │ ├── postgres
│ │ │ └── mysql
│ ├── mapper
│ │ ├── postgres
│ │ └── mysql
│ └── entity
│ ├── postgres
│ └── mysql
src/main/resources
├── mapper
│ ├── postgres
│ └── mysql
这种结构确保了不同数据源的相关代码物理隔离,大大降低了误用的可能性。
4. 业务层实现与使用
4.1 Mapper接口定义
PostgreSQL的Mapper接口:
java复制package com.example.mapper.postgres;
@Repository
public interface SystemConfigMapper {
@Select("SELECT * FROM sys_config WHERE config_key = #{key}")
SystemConfig getByKey(String key);
// PostgreSQL特定的字符串聚合函数
@Select("SELECT STRING_AGG(username, ',') FROM sys_user WHERE dept_id = #{deptId}")
String getUsernamesByDept(Long deptId);
}
MySQL的Mapper接口:
java复制package com.example.mapper.mysql;
@Repository
public interface BizDataMapper {
@Select("SELECT * FROM biz_data WHERE create_time > #{time}")
List<BizData> listAfterTime(Date time);
// MySQL特定的字符串聚合函数
@Select("SELECT GROUP_CONCAT(product_name) FROM biz_product WHERE category = #{category}")
String getProductsByCategory(String category);
}
注意两个Mapper接口放在不同的包下,且使用了各自数据库特有的聚合函数。
4.2 Service层使用
在Service中注入对应数据源的Mapper:
java复制@Service
@RequiredArgsConstructor
public class DataIntegrationService {
private final SystemConfigMapper systemConfigMapper; // PostgreSQL
private final BizDataMapper bizDataMapper; // MySQL
public Map<String, Object> getCombinedData(Long deptId, String category) {
Map<String, Object> result = new HashMap<>();
// 查询PostgreSQL数据
String users = systemConfigMapper.getUsernamesByDept(deptId);
result.put("users", users);
// 查询MySQL数据
String products = bizDataMapper.getProductsByCategory(category);
result.put("products", products);
return result;
}
@Transactional(transactionManager = "mysqlTransactionManager")
public void updateBizData(BizData data) {
// 此方法需要MySQL事务
bizDataMapper.update(data);
}
}
4.3 事务管理注意事项
事务管理是多数据源中最容易出问题的部分,需要特别注意:
- 查询操作:纯查询不需要事务注解
- 写操作:必须指定正确的事务管理器
- 跨数据源操作:不能放在同一个事务中
正确的事务使用示例:
java复制// 正确:指定MySQL事务管理器
@Transactional(transactionManager = "mysqlTransactionManager")
public void saveBizData(BizData data) {
bizDataMapper.insert(data);
}
// 错误:没有指定事务管理器,将使用默认的PostgreSQL事务管理器
@Transactional
public void saveBizData(BizData data) {
bizDataMapper.insert(data); // 事务不会生效!
}
// 错误:尝试跨数据源事务(需要分布式事务支持)
@Transactional
public void updateBoth(Config config, BizData data) {
systemConfigMapper.update(config); // PostgreSQL
bizDataMapper.update(data); // MySQL
// 这里两个更新不在同一个事务中!
}
5. 常见问题与解决方案
5.1 启动时报错"找不到DataSource Bean"
可能原因:
- 配置类没有被Spring扫描到
- 数据源前缀配置错误
- 包扫描范围不正确
解决方案:
- 确保主启动类在配置类的上级包中
- 检查application.yml中的前缀与@ConfigurationProperties是否一致
- 确认@ComponentScan包含配置类所在包
5.2 SQL执行时报语法错误
可能原因:
- 在PostgreSQL的Mapper中使用了MySQL特有语法
- 在MySQL的Mapper中使用了PostgreSQL特有语法
解决方案:
- 严格区分Mapper的包路径
- 为不同数据库编写对应的SQL
- 可以使用数据库方言注解标记方法:
java复制@PostgresOnly
@Select("SELECT STRING_AGG(...)")
public String getAggregatedData();
@MysqlOnly
@Select("SELECT GROUP_CONCAT(...)")
public String getAggregatedData();
5.3 事务不生效问题
可能原因:
- 忘记添加@Transactional注解
- 使用了默认的事务管理器
- 异常类型不是RuntimeException
解决方案:
- 写操作必须添加@Transactional
- 非主数据源操作必须指定transactionManager
- 检查异常处理逻辑:
java复制// 正确示例
@Transactional(transactionManager = "mysqlTransactionManager",
rollbackFor = Exception.class)
public void updateData(BizData data) throws Exception {
// ...
}
5.4 性能优化建议
- 根据业务特点调整连接池参数
- 为不同数据源配置不同的连接池监控
- 考虑使用二级缓存减轻数据库压力
- 高频跨库查询可以考虑使用本地缓存
连接池监控配置示例:
java复制@Bean
public MeterRegistryCustomizer<MeterRegistry> metrics() {
return registry -> {
HikariDataSource postgresDs = postgresDataSource();
if (postgresDs != null) {
registry.register("postgres.connections",
new HikariDataSourceMetrics(postgresDs));
}
HikariDataSource mysqlDs = mysqlDataSource();
if (mysqlDs != null) {
registry.register("mysql.connections",
new HikariDataSourceMetrics(mysqlDs));
}
};
}
6. 扩展思考与进阶方案
6.1 动态数据源路由
对于更复杂的场景,可以考虑实现动态数据源路由:
java复制public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
// 使用示例
public void someMethod() {
DataSourceContextHolder.setDataSourceType("mysql");
try {
// 这里所有操作都会使用MySQL数据源
bizDataMapper.query(...);
} finally {
DataSourceContextHolder.clear();
}
}
6.2 分布式事务支持
如果需要严格的事务一致性,可以引入Seata等分布式事务解决方案:
java复制@GlobalTransactional
public void crossDatabaseUpdate() {
// 操作PostgreSQL
systemConfigMapper.update(...);
// 操作MySQL
bizDataMapper.update(...);
// 两个操作会在一个分布式事务中
}
6.3 多数据源与微服务
在多数据源架构演进到微服务时,建议:
- 将不同数据源拆分为独立服务
- 通过API网关聚合数据
- 使用GraphQL等方案解决跨服务数据查询
7. 项目实践心得
在实际项目中实施多数据源方案,我总结了以下几点经验:
-
命名规范至关重要:从包名到Bean名称,清晰的命名约定可以避免很多混淆。我们团队约定所有PostgreSQL相关的名称都包含"postgres",MySQL相关的包含"mysql"。
-
代码审查重点:在代码审查时需要特别关注:
- Mapper注入是否正确
- 事务注解是否使用了正确的事务管理器
- SQL语法是否匹配目标数据库
-
测试策略调整:多数据源环境下,测试需要更全面:
java复制@SpringBootTest class MultiDataSourceTest { @Autowired private SystemConfigMapper systemConfigMapper; // PostgreSQL @Autowired private BizDataMapper bizDataMapper; // MySQL @Test void testPostgresQuery() { // 测试PostgreSQL查询 } @Test @Transactional(transactionManager = "mysqlTransactionManager") void testMysqlTransaction() { // 测试MySQL事务 } } -
监控与运维:生产环境中需要为不同数据源配置独立的监控:
- 不同的连接池指标
- 独立的慢SQL监控
- 差异化的告警策略
-
文档完整性:多数据源项目的文档需要特别详细,包括:
- 架构决策记录(ADR)
- 数据源配置手册
- 常见问题排查指南
- 新成员上手checklist
这套方案在我们项目中运行稳定,成功支持了日均10万+的跨库查询操作。最关键的是建立了清晰的代码规范和操作约束,使得团队新成员也能快速上手,避免了常见的多数据源陷阱。