ARTICLE DETAIL

资讯详情

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

多数据源动态连接与运行时注册:原理、实践与避坑指南

多数据源动态连接与运行时注册:原理、实践与避坑指南 我最早被问到“多数据源动态连接”的时候还是在给一个平台做改造。数据库从1个变成3个后来又加到7个最难受的不是写代码而是每次加一个库就要重启应用。后面我把这套方案整理成了动态路由加运行时注册的方式才算真正解决痛点。这篇文章就把这套思路完整拆开讲从AbstractRoutingDataSource的原理到动态注册数据源再到连接池回收和事务陷阱全部用实际代码和踩坑记录说话。适合正在做多数据源改造、或者想给项目加上动态切换能力的同学参考。1. 动态数据源的整体设计思路1.1 为什么需要“动态”而不是“写死”多个数据源很多项目一开始做多数据源第一反应是配置两个DataSource然后在Service层写死调用哪一个。比如专门建一个primaryDataSource再建一个secondaryDataSource用Qualifier注入。这种方式在小规模场景下能跑但有几个非常尴尬的问题。首先是扩展性问题。每增加一个数据库都要改配置、新增一个DataSource Bean、修改业务代码里的注入逻辑。如果数据库数量是固定的这种写法还能忍受一旦数据源数量频繁变化比如SaaS平台按租户分库、数据中台按业务线分库每次加租户都要发版重启运维和开发的成本都不可接受。其次是代码耦合问题。数据源的选择逻辑散落在业务代码里serviceA用dataSource1serviceB用dataSource2调用的地方彼此不统一。后续接手的人看到一堆Qualifier注解根本分不清当前这个方法到底连的是哪个库排查问题全靠猜。动态数据源的核心价值在于让数据源的选择变成一种运行时路由行为。业务代码不再关心“我用的是哪个DataSource”而是只提供一个路由键比如租户ID、业务线编码数据源路由层根据这个键在运行时决定到底从哪个库里拿连接。新增数据库时只需要在配置中心或者管理系统里注册一个新的数据源应用无需重启就能生效。1.2 动态路由的几种实现路径对比做动态数据源业界常见的方案有三种我结合自己的使用体验对比一下。第一种是基于AbstractRoutingDataSource实现动态路由。这是Spring框架本身提供的扩展点核心原理是维护一个targetDataSources映射表路由时通过determineCurrentLookupKey()返回的key来决定使用哪个数据源。优点是轻量不需要引入额外依赖路由逻辑清晰缺点是它只解决“路由”问题不解决“数据源实例动态创建”的问题也就是说数据源实例还得预先创建好放进Map里。第二种是基于第三方框架比如MyBatis-Plus的多数据源插件、Baidu DBPack这类中间件。框架封装度高开箱即用但定制性受限遇到特殊场景反而难排查。我早期用过一阵子后来因为需要自己控制连接池参数和动态注册逻辑还是回到了自研路由方案。第三种是完全自研动态数据源管理器。自己维护一个MapString, DataSource在运行时创建、注册、销毁数据源实例同时配合Spring的AbstractRoutingDataSource做路由转发。这种方式最灵活既能实现路由也能实现动态注册适合数据源数量变化频繁的项目。我这篇文章的方案就是基于这个思路做的。三种方案没有绝对的好坏关键看项目阶段。如果你的数据源数量固定用AbstractRoutingDataSource就够了如果你需要支持运行时动态增加数据源至少要自己实现一层数据源管理器。下面先讲基础路由怎么实现。1.3 方案选型背后的几个关键判断我在设计这套方案时做了几个关键决策这里逐一解释为什么这么选。第一个决策是不在业务代码里直接持有DataSource引用而是统一走DynamicDataSource路由。原因前面说过了解耦。业务方只需要在入口处设置路由键连接获取的动作全部交给路由层业务代码永远只依赖一个DataSource也就是路由数据源本身。第二个决策是数据源路由键用ThreadLocal传递。路由键要跟着当前请求线程走既能在Service层设置也能在Controller层设置而且必须在请求结束后清理掉。用ThreadLocal存路由键是主流做法比如MyBatis-Plus内部也是类似思路。这个方案的优点是简单直接缺点是如果线程池里的线程没有清理ThreadLocal下一次复用线程时会拿到上次的路由键导致数据串库。这个问题我在后面常见问题排查部分详细讲。第三个决策是数据源实例的创建参数要可配置化。每个数据库的地址、账号、密码、连接池大小都不同不能写死在代码里。我设计了一个DataSourceProperty来承载这些配置支持Map格式传入这样后续可以对接Nacos等配置中心实现数据源的热加载。2. 基础版动态路由实现详解2.1 核心类的代码实现先看最核心的路由类继承Spring的AbstractRoutingDataSource。这个类要求我们实现determineCurrentLookupKey()返回当前请求的数据源key然后Spring会拿着这个key去targetDataSources里找对应的DataSource找不到就用默认数据源。public class DynamicDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { return DynamicDataSourceContextHolder.getDataSourceKey(); } }对应的路由键持有者用ThreadLocal实现public class DynamicDataSourceContextHolder { private static final ThreadLocalString HOLDER new ThreadLocal(); public static void setDataSourceKey(String key) { HOLDER.set(key); } public static String getDataSourceKey() { return HOLDER.get(); } public static void clearDataSourceKey() { HOLDER.remove(); } }这里需要注意一点动态数据源的所有数据源要预先注入到targetDataSources中才可以路由。所以需要一个配置类把默认数据源和路由数据源装配起来。我的做法是这样Configuration public class DataSourceConfig { Bean Primary public DataSource dynamicDataSource() { DynamicDataSource dynamicDataSource new DynamicDataSource(); MapObject, Object targetDataSources new HashMap(); targetDataSources.put(master, primaryDataSource()); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(primaryDataSource()); return dynamicDataSource; } Bean public DataSource primaryDataSource() { return DataSourceBuilder.create() .url(jdbc:mysql://localhost:3306/master_db) .username(root) .password(123456) .build(); } }Primary这个注解很关键。因为容器里有多个DataSource类型的BeanSpring在自动注入时如果没有指定Qualifier会优先选择Primary标记的那个。我们的路由数据源要作为全局默认入口所以必须加上Primary否则MyBatis、JPA这些ORM框架在初始化时会因为找不到唯一的DataSource而报错。2.2 路由切面的编写实践有了路由类和key持有者之后还需要解决一个问题路由键在哪里设置如果让业务代码手动调用DynamicDataSourceContextHolder.setDataSourceKey()容易漏设也容易忘记清理。我比较推荐用AOP切面来管理路由键的生命周期。我当时的做法是自定义一个DataSource注解标注在Service方法或者Mapper方法上然后在切面中读取注解的value值作为路由键。需要注意的是这里有两个关键细节切面的优先级要合理。如果你同时开启了事务和切面必须保证数据源切面优先于事务切面执行。否则事务先拿到了连接再切换数据源就无效了这个坑下面会专门说。切面的After通知里要把ThreadLocal清掉。如果忘记了线程复用时就会串库。自定义注解定义如下Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface DataSource { String value() default master; }对应的切面Aspect Component public class DataSourceAspect { Before(annotation(dataSource)) public void setDataSource(JoinPoint joinPoint, DataSource dataSource) { DynamicDataSourceContextHolder.setDataSourceKey(dataSource.value()); } After(annotation(dataSource)) public void clearDataSource(JoinPoint joinPoint, DataSource dataSource) { DynamicDataSourceContextHolder.clearDataSourceKey(); } }这样业务代码只需要在方法上标注DataSource(order_db)路由键的设置和清理都由切面统一管理既简洁又不容易出错。实测下来这种方式的基本用法非常顺手但要注意切面被代理的机制是否生效。如果你的方法是被同类内部的另一个方法调用的AOP代理不会触发因为调用发生在内部没有经过代理对象。这个属于Spring AOP的典型限制需要熟悉。2.3 与MyBatis、JPA等框架的对接方式数据源切换最终是要让ORM框架能用到正确的连接。如果你的项目是Spring Boot MyBatis最简单的做法是把SqlSessionFactory绑定到路由数据源上这样每次执行SQL时MyBatis会从路由数据源里获取连接也就是先走determineCurrentLookupKey()再拿连接实现了动态切换。如果你用的是JPA或Hibernate原理相同只需要确保EntityManagerFactory的DataSource是路由数据源即可。比较常见的写法是spring: jpa: database-platform: org.hibernate.dialect.MySQL8Dialect然后在配置类中用Primary标记路由数据源JPA自动使用这个数据源不用额外指定。如果你在项目中集成了多套ORM框架比如同时用MyBatis和JPA那就要分别确认它们的SqlSessionFactory和EntityManagerFactory注入的DataSource是否正确否则会出现“代码里切了数据源但MyBatis实际走的还是默认库”的怪现象。我见过一个案例项目的DataSource装配正确但MyBatis的SqlSessionFactoryBean里又单独指定了一个dataSource导致路由数据源没起作用。排查了很久才发现是配置文件里写死了DataSource。2.4 事务边界与数据源切换的冲突处理这块是动态数据源方案里最容易出问题的地方我单独拿出来重点讲。Spring的事务管理和数据源路由之间存在一个天然的时差问题。当你在方法上添加Transactional注解后Spring事务管理器会在方法开始执行时立即从当前数据源获取一个数据库连接并且把连接绑定到当前线程整个事务期间都使用这个连接。如果事务已经开启连接已经拿到了此时再去调用DynamicDataSourceContextHolder.setDataSourceKey()切换数据源实际上切换的是下一个连接获取请求的路由键当前事务根本不认识这个切换。更麻烦的是事务管理器启动时还会绑定一个DataSourceConnectionHolder到线程上即使你切换了路由键事务管理器从线程绑定信息里拿到的还是旧连接。很多新手在这个地方翻车明明切了数据源查出来的数据还是上一个库的偏偏又找不到错在哪里。有几种规避方案。第一是避免在事务方法内部切换数据源。如果必须切换把不同数据源操作拆分成独立的事务方法通过Spring的代理机制跨越事务边界重新获取连接。第二是自定义事务管理器。你可以继承AbstractRoutingDataSource同时让DataSourceTransactionManager从当前路由键对应的数据源获取连接但这个实现复杂度比较高对于大部分项目来说成本偏高。我的建议是优先第一种方案也就是通过事务传播行为控制好事务边界避免过大事务包住多个数据源操作。3. 运行时动态注册与关闭数据源的完整方案3.1 动态数据源管理器的设计基础路由方案能解决的问题是“固定数据源之间切换”但解决不了“运行时新增一个数据库”。要支持运行时动态注册需要自己维护一个数据源注册中心把新增的数据源实例挂载到路由数据源的targetDataSources中并且要处理连接池的创建销毁、参数校验、状态刷新等一系列问题。我的设计思路是这样的设计一个DynamicDataSourceManager组件内部维护MapString, DataSourceProperty保存数据源配置信息同时通过DynamicDataSource暴露的方法将新的数据源注入到路由映射表中。这里有一个核心细节每次新增数据源之后必须调用AbstractRoutingDataSource.afterPropertiesSet()方法来刷新内部的数据源映射表否则路由不会感知到新增的数据源。afterPropertiesSet()这个方法很多人容易漏掉。AbstractRoutingDataSource在初始化时会把targetDataSources转换成一个只读的MapObject, DataSource缓存起来后续路由时都是从这个缓存里查找数据源的。你改了targetDataSources之后如果不去刷新缓存路由时根本找不到新注册的数据源。相当于你往抽屉里塞了新东西但标签没更新拿东西时还是按旧标签找。这个问题在动态数据源场景下尤其致命因为不是在启动时一次性注入而是运行时多次写入。3.2 动态注册数据源的代码实现看一段实际可用的配置类。我设计了一个DataSourceProperty用来描述数据源的基本属性包含连接地址、用户名、密码、驱动类名等信息。public class DataSourceProperty { private String key; private String url; private String username; private String password; private String driverClassName com.mysql.cj.jdbc.Driver; // getters and setters }管理器的实现Component public class DynamicDataSourceManager { Autowired private DynamicDataSource dynamicDataSource; private final MapString, DataSource dataSourceMap new ConcurrentHashMap(); /** * 注册新的数据源 */ public synchronized boolean addDataSource(DataSourceProperty property) { // 参数校验 if (property.getKey() null || property.getUrl() null) { throw new IllegalArgumentException(数据源key和url不能为空); } if (dataSourceMap.containsKey(property.getKey())) { throw new IllegalArgumentException(数据源已存在 property.getKey()); } // 构建数据源此处可以选用连接池实现这里以HikariCP为例 HikariConfig config new HikariConfig(); config.setJdbcUrl(property.getUrl()); config.setUsername(property.getUsername()); config.setPassword(property.getPassword()); config.setDriverClassName(property.getDriverClassName()); config.setMaximumPoolSize(10); config.setMinimumIdle(5); config.setPoolName(HikariPool- property.getKey()); config.setConnectionTimeout(30000); config.setIdleTimeout(600000); config.setMaxLifetime(1800000); DataSource dataSource new HikariDataSource(config); // 将新数据源添加到路由缓存 MapObject, Object targetDataSources new HashMap(dynamicDataSource.getTargetDataSources()); targetDataSources.put(property.getKey(), dataSource); dynamicDataSource.setTargetDataSources(targetDataSources); // 关键刷新路由缓存 dynamicDataSource.afterPropertiesSet(); dataSourceMap.put(property.getKey(), dataSource); return true; } }这个addDataSource方法展示了完整的数据源注册流程构建连接池、加入路由映射、刷新缓存、持久化配置记录。每一步都有它的必要性缺一个都会出问题。比如不调用afterPropertiesSet()新数据源不会出现在路由缓存里不保存到dataSourceMap后面如果要销毁数据源就找不到对应的连接池实例了。3.3 移除数据源与连接池销毁有新增就有删除。移除数据源的难点在于不仅要让路由不再指向它还要安全地关闭连接池否则会造成连接池泄漏数据库连接一直占用时间长了连接数飙满数据库直接拒接新连接。安全的移除流程分三步先从路由缓存中移除再刷新路由缓存让移除生效最后关闭连接池。这里有个很重要的操作顺序必须先移除再关闭反过来会出大问题。假如你先关了连接池此时有一个请求还在使用旧数据源连接池关闭会打断正在执行的SQL轻则报错重则数据不一致。所以正确顺序一定是“先摘路由再销毁实例”。public synchronized boolean removeDataSource(String key) { if (master.equals(key)) { throw new IllegalArgumentException(默认数据源不允许移除); } DataSource removedDataSource dataSourceMap.remove(key); if (removedDataSource null) { return false; } // 先从路由缓存中移除 MapObject, Object targetDataSources new HashMap(dynamicDataSource.getTargetDataSources()); targetDataSources.remove(key); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.afterPropertiesSet(); // 最后关闭连接池 if (removedDataSource instanceof AutoCloseable) { try { ((AutoCloseable) removedDataSource).close(); } catch (Exception e) { log.error(关闭数据源失败: {}, key, e); } } return true; }这里还需要补充一个细节如果dataSourceMap里存的是HikariDataSourceclose()方法会等待正在使用的连接归还后再关闭整个连接池。这个等待过程有超时时间默认情况下基本不会卡住但如果你有长事务在跑可能关闭时间会延长。因此生产环境下我建议在移除数据源前增加一个“下线标记”机制把数据源标记为只读或拒绝新请求等存量请求处理完后再真正关闭连接池类似服务发布时的优雅下线。3.4 动态数据源与持久化配置的结合思路前面讲的是内存版的数据源管理器。如果只是进程内的动态操作重启之后配置全部丢失数据源需要重新注册。在实际的生产系统中我们肯定需要把数据源配置集中管理起来比如存到数据库、Nacos配置中心或Apollo中启动时自动加载运行时监听配置变更自动新增或移除数据源。这块的实现思路不复杂写一个DataSourceConfigLoader启动时从配置中心拉取所有数据源配置并批量注册再通过配置中心的监听机制配置变更时回调addDataSource或removeDataSource。我在项目中见过有人直接把数据源配置表格存在一个公共库里然后通过定时任务扫描表变化有新增就自动注册有修改就替换连接池效果也不错。这样数据源的增删就变成了“往配置表里插一条记录”不需要碰代码也不需要重启应用。要注意的是动态注册数据源虽然方便运维但同时带来了管理复杂度。数据源多了之后缺乏可视化视图连接池状态、连接数、活跃连接数都很难监控。建议至少加上对连接池核心指标活跃连接数、空闲连接数、等待获取连接数的监控配合告警第一时间发现连接池异常。4. 连接池参数选择与资源管理经验4.1 HikariCP连接池的关键参数说明动态数据源方案里每个数据源连接池的参数不一定是千篇一律的。不同的数据库、不同的业务峰值连接池的参数都应该不同。比如核心库的峰值QPS高连接池就要大一些报表库访问频率低连接池保持最小几个连接就够了。我通常用HikariCP作为连接池实现它的性能和稳定性在Spring Boot 2.x之后是默认选择。默认参数并不适合所有场景我会按下面这套基准来配置参数推荐值说明maximum-pool-size10 ~ 50最大连接数与业务并发度相关不宜过大minimum-idle5 ~ 10最小空闲连接数过低会导致频繁创建连接connection-timeout30000获取连接超时时间超过会抛异常idle-timeout600000空闲连接存活时间避免长时间占着不放max-lifetime1800000连接最大存活时间建议小于数据库wait_timeoutvalidation-timeout5000连接校验超时配合test-while-idle使用关于max-lifetime这个参数有个经验之谈一定要比数据库的wait_timeout短。MySQL默认的wait_timeout是8小时连接池里的连接如果超过8小时没有活动会被数据库主动断开但连接池并不知道等到用这个连接时才发现Socket连接已经断了。HikariCP的max-lifetime设置为1800000毫秒30分钟预留了安全裕度即使数据库回收空闲连接连接池也会提前把连接替换掉。4.2 连接池预热与慢启动问题动态注册数据源之后有一个容易被忽视的问题连接池冷启动。HikariCP默认在初始化时并不会立即创建minimumIdle个连接而是根据实际使用情况懒加载。这意味着一个数据源刚注册后第一次访问时要现创建连接单次查询的耗时会明显偏高如果此时正好有并发请求涌进来可能会因为创建连接太慢而导致获取连接超时。解决办法有两种。第一种是注册数据源时主动触发一次连接预热心机一点的做法是注册完成后调一下dataSource.getConnection().close()强制连接池提前建立连接。第二种是调整HikariCP的initializationFailTimeout参数让连接池初始化时就建立连接。但这块需要小心如果配置的数据库地址本身有问题启动时就会直接报错而不是等到首次调用时才报错。具体怎么取舍看你想要“快速失败”还是“延迟感知”。我在动态注册场景下的做法是注册完数据源后主动执行一个轻量级的SQL语句比如SELECT 1既能验证数据源配置是否正确也能触发连接池初始化。这个操作的额外好处是如果配置错误能在注册时立刻暴露问题方便事务回滚而不是等到某个业务调用时才突然报错。4.3 数据源状态监控与告警多数据源治理到后期一个绕不开的问题是监控。你可以用Spring Boot Actuator暴露健康检查但默认情况下它只检查主数据源动态注册的那些数据源不会自动纳入健康检查。我改造过一个DataSourceHealthIndicator注册了所有数据源的健康检查这样/actuator/health返回时能展示每个数据源的状态做旁路监控时能全局看到所有数据源的可用性。更重要的是连接池指标监控。HikariCP提供了JMX MBean可以通过Micrometer集成到Prometheus Grafana实时展示每个连接池的totalConnections、activeConnections、idleConnections、pendingThreads。我把这些指标加上标签pool_name区分不同数据源在Grafana里做了一张看板哪个库的连接数飙高、哪个库的等待线程数多了一眼就能看出来。没有监控的多数据源等于在裸奔。这事我是有深刻教训的上线第一天某个数据源的连接池配置太小高峰期排队获取连接系统整体响应时间从50ms飙到2000ms查了半天才定位到是连接池打满了。从那以后所有动态数据源一律接入监控参数调优也必须有据可依。5. 实操中常见的坑与排查技巧5.1 切换数据源不生效典型场景业务代码里设置了路由键执行查询时返回的却是默认数据源的数据。我先说说排查路径。第一步先确认AOP切面是否真的执行了。最直接的验证方式是在切面方法里打日志打印当前线程ID和数据源key。如果切面没执行大概率是方法不是通过Spring代理对象调用的。第二步检查determineCurrentLookupKey()返回的key是否和targetDataSources里的key一致。注意一个隐蔽的问题数据库配置的key注意大小写和空格。如果用OrderDB注册路由时却用orderdb自然匹配不上导致走了默认数据源。第三步也是最容易踩的坑事务切面的优先级。数据源路由的AOP代理必须先于事务切面执行。如果事务先执行连接已经被绑定到当前线程了再切换数据源是无效的。解决方案是给路由切面设置Order(Ordered.HIGHEST_PRECEDENCE)确保它最优先执行。第四步查MyBatis的SqlSessionFactory是否绑定了路由数据源。有些旧项目的配置里写死了数据源即使路由切面生效了最终执行的连接也是固定数据源的。5.2 线程池复用导致数据串库这个坑出现的频率非常高尤其是在用了动态线程池或者执行器框架之后。ThreadLocal存储的路由键会跟随线程如果线程在执行完A任务后没有清理ThreadLocal线程被线程池回收并复用去执行B任务B任务就会背上A任务的路由键导致数据串库。我自己踩过一次在异步任务里切换了租户库跑完之后没清空ThreadLocal结果线程池里下一个任务查了错误的数据。这种问题非常难排查因为它是偶发的、跟线程调度相关单跑一次往往复现不了。根治方案是所有设置路由键的地方必须用try/finally保证执行完后清理。用切面管理时就简单After通知里清理即可如果你在业务代码里手动调用setDataSourceKey务必确保finally块里调用clearDataSourceKey()。另外给线程池中的Runnable包一层包装类执行前清空路由键执行后也清空双保险。5.3 afterPropertiesSet没有调用导致新增数据源不生效新增数据源后路由时报找不到数据源或者走了默认数据源但注册时又没报错。这种情况十有八九是afterPropertiesSet()没被调用。AbstractRoutingDataSource在Spring的生命周期中会自动调用afterPropertiesSet()所以在静态配置数据源的场景下你感知不到这个方法的存在。但在动态注册场景里你手动调用了setTargetDataSources后必须手动再调一次afterPropertiesSet()让路由缓存刷新。这个机制类似双缓冲写的是源数据路由用的是缓冲不刷新缓冲就跟没写一样。不过这里还有个版本兼容问题。不同版本的Spring对afterPropertiesSet()的实现细节略有差异有些版本里刷新缓存后旧连接池引用也会被替换掉有些版本则不会。建议升级Spring版本前先用测试用例覆盖一下动态注册和切换的完整链路。5.4 动态数据源多数据源事务回滚失效动态切换数据源时跨数据源的事务回滚是个硬骨头。Spring的本地事务管理器只能管理单一数据源的事务。如果A方法开启了事务查询了库1又切换到库2执行写入最终异常回滚时库2的写入是不会被回滚的因为库2的事务没有被纳入当前事务管理器。有几个处理思路。如果只是要求单数据源内的事务一致性尽量把同一数据源的操作封装到一起事务边界只包住单个数据源的操作。如果确实需要跨多数据源强一致就需要引入分布式事务方案比如Atomikos、Seata等。轻量级的项目也可以考虑用本地消息表加最终一致性的方案避免引入过重的分布式事务框架。我对这个问题的结论是动态路由与跨库强一致性事务天然冲突设计阶段就应该明确边界不要试图在动态数据源方案里塞一个庞大的强一致事务。5.5 常见问题速查表问题现象可能原因排查/解决方案切换数据源后查询结果不变事务先于路由执行连接已固定给路由切面设置最高优先级拆分事务边界新增数据源后路由找不到未调用afterPropertiesSet每次setTargetDataSources后刷新缓存数据源注册时报连接失败防火墙、账号权限、驱动依赖缺失检查网络连通性确认数据库驱动存在线程复用导致串库ThreadLocal未清理finally清理线程池包装类清空移除数据源后连接数不降连接池未关闭或存在长事务先摘路由再关闭连接池等待存量请求结束连接池连接被数据库断开max-lifetime小于数据库wait_timeout调低max-lifetime预留安全裕度数据源配置错误但启动不报错连接池懒加载注册后执行SELECT 1验证连接是否可用6. 最后的几个经验心得写完这套动态数据源方案让我复盘一下工程层面的体会。数据源本身不是难点真正的难点在生命周期管理、事务边界、连接池资源回收这三件事上。我见过很多项目路由部分写得漂漂亮亮一到资源回收就拉胯连接池泄漏到数据库直接报警这种方案称不上合格。如果你们的项目还在起步阶段数据源数量不多我建议直接用最简版的路由方案不要一上来就上动态注册。过早引入数据源管理复杂性对项目是负担不是收益。但如果你们已经感知到数据源数量会不断增长或者有按租户/业务线分库的规划那动态注册肯定是要提前设计好的。这里边最值得花时间的不是路由代码而是生命周期管理和监控体系把新增、下线、替换、监控这四个动作做扎实这套方案才能扛住生产环境。最后分享一个小技巧给所有数据源统一打上应用层命名规范比如master、order_db、report_2024每个数据源除了key再维护一个desc描述字段在日志和监控里都把这个描述带出来。等你有二三十个数据源的时候日志里出现一个裸的key和一个带业务语义的描述定位问题的效率天差地别。这套方案用到现在帮我省下的排查时间远比我写它的时间多。
返回列表