松原公司做网站的流程,张店网站制作哪家好,在线html5制作网站,珠海高端网站建设公司1. 为什么你的项目需要多数据源#xff1f; 我猜很多朋友第一次接触“多数据源”这个概念#xff0c;可能是在项目需求评审会上#xff0c;产品经理轻描淡写地提了一句#xff1a;“我们需要把一部分用户数据存到另一个独立的数据库里#xff0c;做读写分离。” 或者…1. 为什么你的项目需要多数据源我猜很多朋友第一次接触“多数据源”这个概念可能是在项目需求评审会上产品经理轻描淡写地提了一句“我们需要把一部分用户数据存到另一个独立的数据库里做读写分离。” 或者技术负责人说“这个新业务模块为了安全隔离得用一套全新的数据库。” 这时候你心里可能“咯噔”一下脑子里瞬间闪过一堆问题一个项目连两个库配置怎么写代码里怎么切换事务会不会乱套别慌这几乎是每个中大型Java后端项目都会遇到的“甜蜜的烦恼”。我做了这么多年项目从单体应用到微服务多数据源的需求几乎无处不在。比如业务隔离核心的用户订单数据放在主库A后台运营的日志、报表数据放在从库B互不影响安全又清晰。读写分离主库负责写操作增删改多个从库负责读操作查询大幅提升系统并发处理能力这是应对高流量的经典方案。多租户架构一个SaaS系统每个租户的数据物理隔离存放在不同的数据库实例中。老旧系统整合新系统需要同时访问老系统的数据库和新建设的数据库进行数据聚合或迁移。Ruoyi作为一款优秀的开源权限管理系统其设计早就考虑到了这种复杂性。它内置了一套基于Spring AOP和ThreadLocal的动态数据源切换机制核心思想就是在运行时根据你的指令注解或代码把当前线程的数据库连接指向你想要的那个数据源。这就像给一个接线员配了多个电话号码平时他用默认的座机主数据源但一接到特定客户的电话他就能瞬间切换到对应的专线其他数据源进行沟通挂断后自动切回座机。所以搞懂Ruoyi的多数据源绝不仅仅是学会加个配置那么简单。它关乎你如何优雅地设计数据层如何保证在高并发下切换不“串线”如何让跨库操作依然保持事务的“原子性”。接下来我就带你从最基础的配置开始一步步拆解直到搞定那些棘手的业务场景。2. 从零开始配置你的第二个数据源纸上得来终觉浅绝知此事要躬行。咱们先抛开理论直接动手在Ruoyi里配出第二个数据源。我以最常用的Druid连接池为例这也是Ruoyi默认采用的。2.1 第一步在DruidConfig中声明新的DataSource BeanRuoyi的数据库配置核心在ruoyi-framework模块下的com.ruoyi.framework.config包里找到DruidConfig这个类。你会看到里面已经有一个主数据源比如叫masterDataSource的配置方法。我们现在要做的就是“依葫芦画瓢”为第二个数据源假设我们叫它other添加一个Bean。这里有个关键点使用ConditionalOnProperty注解。这个注解太有用了它允许我们通过配置文件application.yml里的一个开关来决定是否创建这个Bean。这样在开发、测试、生产不同环境我们可以灵活地启用或禁用某个数据源。// 在 DruidConfig.java 类中添加以下方法 Bean ConfigurationProperties(prefix spring.datasource.druid.other) // 绑定yml中other的配置 ConditionalOnProperty(prefix spring.datasource.druid.other, name enabled, havingValue true) public DataSource otherDataSource(DruidProperties druidProperties) { // 1. 使用DruidDataSourceBuilder创建一个基础数据源实例 DruidDataSource dataSource DruidDataSourceBuilder.create().build(); // 2. 将全局的DruidProperties包含一些通用配置如初始化大小、最大连接数等应用到新实例 // 3. 更重要的是将spring.datasource.druid.other下的特定配置如url、username注入进来 return druidProperties.dataSource(dataSource); }我来解释一下这几行代码Bean告诉Spring这个方法会返回一个对象并且这个对象要交给Spring容器管理。ConfigurationProperties这是Spring Boot的神器它自动将application.yml里spring.datasource.druid.other下面的所有属性比如url,username,password映射到这个DataSource对象的对应字段上。你不需要手动一个个去set。ConditionalOnProperty条件装配。只有当配置文件中存在spring.datasource.druid.other.enabledtrue这个属性时Spring才会执行这个方法创建这个Bean。如果没配或者配了false这个数据源就直接不存在避免了配置缺失导致的启动报错。druidProperties.dataSource(dataSource)这里调用了Ruoyi封装好的一个工具方法它除了注入other特有的配置外还会把一些在spring.datasource.druid根节点下定义的公共Druid配置像initialSize、maxActive、validationQuery等也合并进来避免重复定义非常方便。2.2 第二步在YAML中编写精细化配置光有Java代码不行数据源的连接信息必须写在配置文件里。打开application.yml或者application-{profile}.yml在原有的主数据源配置下面添加我们的other数据源配置。这里我强烈建议你不要简单复制主数据源的连接池参数。不同的业务场景对连接池的需求可能完全不同。比如一个主要用于复杂报表查询的从库可能需要更大的最大连接数maxActive和更长的等待超时时间maxWait而一个用于缓存少量热点数据的库可能连接数可以很小。spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: # 主数据源 (通常也是默认数据源) master: enabled: true url: jdbc:mysql://localhost:3306/ry_main?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8 username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 # ... 其他Druid监控、过滤器等公共配置可以参考原有主库 # 第二个数据源 - 我们命名为 other other: enabled: true # 关键开关设为true才会创建Bean url: jdbc:mysql://192.168.1.100:3306/ry_other?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8 username: other_user password: other_pass driver-class-name: com.mysql.cj.jdbc.Driver # 连接池参数可以独立配置适应业务特点 initialSize: 3 minIdle: 3 maxActive: 15 maxWait: 30000 validationQuery: SELECT 1 # 注意如果不需要独立的监控统计可以复用主数据源的filters等全局配置踩坑提醒这里最容易出错的就是url的格式特别是MySQL 8的时区问题serverTimezone和SSL关闭问题useSSLfalse一定要和你的数据库版本匹配。我曾经就因为漏了时区配置导致测试环境一切正常一上生产就狂报连接错误。2.3 第三步注册数据源到动态路由配置了Bean写好了YAML但Spring现在只知道有这两个DataSource对象还不知道该用哪个、怎么切换。所以我们需要一个“路由器”这就是Ruoyi的DynamicDataSource类。你需要在DruidConfig中找到那个被Primary注解标记的dataSource方法它就是创建动态数据源路由的方法。我们需要把新创建的otherDataSourceBean 也添加到这个路由器的目标数据源映射targetDataSources里。Bean(name dynamicDataSource) Primary // 标记为默认数据源 public DynamicDataSource dataSource(DataSource masterDataSource, Qualifier(otherDataSource) DataSource otherDataSource) { // 创建目标数据源映射 MapObject, Object targetDataSources new HashMap(); targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource); // 添加新的数据源键名是 DataSourceType.OTHER targetDataSources.put(DataSourceType.OTHER.name(), otherDataSource); // 创建动态数据源路由器并设置默认数据源通常是MASTER DynamicDataSource dynamicDataSource new DynamicDataSource(); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(masterDataSource); return dynamicDataSource; }注意这里的Qualifier(otherDataSource)它用于明确指定注入我们上面定义的otherDataSourceBean防止有多个同类型Bean时Spring不知道注入哪个。到这一步你的项目就已经具备了连接两个数据库的能力。接下来就是学习如何指挥这个“路由器”进行切换了。3. 两种切换模式注解派与编程派数据源配置好了怎么用呢Ruoyi提供了两种主流方式我把它们叫做“注解派”和“编程派”。它们各有优劣适用于不同的场景甚至可以在一个项目里混合使用。3.1 注解派DataSource 的优雅与局限这是最常见、最声明式的方式。Ruoyi提供了一个DataSource注解你可以直接把它用在Service层的方法或者类上。Service public class UserReportService { // 这个方法默认使用主数据源由DynamicDataSource的defaultTargetDataSource决定 public ListUser getMasterUsers() { return userMapper.selectUserList(new User()); } // 这个方法明确指定使用 OTHER 数据源 DataSource(value DataSourceType.OTHER) public ListOperationLog getLogsFromOther() { return logMapper.selectOperationLogList(new OperationLog()); } }它的工作原理Spring AOP面向切面编程在幕后帮了大忙。Ruoyi有一个切面DataSourceAspect它会拦截所有被DataSource注解标记的方法。在执行方法前切面根据注解值如OTHER通过DynamicDataSourceContextHolder.setDataSourceType(...)将数据源类型设置到当前线程的ThreadLocal变量中。然后DynamicDataSource路由器在获取连接时会先检查这个ThreadLocal变量如果有值就路由到对应的具体数据源没有就用默认的。方法执行完毕后切面再清理掉ThreadLocal变量避免污染后续操作。优点极其简洁一行注解意图清晰代码侵入性低。易于管理一眼就能看出哪个方法用了哪个数据源。局限和坑失效场景AOP代理有局限性。如果同一个类内部的方法A无注解调用了方法B有DataSource注解这个注解是会失效的因为内部调用不走代理对象。这是Spring AOP的经典坑。粒度固定注解的粒度是方法或类。如果你需要在一个方法内部根据一段复杂的业务逻辑动态决定切换多次数据源注解就无能为力了。事务上下文在带有Transactional注解的方法上再使用DataSource需要特别注意顺序。通常数据源切换需要在事务开启之前完成否则事务可能绑定到错误的数据源上。Ruoyi的切面一般会配置有更高的优先级Order值更小来保证这一点。3.2 编程派精准掌控的DataSourceSwitcher当注解派不够灵活时编程派就该上场了。它的核心思想是手动管理数据源切换的上下文。Ruoyi提供了DynamicDataSourceContextHolder这个工具类但我们最好对它做一层封装确保资源ThreadLocal的清理避免内存泄漏。下面是我在项目中常用的一个DataSourceSwitcher工具类import com.ruoyi.common.utils.spring.SpringUtils; import com.ruoyi.framework.datasource.DynamicDataSourceContextHolder; import org.springframework.transaction.support.TransactionTemplate; /** * 数据源切换执行器 */ public class DataSourceSwitcher { /** * 在指定数据源上下文中执行一个任务无返回值 * param dataSourceType 数据源类型如 DataSourceType.OTHER.name() * param task 要执行的逻辑一个Runnable */ public void executeWithDataSource(String dataSourceType, Runnable task) { String previousDs DynamicDataSourceContextHolder.getDataSourceType(); // 保存之前的 try { DynamicDataSourceContextHolder.setDataSourceType(dataSourceType); task.run(); } finally { // 恢复之前的数据源而不是简单地clear if (previousDs ! null) { DynamicDataSourceContextHolder.setDataSourceType(previousDs); } else { DynamicDataSourceContextHolder.clearDataSourceType(); } } } /** * 在指定数据源上下文中执行一个任务并返回结果 * param dataSourceType 数据源类型 * param supplier 包含业务逻辑的Supplier * param T 返回类型 * return 业务逻辑的执行结果 */ public T T executeWithDataSource(String dataSourceType, SupplierT supplier) { String previousDs DynamicDataSourceContextHolder.getDataSourceType(); try { DynamicDataSourceContextHolder.setDataSourceType(dataSourceType); return supplier.get(); } finally { if (previousDs ! null) { DynamicDataSourceContextHolder.setDataSourceType(previousDs); } else { DynamicDataSourceContextHolder.clearDataSourceType(); } } } /** * 在指定数据源上下文中执行一个需要事务的任务 * 注意这个事务是独立于外部事务的 * param dataSourceType 数据源类型 * param transactionTemplate 事务模板需从Spring容器注入 * param action 要在事务中执行的逻辑 */ public void executeInTransaction(String dataSourceType, TransactionTemplate transactionTemplate, TransactionCallbackWithoutResult action) { executeWithDataSource(dataSourceType, () - { transactionTemplate.execute(action); }); } }业务中使用示例Service public class ComplexBusinessService { Autowired private DataSourceSwitcher dataSourceSwitcher; Autowired private OrderMapper orderMapper; // 默认操作主库 Autowired private LogMapper logMapper; // 假设这个Mapper映射的操作在OTHER库 public void hybridOperation(Long orderId) { // 1. 在主库查询订单 Order order orderMapper.selectOrderById(orderId); // 2. 动态切换到OTHER库插入一条操作日志 dataSourceSwitcher.executeWithDataSource(DataSourceType.OTHER.name(), () - { OperationLog log new OperationLog(); log.setOrderId(orderId); log.setAction(QUERY); logMapper.insertLog(log); // 这个方法对应的SQL在OTHER库执行 }); // 3. 执行完后上下文已自动切回主库继续主库操作 order.setStatus(PROCESSED); orderMapper.updateOrder(order); } }编程派的优势极致灵活可以在一个方法内的任意位置、根据任意条件进行切换。避免AOP坑彻底绕开了同类内部调用导致注解失效的问题。易于组合可以方便地与TransactionTemplate等编程式事务管理工具结合实现更精细的事务控制。需要注意的一定要在finally块中清理或恢复上下文这是保证线程安全、防止数据源“泄漏”到其他无关操作的关键。我封装的工具类采用了“恢复”而非“清除”的策略这在嵌套切换的场景下更安全。4. 应对复杂场景跨库事务与高并发安全多数据源最让人头疼的莫过于事务。Spring的Transactional默认只能管理一个数据源上的事务。当你一个业务涉及多个数据库的写操作时如何保证“要么全成功要么全失败”呢4.1 理解事务传播行为与局限首先看一个常见的错误尝试Service public class ProblematicService { Autowired private MasterService masterService; // 操作主库 Autowired private OtherService otherService; // 操作OTHER库其方法上可能有 DataSource(OTHER) Transactional public void transferData() { // 在主库插入数据 masterService.insertA(); // 在OTHER库插入数据 otherService.insertB(); // 这里切换了数据源 // 如果这里抛异常我们期望A和B都回滚... } }很遗憾这样不行。insertB()虽然切换到了OTHER库但Transactional管理的事务是绑定在第一个数据源主库连接上的。insertB()的操作在另一个物理连接上执行它根本不受外层Transactional的控制。如果insertB()成功后外层方法抛异常主库的A会回滚但OTHER库的B已经提交了造成数据不一致。4.2 方案一使用分布式事务重器慎用对于强一致性要求的金融类业务最终方案是引入分布式事务管理器如Seata。它通过TC事务协调器、TM事务管理器、RM资源管理器的模式协调多个数据库资源实现真正的全局事务2PC/XA模式或最终一致性AT/TCC模式。在Ruoyi中集成Seata是另一个专题大致步骤是引入Seata依赖、配置file.conf和registry.conf、在数据源上配置Seata的DataSourceProxy、在全局事务入口方法上使用GlobalTransactional。但请注意分布式事务性能开销大架构复杂非必要不使用。很多业务场景可以通过设计来避免跨库写事务。4.3 方案二设计规避与最终一致性这是更常用的思路。对于上面的例子我们可以这样 redesign异步化将insertB()操作发到消息队列如RocketMQ/Kafka。transferData方法只保证主库操作和消息发送的本地事务成功。由消费者异步去OTHER库执行插入。这实现了最终一致性。事务表在主库中创建一个“事务日志表”。transferData方法在一个本地事务内1) 主库insertA2) 向事务日志表insert一条待同步到OTHER库的记录。然后有一个独立的定时任务或监听器扫描这个日志表取出记录去OTHER库执行insertB成功后标记日志为已完成。业务合并重新审视业务是否真的必须立即写两个库能否将OTHER库的数据作为主库的冗余或衍生数据通过ETL或CDC变更数据捕获工具异步同步过去4.4 高并发下的线程安全与性能即使不考虑跨库事务在高并发下使用动态数据源也要小心ThreadLocal是基石Ruoyi的DynamicDataSourceContextHolder核心就是ThreadLocal。这意味着数据源上下文是线程隔离的这是安全的基石。但务必确保每次切换后都清理否则这个线程处理下一个请求时可能会“意外”使用上一个请求设置的数据源。连接池配置为每个数据源合理配置Druid连接池参数。高并发读的从库可以适当调大maxActive写操作少的主库可以设小一点。监控Druid的统计界面观察活跃连接数、等待计数及时调整。避免频繁切换一次HTTP请求或一个业务方法内尽量减少数据源切换次数。频繁切换本身有性能开销虽然很小更重要的是让代码逻辑更清晰。如果一段逻辑需要交替查询两个库很多次可以考虑将其拆分为两个独立的方法或者思考是否有数据冗余、缓存优化的空间。在我经历的一个电商大促项目中我们通过“注解用于常规CRUD编程式用于复杂聚合逻辑配合消息队列解耦跨库写操作”的组合策略平稳度过了流量洪峰。多数据源就像一把锋利的瑞士军刀用好了能解决复杂问题但也要时刻注意别割伤自己。希望这些从配置到集成的实战经验能帮你把这把刀用得更加得心应手。