ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+MyBatisPlus多数据源实战:MySQL与SqlServer双库切换完整方案

SpringBoot+MyBatisPlus多数据源实战:MySQL与SqlServer双库切换完整方案 做项目遇到多数据源需求往往是两种让人头大的情况一种是历史遗留系统太老新业务想用MySQL旧数据又躺在SqlServer里动不了另一种是刚接手一个项目一看配置一半库在MySQL、一半在Oracle或SqlServer领导还甩一句“尽快把对接调通”。我刚接触SpringBoot多数据源的时候也是从一头雾水开始翻文档、踩坑、改配置折腾了整整一个下午才把MySQL和SqlServer两个库的数据在同一个接口里查出来。这篇文章就把我实测跑通的完整方案写下来围绕SpringBoot、多数据源、MySQL、SqlServer、MyBatisPlus这套组合从环境准备、连接串避坑、代码实现到真机排障一次讲清楚。不管你是正在准备毕设的学生还是公司里临时被叫去接报表库的CRUD开发照着这篇操作基本都能把双数据源跑起来。1. 多数据源的常见场景与方案选型1.1 什么时候真的需要多数据源先说结论能单库解决就别双库多数据源不是拿来炫技的。但在下面这几类场景里绕不开它。第一类是业务库与报表库分离。MySQL存核心交易数据SqlServer专门跑老业务方的报表两边数据都需要在同一个管理后台展示。第二类是主从读写分离的雏形。主库写、从库读虽然一般用中间件做但中小项目直接配两个数据源按方法分流也很常见。第三类是系统迁移过渡期旧数据在SqlServer新功能基于MySQL开发双向同步没完全跑通前必须在代码层同时访问两个库。第四类是第三方对接比如要拉取对方ERP存储在SqlServer里的单据对方只给只读账号。这些场景有个共同点不是“想用两个数据库”而是“业务本身横跨两个库”。理解了这一点后面做技术选型就有方向了——多数据源的解法要围绕“怎么让开发写代码时感觉像操作一个库”。1.2 技术方案的取舍注解路由与多套SessionFactory的权衡实现多数据源业界比较主流的路线有两类。一类是手动创建多个DataSource、多个SqlSessionFactory每套Mapper指定自己的数据源。这种做法的好处是彻底隔离事务边界清晰缺点同样明显——配置量巨大每加一个库就要重复一套Bean注入而且Mapper包扫描稍微写错一个点就启动失败。另一类就是我这次采用的方案基于dynamic-datasource-spring-boot-starter苞米豆出品的那款社区很常用的动态数据源组件配合DS注解做路由。它的核心思路是维护一个数据源Map利用AOP拦截DS注解在每次数据库操作前将当前线程的数据源切换到指定目标上操作完成再恢复。开发感知极低只需要在Service方法上加注解Mapper接口里写SQL完全不用关心数据源来自哪个库。我选择后者主要有三个考虑一是MyBatisPlus官方周边对它的支持非常成熟分页插件、逻辑删除这些高级功能可以无缝使用二是配置结构直观YAML里一目了然三是切换粒度灵活既可以在类上统一指定也可以在方法上单独覆盖。实际测下来这个方案在中小型项目里是性价比最高的代码量少、排查问题也方便。当然方案再方便也有前提必须理解DS的切换过程和Spring事务的边界否则会出现“注解加在Service上却不生效”“同一个事务里切换数据源报错”这类经典问题。这部分我放到后面实战排障章节细讲。2. 环境准备与连接串细节最容易翻车的环节2.1 数据库版本与驱动类型的匹配关系很多人上来就写配置报错后一脸懵其实就是驱动和数据库版本对不上。多数据源环境下这个问题会被放大——两套驱动同时加载任何一个不匹配都会导致项目启动失败。我这次用的环境是MySQL 8.0.28、SqlServer 2016、SpringBoot 2.7.x、MyBatisPlus 3.5.3.1。对应的驱动坐标如下MySQL驱动mysql-connector-j旧坐标是mysql-connector-java8.x版本连接串里的驱动类名是com.mysql.cj.jdbc.Driver。SqlServer驱动mssql-jdbc微软官方JDBC最新稳定版对应的驱动类名是com.microsoft.sqlserver.jdbc.SQLServerDriver。注意别再用老旧的net.sourceforge.jtds.jdbc.Driver那是第三方开源的JTDS方案对新版SqlServer兼容性不好官方驱动才是首选。这里插一个经验如果你的SqlServer是2008或更老可能还要回头找低版本的mssql-jdbc包因为新版驱动会放弃对老版本数据库实例的支持。反过来如果你的数据库已经很新比如2022驱动版本太老也会报“证书链信任”或“TLS协议版本过低”之类的奇怪错误。原则就是驱动版本不低于数据库版本的主版本号。安装数据库方面也有个细节。MySQL的安装包解压后要手动初始化数据目录通常执行mysqld --initialize-insecure然后自己设置root密码用Navicat等图形工具连接前还要确认端口3306没被占用。SqlServer安装则注意实例名和端口默认端口是1433不过企业里很多DBA会改端口连接串要跟着改。图形化工具方面SQL Server Management StudioSSMS是官方标配如果导入数据总提示“数据无效”八成是导入向导里字段类型映射没选对后面速查表里我会专门列几条。2.2 连接串参数里的学问时区、SSL、字符集都不能忽视单数据源时代连接串写错顶多连不上多数据源时代两个库的细节差异全暴露在一次启动过程里。我把两个连接串的关键参数拆开讲。MySQL连接串一种实用写法是jdbc:mysql://127.0.0.1:3306/business_db?useUnicodetruecharacterEncodingutf8zeroDateTimeBehaviorconvertToNulluseSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue逐个解释一下这些参数的实际作用。serverTimezoneAsia/Shanghai用于解决驱动与数据库服务器时区不一致导致的日期偏移报错useSSLfalse是因为本地开发环境通常没有配置SSL证书Java 8以后默认对MySQL连接是尝试启用SSL的两边没配上就会报“SSL connection error”。如果你用的是MySQL 8.0.29以上版本可能还会碰到allowPublicKeyRetrievaltrue这个参数它是用来解决认证插件使用caching_sha2_password时返回Public Key Retrieval异常的问题。zeroDateTimeBehaviorconvertToNull则避免数据库中0000-00-00 00:00:00这样的零日期值让Java报错。SqlServer连接串相对简洁jdbc:sqlserver://127.0.0.1:1433;DatabaseNamereport_db;encrypttrue;trustServerCertificatetrue;loginTimeout30重点在encrypttrue和trustServerCertificatetrue。微软从2017年之后的驱动版本开始默认启用加密连接如果SqlServer实例本身没强制加密你反而要在连接串里显式encryptfalse或者用trustServerCertificatetrue跳过证书校验。这两个参数经常导致“无法登录”“连接时出错”这类问题尤其是数据库服务器和Java应用不在同一台机器上时。loginTimeout是为了防止应用启动时SqlServer不可用导致线程长时间挂住实测很有效。3. 核心代码实现从依赖到配置再到Service调用3.1 Maven依赖与YAML配置的完整写法动手之前先说一个依赖版本的问题。SpringBoot和MyBatisPlus、动态数据源组件三者之间存在版本耦合我踩过“SpringBoot版本太高”导致的自动配置不生效的坑。如果你用的是SpringBoot 2.7.x推荐引入的依赖版本如下dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.5.2/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency动态数据源组件内部的spring-boot-autoconfigure版本如果和你的SpringBoot主版本差太远会出现DataSourceAutoConfiguration被覆盖后默认数据库连接池加载异常的情况。我的建议是在SpringBoot 2.7左右的项目中优先选用3.5.x的动态数据源版本没必要追最新。接着看application.yml的配置这是整套方案的核心之一spring: datasource: dynamic: primary: master strict: true datasource: master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/business_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 report: driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver url: jdbc:sqlserver://127.0.0.1:1433;DatabaseNamereport_db;encrypttrue;trustServerCertificatetrue;loginTimeout30 username: sa password: your_password几个字段的解释要说透primary指定默认数据源DS没标注时走这个strict设为true后如果代码里指定了不存在的数据源名会直接抛异常而不是静默回退开发阶段强烈建议开启能帮你第一时间暴露把数据源名写错的低级问题datasource下的master和report就是自定义的数据源标识DS(report)里的字符串必须对应这里。MyBatisPlus相关的配置建议放在另一个配置块mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.example.multids.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmapper-locations这里用classpath*:前缀是防止多个数据源的Mapper XML放在不同jar包或模块中时扫描不到。我在单库项目里习惯了classpath:前缀但多数据源场景最好统一使用classpath*:因为动态数据源方案中Mapper接口可能分布在多个包路径下。3.2 数据源配置类的正确打开方式接入动态数据源组件后还需要不要自己定义DataSourceConfig答案是大多数情况下不要组件已经自动装配了路由数据源你再手动声明一个DataSourceBean就会把自动配置覆盖掉轻则数据源路由失效重则启动直接报错。这个点非常关键我自己就犯过一次。正确做法是只做两件事配置主类扫描MapperScan把Mapper接口扫进容器再定义分页插件Bean。Mapper扫描位置要与业务分包保持一致。我的工程结构供参考com.example.multids ├── controller ├── service │ └── impl ├── mapper │ ├── BusinessUserMapper.java │ └── ReportOrderMapper.java ├── entity ├── config │ └── MybatisPlusConfig.java对应的启动类或配置类上写SpringBootApplication MapperScan(com.example.multids.mapper) public class MultidsApplication { public static void main(String[] args) { SpringApplication.run(MultidsApplication.class, args); } }如果两个数据源的Mapper需要分得特别开也可以在配置上用MapperScan(basePackages com.example.multids.mapper.mysql)和MapperScan(basePackages com.example.multids.mapper.sqlserver)做两个扫描不会造成冲突因为底层SqlSessionTemplate是动态数据源组件串起来的。分页插件配置需要显式指定方言。在这个场景里MySQL用MySQL方言、SqlServer用SQLServer方言不能只配置一个MyBatisPlus的PaginationInnerInterceptor让它自己猜。因为DS切换发生在MyBatis执行层面分页插件的方言识别如果沿用默认机制在某些代理链路上会选错方言导致生成的count语句和limit语句文法不对。样例配置如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor mysqlPage new PaginationInnerInterceptor(DbType.MYSQL); mysqlPage.setMaxLimit(1000L); PaginationInnerInterceptor sqlServerPage new PaginationInnerInterceptor(DbType.SQL_SERVER2005); sqlServerPage.setMaxLimit(1000L); // 注意这里只保留一个分页拦截器实例通过动态数据源机制路由方言 interceptor.addInnerInterceptor(mysqlPage); return interceptor; } }这里有个细节很多人不知道PaginationInnerInterceptor内部支持根据Connection的元数据自动识别方言前提是你不要手动覆盖它的方言类型。如果你只配置一个MySQL方言实例切到SqlServer库执行分页查询时会生成带LIMIT的语法而SqlServer 2016虽然也支持OFFSET FETCH但2008及以下版本不支持。保守做法是配置一个不指定方言的PaginationInnerInterceptor或者用我上面写法生成两个但通过条件识别实测更稳妥的是直接不指定方言让插件通过JDBC的DatabaseMetaData判断。不过MyBatisPlus有些版本在跨数据源切换时对DatabaseMetaData的缓存处理有瑕疵如果你发现切库后第一条SQL方言判断正确、后续判断错误就需要手动指定方言了。这个我在第4章问题排查里详细说。3.3 Service层示例一套代码操作两个库配置到位后业务代码里切换数据源是很自然的。拿一个典型需求来演示管理后台的订单列表页需要同时展示MySQL业务库里最新的订单信息以及SqlServer报表库里去年同期订单量。先建实体和Mapper。这里用MyBatisPlus的BaseMapper能少写很多基础CRUD也给后面测试分页功能提供了现成的方法。public class BusinessOrder { private Long id; private String orderNo; private BigDecimal amount; private LocalDateTime createTime; } public interface BusinessOrderMapper extends BaseMapperBusinessOrder { // 这里可以写自定义查询 }SqlServer端的实体类似但注意字段类型可能不同。SqlServer里如果金额字段是decimal(18,2)Java侧用BigDecimal没问题如果是numeric也是同理。日期字段如果是datetime映射到LocalDateTime时要确认驱动版本支持java.time类型mssql-jdbc从6.2版本开始完全支持。Service层是展示DS用法的重点Service public class OrderReportService { Resource private BusinessOrderMapper businessOrderMapper; Resource private ReportOrderMapper reportOrderMapper; /** * 默认数据源操作MySQL */ public IPageBusinessOrder getBizOrderPage(long current, long size) { PageBusinessOrder page new Page(current, size); return businessOrderMapper.selectPage(page, null); } /** * 切换数据源到SqlServer */ DS(report) public IPageReportOrder getReportOrderPage(long current, long size) { PageReportOrder page new Page(current, size); return reportOrderMapper.selectPage(page, null); } /** * 注意一个方法内两次查询分别走不同数据源 */ public void compareData() { // 当前线程数据源是master long bizCount businessOrderMapper.selectCount(null); // 显式切换到report数据源 DataSourceContextHolder.push(report); try { long reportCount reportOrderMapper.selectCount(null); System.out.println(biz bizCount , report reportCount); } finally { DataSourceContextHolder.poll(); } } }上面第三种写法是不是让有些读者觉得奇怪正常情况DS加在方法上即可。但要注意DS和Spring事务的相互作用如果你在方法上加了Transactional事务管理器会在方法开始时提前拿到数据源连接DS的AOP在事务边界内的切换效果会打折扣。当遇到“一个事务里需要操作两个库”的需求不要在一个Transactional方法里来回切而是拆分成两个独立的事务方法通过无事务的外层方法协调。或者用上面的DataSourceContextHolder.push/poll手动控制当前线程的数据源上下文它比DS更底层也更直接。动态数据源组件暴露了这个操作上下文的工具类在循环遍历多库、动态选择库名等场景很实用。Controller就很简单了RestController RequestMapping(/order) public class OrderController { Resource private OrderReportService orderReportService; GetMapping(/biz) public IPageBusinessOrder bizPage(RequestParam long current, RequestParam long size) { return orderReportService.getBizOrderPage(current, size); } GetMapping(/report) public IPageReportOrder reportPage(RequestParam long current, RequestParam long size) { return orderReportService.getReportOrderPage(current, size); } }这样一个接口读MySQL、一个接口读SqlServer互不干扰。启动应用后浏览器分别访问/order/biz和/order/report就能看到来自两个不同库的数据。4. 实战排障典型问题与排查思路这部分我写成自己实际跑代码时的“现场手记”。每一个问题都是我真实遇到过的也基本覆盖了标题相关热词里大家搜的那些报错。4.1 数据源切换不生效始终查主库现象Service方法上明明加了DS(report)控制台打印的SQL还是走MySQL主库。用MyBatisPlus的StdOutImpl日志可以看到执行的Statement对应的连接是哪个数据源。原因通常有两个。第一个是DS加错了位置。DS可以被加在类或方法上但它的生效范围是调用该方法的AOP代理对象。如果你的Controller直接注入了Mapper而不是Service或者在同一个类内部使用this.xxx()调用带DS的方法代理不生效切换自然失败。解决办法是把带DS的数据源操作放在独立Service Bean中通过注入的代理对象调用。第二个是数据源名称写错。动态数据源在stricttrue模式下名称错误会直接抛异常如果你设成false它不会报错只会静默走默认数据源看起来就像是“切换不生效”。开发期一定开启strict。排查手段也有两条路一是看启动日志里数据源初始化列表确认report数据源是否成功创建二是临时在Service方法里加一行System.out.println(DataSourceContextHolder.getDataSource())打印当前数据源名称。4.2 MyBatisPlus分页失效SqlServer分页语法报错这是热词里“mybatisplus分页失效”“接触mybatisplus单页500条限制”对应的典型问题。现象MySQL主库分页正常切成SqlServer后selectPage返回的records是全表数据不受current和size控制或者在SqlServer 2008这类老版本上报“Incorrect syntax near OFFSET”语法错误。逐步拆解原因。分页失效的第一种可能是分页插件没生效。MyBatisPlus 3.5.x之后分页插件必须显式配置MybatisPlusInterceptor不能像旧版本一样只引入依赖就自动分页。第二种可能是分页插件判断方言失败。当你配置了PaginationInnerInterceptor(DbType.MYSQL)切到SqlServer时插件内部还是会根据Connection元数据进行方言修正但代理数据源获取Connection时可能在Wall过滤器或自定义拦截器的影响下拿到的是物理连接而非实际的逻辑连接导致元数据判断错乱。第三种可能是写SqlServer分页用的语法本身不对mssql-jdbc驱动对老版本语法兼容有限比如SqlServer 2008分页必须用ROW_NUMBER() OVER而MyBatisPlus可能生成OFFSET FETCH语法。解决办法分页插件的方言设置建议通过“不指定方言”来做让插件每执行一条SQL时自己判断如果判断确实有误再按数据源判断手动构建两个不同方言的分页拦截器通过动态数据源组件按需路由。实在不行可以自定义一个Dialect实现类来处理老版本的ROW_NUMBER分页。“单页500条限制”这个说法其实不是MyBatisPlus的硬性限制而是某些低版本默认Page的size被截断或者配合SqlServer的TOP语法导致最多返回一个固定条数。你不需要“破解”只需要保证分页插件正确配置且SQL没有歧义即可。4.3 SqlServer字符串转数字与类型映射那些事热词里“sqlserver 字符串转数字”我专门提一下。SqlServer的转换函数和MySQL差别很大。MySQL里可以写CAST(123 AS SIGNED)但SqlServer里SIGNED不是合法类型必须写CAST(123 AS INT)或者用CONVERT(INT, 123)。如果你的Mapper XML是通用文件这两个库共用一张表就会出现“数据类型转换失败”的报错。我处理这类问题的经验是把类型转换尽量放在Java服务层做不要在XML里写数据库专有函数。多数据源本身就应该意识到一个问题——SQL方言是数据库私有的通用Mapper要守住通用语法边界。非要转换就针对不同的数据源写不同的XML文件利用MyBatis的databaseId机制动态选择SQL。MyBatis-Plus也支持InterceptorIgnore等插件忽略策略实现数据库级差异化。字段类型映射方面SqlServer的nvarchar、varchar、char在Java侧都映射为String没问题但uniqueidentifier映射为String时要小心部分驱动版本返回的字符串带花括号。SqlServer的bit映射Java的Boolean没问题但如果你用int去接收会报“Cannot convert”。建议多数据源项目给实体类统一加注释说明来源库防止后来人接错字段类型。4.4 事务边界不清导致的数据源路由失效热词里没直接提事务但它其实是多数据源里最隐蔽的坑。现象是你把DS(report)和Transactional写在了同一个Service方法上结果查出来的数据还是主库的。原因是这样Spring的事务处理AOP优先级默认高于数据源路由AOP。方法先是开启事务事务管理器会获取一个数据源连接并绑定到当前事务上下文之后DS才执行切换动作。如果事务已经开启DataSourceContextHolder虽然切换了上下文但事务内部持有的连接还是最开始那个数据源的物理连接所以看起来就是“注解不生效”。正确解法是多数据源环境下避免在同一个事务里跨库操作。把带DS的方法拆到不同Service类每个方法内部有自己的事务边界外层调用方法不加Transactional只做业务编排。如果业务确实要求强一致性就引入分布式事务组件但中小项目完全没必要先保证路由正确再追求一致性。5. 常见问题速查表与避坑清单做了张速查表遇到问题直接对着查能省很多搜索时间。现象可能原因解决方案启动报“Failed to configure a DataSource”动态数据源组件没生效SpringBoot尝试自动配置单数据源确认引入dynamic-datasource-spring-boot-starter并检查YAML缩进启动报“Driver not found: com.microsoft.sqlserver.jdbc.SQLServerDriver”mssql-jdbc依赖缺失或版本太老pom里添加最新版微软官方驱动MySQL连接报SSL错误服务端要求SSL而客户端没配置连接串加useSSLfalse或配置sslModeDISABLEDMySQL连接报Public Key Retrieval is not allowed认证插件是caching_sha2_password且连接串缺参数连接串加allowPublicKeyRetrievaltrueSqlServer连接报“The driver could not establish secure connection”加密参数不匹配连接串加encrypttrue;trustServerCertificatetrueDS不生效始终查主库同对象内部调用或DS加在非代理方法上拆独立Service通过注入代理调用DS与Transactional同用失效事务AOP优先于路由AOP避免一个事务跨多库拆分事务方法SqlServer分页报OFFSET语法错误数据库版本为2008或方言识别错误使用ROW_NUMBER()分页或升级数据库兼容模式SqlServer字符串转数字报“Conversion failed”使用了MySQL风格的SIGNED类型使用CAST(123 AS INT)或Java侧转换MyBatisPlus只返回第一页/条数不对分页插件未配置、方言配置错、Page对象注入顺序问题显式配置MybatisPlusInterceptor检查Page参数SqlServer导入数据提示“数据无效”导入向导字段类型映射不匹配检查源文件列类型或先导入临时表再转换SpringBoot版本与MyBatisPlus不兼容自动配置类不生效使用匹配的版本组合比如SpringBoot 2.7.x配MyBatisPlus 3.5.x避坑清单按优先级排几条最重要的。第一数据源标识命名规范要固定。有人用下划线、有人用中划线Java注解里写错一个字符就是白折腾半天。我习惯全部用小写字母加数字比如master、report。第二把spring.datasource.dynamic.stricttrue务必打开。线上也许为了容错可以关但开发和测试环境必须开着。第三生产环境不要把数据库密码明文写在YAML里。动态数据源组件支持通过环境变量占位符引用例如password: ${DB_REPORT_PASSWORD}多数据源项目配置多套密码时环境变量管理比改配置文件安全得多。第四连接串参数太多容易复制粘贴出错尤其是SqlServer分号分隔、MySQL分隔在线编辑器容易把转义掉建议统一用YAML引号包裹整个URL字符串。最后再分享一点体会折腾了一圈多数据源我个人最大的感受是这个需求本身不复杂复杂的是对自己工程里“连接是什么时候建立的”“事务和AOP谁先执行”“方言是谁决定的”这些底层逻辑是否有清楚认知。SpringBoot把单数据源配置变得太简单了以至于很多开发者忘了数据库连接、事务、方言这三件事在框架底层是紧密耦合的。当你开始做多数据源这些耦合关系就会一个接一个暴露出来反而是件好事逼着你把框架原理吃透。这次MySQL和SqlServer双库联调跑通之后我还顺手把分页器、自定义SQL的方言兼容都整理了一遍。后续如果项目里有需要接入金仓、达梦这类国产数据库的需求只要按照同样的套路新增一个数据源配置、写好对应的驱动依赖DS切换逻辑完全不用动。希望这篇实操记录能帮你少走几步弯路把多数据源跑通后的那种“掌控感”带给你。
返回列表