若依框架如何配置多数据源?同时连接 MySQL、SQL Server、Firebird 三种数据库

一套系统连三种数据库,不是炫技,是制造业数字化的真实刚需。这篇文章复盘我在若依框架上踩过的多数据源深坑------从事务不回滚到动态切换翻车,每一个坑都花了至少一天。


一、为什么一个系统要连三种数据库?

先交代背景。我们的 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 默认绑定的是 masterTransactionManager。当你在 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 驱动不对 先确认版本再选驱动

九、总结

多数据源这件事,配通只需要半小时,跑稳需要半个月。核心经验:

  1. 别幻想分布式事务:逐条同步 + 补偿机制是工程上最务实的方案
  2. 提前确认数据库版本和字符集:尤其是 Firebird 这种小众数据库
  3. 建一层 DTO 映射:不要把异构库字段直接暴露给业务代码
  4. 增量同步是必须的:全量同步撑不过第一个月

📌 关于作者

我是一名全栈开发者,目前在深圳创业,专注于印刷包装行业的数字化系统建设。

技术栈:Java / Spring Boot / Vue3 / uni-app / MySQL / Redis

我会持续分享全栈开发实战、若依框架深度教程、MES & CRM 产品设计思路。

如果这篇文章帮你省下了踩坑的时间,点个赞 👍 让我知道。更多实战内容,欢迎关注微信公众号「MqCode」,每周更新。

相关推荐
颜酱15 分钟前
# 02 | 搭骨架:用 LangGraph 编排 12 步工作流(思路)
前端·人工智能·后端
颜酱16 分钟前
02 | 搭骨架:用 LangGraph 编排 12 步工作流
前端·人工智能·后端
嘉伟桑1 小时前
Python调用电价API返回JSON示例:分时电价、现货电价和字段解析
后端
武子康2 小时前
Shippy:确定性工具、会话级 Sandbox 与 Live-Data Eval(4 类收敛 + 7 步实现方案 + 6 类评测指标)
前端·人工智能·后端
我叫黑大帅2 小时前
我为什么单一消费者的场景下,要用 Redis List 当消息队列?
redis·后端·面试
AskHarries2 小时前
文件上传系统
后端
止语Lab3 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活3 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困3 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn3 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端