一套系统连三种数据库,不是炫技,是制造业数字化的真实刚需。这篇文章复盘我在若依框架上踩过的多数据源深坑------从事务不回滚到动态切换翻车,每一个坑都花了至少一天。
一、为什么一个系统要连三种数据库?
先交代背景。我们的 MES 系统要跑在印刷包装厂的环境里,这些工厂的 IT 现状通常是这样的:
| 数据库 | 用途 | 为什么不能动 |
|---|---|---|
| MySQL 8 | 我们自己的 MES 主库 | 新建的,可控 |
| SQL Server | 客户的旧 ERP 系统 | 用了 8 年,不敢动 |
| Firebird | 老旧的印刷行业软件 | 用得更久,更不敢动 |
老板的要求很简单:新系统不能要求客户换掉旧系统。数据必须在三套库之间流转------工单从 ERP 同步过来、生产数据写进 MES、部分统计还要回写到 Firebird 给老软件读。
这就叫异构数据库数据同步,听起来高大上,做起来全是坑。
注:本文基于若依框架(RuoYi Spring Boot 3 版本)的多数据源方案进行实战讲解。方案思路可复用到任何 Spring Boot 项目。
二、若依框架的多数据源原理
2.1 框架做了什么
若依框架内置了基于 dynamic-datasource 的多数据源支持。核心思路是注解切换:
less
@DS("master") // 走主库
public void insertOrder() { ... }
@DS("erp") // 走客户的 SQL Server
public List<Order> syncOrder() { ... }
框架帮你做的事情:
- 通过 AOP 拦截
@DS注解,自动切换数据源 - 支持多数据源的事务管理
- 配合 Druid 连接池
看起来很简单对吧?接下来就是实际场景中爆出来的坑。
2.2 框架没做的事
若依的多数据源方案有个前提假设:所有库的表结构是你可控的。但实际情况是:
- SQL Server 里的表是别人建的,字段命名毫无规律
- Firebird 的表连文档都没有,全靠猜
- 三套库的数据类型不完全对应
这些才是真正的难度所在。
三、第一步:配置多数据源
3.1 数据源配置
ruby
# application-druid.yml
spring:
datasource:
druid:
# 主数据源 - MES 系统
master:
url: jdbc:mysql://localhost:3306/your_mes_db?useUnicode=true&characterEncoding=utf8
username: your_username
password: ${MYSQL_PWD}
driver-class-name: com.mysql.cj.jdbc.Driver
# 从数据源 - 客户 ERP(SQL Server)
erp:
url: jdbc:sqlserver://HOST:1433;DatabaseName=LEGACY_ERP
username: your_username
password: ${SQLSERVER_PWD}
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
# 从数据源 - 老旧印刷软件(Firebird)
legacy:
url: jdbc:firebirdsql://HOST:3050/PATH/TO/LEGACY.FDB?encoding=GB2312
username: your_username
password: ${FIREBIRD_PWD}
driver-class-name: org.firebirdsql.jdbc.FBDriver
3.2 引入驱动依赖
xml
<!-- pom.xml -->
<!-- MySQL 驱动(若依已自带) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- SQL Server 驱动 -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>12.6.1.jre11</version>
</dependency>
<!-- Firebird 驱动 -->
<dependency>
<groupId>org.firebirdsql.jdbc</groupId>
<artifactId>jaybird</artifactId>
<version>5.0.4.java11</version>
</dependency>
⚠️ 坑 #1:Firebird 驱动版本混乱
Jaybird 4.x 需要 Java 11+,但有些老版本 Firebird 数据库(2.x)只能用 Jaybird 3.x 连接。先确认对方 Firebird 的版本,否则连上去就是
GDS Exception。
四、真正的大坑:多数据源事务管理
4.1 切换数据源后事务不回滚
这是最常见也最致命的问题:
typescript
// ❌ 错误写法:切换数据源后,事务管理会乱
@Service
public class OrderSyncService {
@Autowired
private JdbcTemplate jdbcTemplate;
@DS("master")
@Transactional
public void syncOrderFromErp(Long orderId) {
// 第一步:从 SQL Server 读取工单数据
String sql = "SELECT * FROM TB_ORDER WHERE ID = ?";
Map<String, Object> order = jdbcTemplate.queryForMap(sql, orderId);
// 第二步:写入 MySQL(此时数据源已经切回 master)
jdbcTemplate.update("INSERT INTO mes_order (...) VALUES (...)", ...);
// 第三步:如果这里抛异常------
throw new RuntimeException("模拟异常");
// MySQL 的 INSERT 已经提交了!不会回滚!
}
}
原因 :Spring 的 @Transactional 默认绑定的是 master 的 TransactionManager。当你在 erp 数据源上执行查询后,erp 的事务已经独立提交,和 master 事务不在同一个事务管理器下。
4.2 正确做法:显式指定事务管理器 + 补偿机制
typescript
// ✅ 正确写法
@Service
public class OrderSyncService {
@Autowired
private JdbcTemplate jdbcTemplate;
@DS("erp")
public Map<String, Object> readOrderFromErp(Long orderId) {
return jdbcTemplate.queryForMap(
"SELECT * FROM TB_ORDER WHERE ID = ?", orderId
);
}
@DS("master")
@Transactional(transactionManager = "masterTransactionManager")
public void saveOrderToMes(Map<String, Object> orderData) {
jdbcTemplate.update("INSERT INTO mes_order (...) VALUES (...)", ...);
}
// 协调方法:逐条同步,加补偿逻辑
public SyncResult syncOneOrder(Long orderId) {
try {
Map<String, Object> data = readOrderFromErp(orderId);
saveOrderToMes(data);
return SyncResult.success(orderId);
} catch (Exception e) {
log.error("工单 {} 同步失败", orderId, e);
return SyncResult.fail(orderId, e.getMessage());
}
}
}
核心原则 :跨数据库的强一致性基本做不到。工程上的做法是 逐条同步 + 异常补偿 + 定时重试。
五、第二个大坑:异构数据库的数据类型映射
三个数据库的数据类型各有各的脾气:
| 数据类型 | MySQL | SQL Server | Firebird | 处理方式 |
|---|---|---|---|---|
| 日期时间 | datetime |
datetime |
TIMESTAMP |
统一转 LocalDateTime |
| 布尔 | tinyint(1) |
bit |
SMALLINT |
用 Integer 接收,代码层判断 |
| 长文本 | longtext |
nvarchar(max) |
BLOB SUB_TYPE TEXT |
统一用 String 接收 |
| 金额 | decimal(18,2) |
money |
NUMERIC(18,2) |
BigDecimal 统一处理 |
SQL Server 的 money 类型映射到 Java 时,默认会变成 Double,导致精度丢失。一个客户的工单金额 12345.67 同步后变成了 12345.669999...
ini
// ❌ 错误:精度丢失
Double amount = (Double) row.get("TOTAL_AMOUNT");
// ✅ 正确:强制用 BigDecimal
BigDecimal amount = (BigDecimal) row.get("TOTAL_AMOUNT");
六、第三个坑:Firebird 的编码问题
arduino
// ❌ 不指定编码,中文全部乱码
"jdbc:firebirdsql://HOST:3050/PATH/TO/LEGACY.FDB"
// ✅ 必须指定 encoding
"jdbc:firebirdsql://HOST:3050/PATH/TO/LEGACY.FDB?encoding=GB2312"
如果不知道对方建库时的字符集,用 Firebird 的 gstat 工具查:
perl
gstat -h YOUR_DB.GDB | grep "Character set"
拿不到的话,就用 GB2312 和 UTF8 各试一次,看哪次不出现"锟斤拷"。
七、增量同步策略
全量同步不现实。增量同步的关键是找到可靠的变更标识:
| 条件 | 方案 |
|---|---|
有 UPDATE_TIME |
用时间戳做增量标记 |
| 有自增 ID | 用 MAX(id) 做增量 |
有 CREATE_DATE |
按日期分区,每天同步当日数据 |
| 什么都没有 | 全量查询 + 本地 MD5 比对 |
如果对方允许,帮他们在 ERP 表上加一个
UPDATE_TIME字段 + 触发器自动更新,比任何取巧方案都靠谱。
八、踩坑汇总
| 坑 | 原因 | 解决 |
|---|---|---|
| 多数据源事务不回滚 | 跨 TransactionManager |
显式指定,读写分离 |
| 跨库强一致性 | 数据库物理隔离 | 逐条同步 + 补偿重试 |
| SQL Server money 精度丢失 | JDBC 映射为 Double | 强制用 BigDecimal |
| Firebird 中文乱码 | 字符集不匹配 | URL 指定 encoding |
| 异构表字段映射 | 三套库各自为政 | 建 DTO 层做转换 |
| 驱动版本不兼容 | 数据库版本和 JDBC 驱动不对 | 先确认版本再选驱动 |
九、总结
多数据源这件事,配通只需要半小时,跑稳需要半个月。核心经验:
- 别幻想分布式事务:逐条同步 + 补偿机制是工程上最务实的方案
- 提前确认数据库版本和字符集:尤其是 Firebird 这种小众数据库
- 建一层 DTO 映射:不要把异构库字段直接暴露给业务代码
- 增量同步是必须的:全量同步撑不过第一个月
📌 关于作者
我是一名全栈开发者,目前在深圳创业,专注于印刷包装行业的数字化系统建设。
技术栈:Java / Spring Boot / Vue3 / uni-app / MySQL / Redis
我会持续分享全栈开发实战、若依框架深度教程、MES & CRM 产品设计思路。
如果这篇文章帮你省下了踩坑的时间,点个赞 👍 让我知道。更多实战内容,欢迎关注微信公众号「MqCode」,每周更新。