多数据源切换这类需求,在真实项目里出现的频率比很多人想象中高。一开始可能只是分离读库和写库,做着做着就变成需要接多个业务库,甚至有那种一个系统同时对接主库、历史库、统计库的场景。Spring Boot + MyBatis这套组合本身就是国内中小型团队最常用的技术栈,所以“在Spring Boot项目里用MyBatis实现多数据源切换”这个话题,几乎每隔一阵子就会被翻出来问一遍。网上一搜能搜到一堆贴代码的文章,但真正能讲清楚“为什么这么切”“切换时哪些坑必须避开”“事务和路由的关系是什么”的并不多。这篇博文就按我自己落地过的方案来写,从设计思路到完整代码,再到实际部署时的常见问题,全部摊开讲。适合刚接触多数据源的Java开发,也适合那些配置完能跑、但一上线就偶发“串库”问题的同学对照排查。
1. 多数据源切换的核心思路与方案选型
1.1 先想明白:你真的需要多数据源吗
在动手写代码之前,有一个特别容易忽略的问题:多数据源这句话本身意味着什么。大多数项目的长期演进路径通常是从单库开始的,后来为了读写分离、垂直拆库或者接入第三方系统库,才需要让一套应用同时连接多个逻辑上的数据库。
如果只是读写分离,更推荐先考虑中间件方案,比如MyCat、ShardingSphere,它们能在不侵入业务代码的前提下做读写分离。但要注意,这些组件在某些复杂环境下(公司网络策略、云环境限制、团队维护成本)不一定允许你用。反过来,如果你需要对接的多个库之间表结构差异较大、各自业务独立,甚至可能分布在不同维度,用应用层多数据源路由反而是更灵活可控的办法。
我个人定下的判断标准只有一个:是否需要“在同一个业务方法中,根据参数或上下文决定操作哪个库”。如果答案是“是”,那应用层动态数据源方案基本绕不开。如果只是简单地按模块分开,比如“订单模块连订单库”“用户模块连用户库”,这本质上是多套SqlSessionFactory共存,跟“切换”是两回事。后者性能上更直接,但代码冗余也更大,同一个实体在不同库的复用会比较麻烦。
1.2 常见实现方式对比:动态数据源路由与多SqlSessionFactory方案
行业内主流的应用层方案大致有三类。
第一类是Spring自带的AbstractRoutingDataSource结合@DataSource注解,这也是网上能搜到最多的一种。核心原理是Spring在getConnection()时调用determineCurrentLookupKey(),我们把这个方法改成从ThreadLocal里拿当前线程指定的数据源标识,即可按需路由。这种方案对MyBatis非常友好,因为MyBatis的SqlSession底层获取连接就是走DataSource接口,所以路由层能做得很透明。
第二类是直接创建多个SqlSessionFactory和多个SqlSessionTemplate,比如把两个库的相关Mapper分别用不同包路径或不同注解区分,然后由@MapperScan分别扫描到各自的SqlSessionFactory。这么做的好处是绝对的隔离,连事务管理器都必须分开配置。但坏处也明显:凡是涉及跨库的数据操作,你只能编程式地注入两个Template,代码里遍布xxxMapperA.insert()和xxxMapperB.insert(),维护性很差。
第三类是借助第三方封装好的框架,比如baomidou的dynamic-datasource-spring-boot-starter。它底层也是典型的AbstractRoutingDataSource思路,但把很多边界情况处理好了。如果团队不太关心底层实现,确实可以直接引入这个包快速搞定。不过我自己不太喜欢在八股文里依赖太多底层黑盒,万一版本升级或特殊定制时出了问题,排查成本反而不低。
我的选择是第一类方案:自己实现AbstractRoutingDataSource子类、配合ThreadLocal和AOP切面。这个方案代码量不多,但每一步都有清晰的掌控感,出了问题也知道去哪查。下面所有代码都是基于这个思路。
1.3 动态数据源路由的核心原理速览
AbstractRoutingDataSource是Spring提供给DataSource实现类扩展的抽象类。常规DataSource的getConnection()是固定返回某个真实数据库连接,而AbstractRoutingDataSource内部维护了一个Map<Object,Object>的targetDataSources,在getConnection()时先解析出当前线程应该使用哪个key,再从map中取出真实的DataSource返回连接。
所以我们只需要做三件事:
- 实现
determineCurrentLookupKey()方法,把“当前线程想要的数据源标识”返回回去; - 通过拦截器或AOP,在进入业务方法时,把目标数据源的标识设置进
ThreadLocal; - 方法执行完毕后手动清除
ThreadLocal,避免内存泄漏和数据串库。
前面这两步是正路,第三步却是很多人会漏的。漏掉后最典型的表现是:用线程池执行定时任务时,第一次运行命中A库,第二次复用同一个线程时ThreadLocal里仍然残留A库的key,实际上该跑B库了,结果又跑到A库去。这种坑查起来非常魔幻。所以“清除”和“设置”同等重要。
2. 环境准备与工程配置
2.1 依赖引入与版本选择
以Spring Boot 2.7.x为例,MyBatis官方整合包使用mybatis-spring-boot-starter,版本从线上的综合评估来说比较稳定的是3.0.2以下那批2.x版本。如果你用Spring Boot 3.x,那么mybatis-spring-boot-starter对应的3.x版本也可以,整体代码逻辑不变。
依赖清单大致如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>这里花点时间解释下AOP依赖。虽然我们用的是Spring@Transactional这类现成注解,但多数据源切换的标志位设置需要切面在业务方法前后执行。如果没有AOP,就只能靠提供DataSourceContextHolder.setDataSourceKey("master")这样的手动调用。虽然也能实现,但每个Service方法里都有手动设置,函数式味太浓,实在不够优雅。所以AOP几乎是必需品。
2.2 主配置文件的关键配置
如果把两个数据源的连接信息直接写在application.yml里,通常会这样设计:
spring: datasource: # 定义一个业务主库 master: jdbc-url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver # 定义一个只读报表库 report: jdbc-url: jdbc:mysql://localhost:3306/report_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: report password: report123 driver-class-name: com.mysql.cj.jdbc.Driver这里有个注意点。如果你同时配置了spring.datasource.url和spring.datasource.master.jdbc-url,Spring Boot的自动装配可能会因为找不到默认url而报错。因为我们后面要手动定义数据源,所以需要想办法让Spring Boot不再进行DataSourceAutoConfiguration的默认自动配置。最省事的方式是排除它:
spring: autoconfigure: exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration当然,也可以在@SpringBootApplication上写exclude属性,效果一致。我第一次做多数据源时没排除,启动直接报Failed to configure a DataSource,当时还对照了半天,后来查资料发现问题就出在这。既然数据源都要我们自己定义了,交给Spring Boot去自动读spring.datasource.url反而是多余的。
2.3 准备两个测试库
为了看得见摸得着,建议先准备两个独立的库,每个库里都建立一张表结构完全一致但是数据不一样的表。比如master_db.user_info和report_db.user_info,前者几条数据,后者几条数据。这样对比查询结果时最直观,一眼就能看出是否发生了切换。
如果本地没有MySQL环境,也可以用H2内存库来模拟,启动时初始化两个schema,配置方式完全一致。线上反正都是真实库,差别不大。
3. 完整代码实现:从路由类到AOP切面
3.1 定义数据源标识常量与注解
为了不在代码里到处写魔法字符串,定义一组常量是比较好的习惯:
public interface DataSourceKey { String MASTER = "master"; String REPORT = "report"; }自定义注解则用于标记需要切换数据源的方法或类:
import java.lang.annotation.*; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DataSource { String value() default DataSourceKey.MASTER; }默认值设为master,这样大多数默认走主库的Service方法甚至不用写注解,只有在读写报表时才显式声明。这个设计在真实项目中很实用,可以大幅减少代码改动。
3.2 数据源上下文持有器
这是整个切换机制的枢纽。它的职责是保存当前线程使用的数据源key,并且必须在每次调用后清除:
public class DataSourceContextHolder { private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>(); public static void setDataSource(String dataSourceKey) { CONTEXT.set(dataSourceKey); } public static String getDataSource() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }为什么用ThreadLocal而不用普通全局变量?那是因为数据库连接本身不是线程安全的,同一时刻不同请求可能访问不同库。ThreadLocal天然把数据和执行线程隔离,取连接时也不需要额外加锁,这也是标准做法。
有个细节要注意:使用ThreadLocal.remove()而不是ThreadLocal.set(null)。前者能彻底移除当前线程的副本,后者只是把值置空,部分线程复用场景下仍可能残留旧的Map结构。这个差异在一些较老JDK版本里问题不大,但规范点总没错。
3.3 核心路由类:继承AbstractRoutingDataSource
重点来了。自定义路由类:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); } }没有数据库连接池配置,没有复杂逻辑,继承一个方法就完成了一大半。关键点是:Spring发起getConnection()时,AbstractRoutingDataSource内部,先调用determineCurrentLookupKey()拿到当前key,再从resolvedDataSources里寻找对应的真实DataSource。
如果determineCurrentLookupKey()返回null会怎样?只要你在配置类里设置了defaultTargetDataSource,Spring会优雅降级到默认数据源。这一点一定要利用好。把它默认设为master,就能保证当切面没设置key时,系统仍然能正常跑主库。
3.4 数据源配置类
现在把tomcat.jdbc.pool或者HikariPool的信息组装起来。Spring Boot默认自带HikariCP,这里直接用它:
import com.zaxxer.hikari.HikariDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Configuration public class DataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } @Bean @ConfigurationProperties("spring.datasource.report") public DataSource reportDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } @Bean @Primary public DataSource primaryDataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceKey.MASTER, masterDataSource()); targetDataSources.put(DataSourceKey.REPORT, reportDataSource()); dynamicDataSource.setDefaultTargetDataSource(masterDataSource()); dynamicDataSource.setTargetDataSources(targetDataSources); return dynamicDataSource; } @Bean public PlatformTransactionManager transactionManager(DataSource primaryDataSource) { return new DataSourceTransactionManager(primaryDataSource); } }注意点:
@Primary必须存在于primaryDataSource()上,否则MyBatis在自动注入DataSource时会产生歧义,报NoUniqueBeanDefinitionException。targetDataSources里的key可以是任意对象,但建议用字符串常量。字符串在map查询时走hash,简单可靠。- HikariCP的配置项比如
maximum-pool-size、connection-timeout可以通过@ConfigurationProperties去预先配置,这里只做基础演示。
3.5 MyBatis配置类
MyBatis接入动态数据源时,需要手动设置SqlSessionFactory的dataSource为我们创建的DynamicDataSource,否则Mapper同样不知道去哪个库取连接。
import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.mapper.MapperScannerConfigurer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import javax.sql.DataSource; @Configuration @MapperScan("com.example.demo.mapper") public class MyBatisConfig { @Bean public SqlSessionFactory sqlSessionFactory(DataSource primaryDataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(primaryDataSource); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/*.xml")); return factoryBean.getObject(); } }如果你的XML文件中配置了typeAliasesPackage、plugins等,也可以在这里继续链式设置。只要有@MapperScan了,就不需要再使用MapperScannerConfigurer方式,两者不要重复配置。
这里有个面试高频点:如果既配置了SqlSessionFactoryBean,又在application.yml里写了mybatis.mapper-locations,到底以哪个为准?答案是手动Bean优先,yml里那个自动配置的是自动生成的SqlSessionFactory,一旦手动声明了Bean,自动配置的就会失效。所以XML路径放在Bean里设置更稳。
3.6 AOP切面实现自动切换
这个切面是整个“自动”二字的精髓。定义切点:拦截所有带有@DataSource注解的方法。在注解中拿到value,设置到上下文。方法执行结束后必须清理。
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.lang.reflect.Method; @Aspect @Component @Order(-1) public class DataSourceAspect { @Pointcut("@annotation(ds)") public void dataSourcePointcut(DataSource ds) { } @Around("dataSourcePointcut(ds)") public Object around(ProceedingJoinPoint joinPoint, DataSource ds) throws Throwable { String dsKey = ds.value(); DataSourceContextHolder.setDataSource(dsKey); try { return joinPoint.proceed(); } finally { DataSourceContextHolder.clear(); } } }关键点解析:
@Order(-1)是为了保证数据源切换在@Transactional事务开启之前生效。如果切面顺序在事务之后,那么事务开启时已经通过默认数据源拿到了数据库连接,后续切面再切换数据源也只对后续连接生效,同一个事务里可能拿到了两个不同库的连接,逻辑直接乱套。finally块里的清除是必须的。如果中途发生异常,没有finally,ThreadLocal中就会残留本次线程的key。下次这个线程再处理其他请求时,就会串库。- 如果类上使用
@DataSource,这个切面也能生效,因为@annotation(ds)不仅匹配方法上的注解,还能匹配类上的注解。但Spring AOP对类上注解的匹配是“类中所有方法都匹配”,这点放心。
实际项目中会发现,@Order这个看似不起眼的设置,比想象中的影响还大。一次线上事故就是加了事务注解后数据源不切换,查来查去最终定位到AOP顺序问题。
3.7 Service层使用示例
假设有UserService接口和实现类:
public interface UserService { List<User> listMasterUsers(); List<User> listReportUsers(); } @Service public class UserServiceImpl implements UserService { @Resource private UserMapper userMapper; @Override @DataSource(DataSourceKey.MASTER) public List<User> listMasterUsers() { return userMapper.selectAll(); } @Override @DataSource(DataSourceKey.REPORT) public List<User> listReportUsers() { return userMapper.selectAll(); } }注意,如果方法上有@Transactional注解,必须确保它不会绕过切面。默认情况下Spring对public方法做代理,所以这里方法修饰符必须是public,否则注解不生效。
可能有人会问,如果同一个类中方法A调用方法B,B上有@DataSource注解,会不会自动切换?答案是:不会。Spring AOP基于代理,自调用时没有经过代理对象,注解不会触发。这一点是很多小白容易踩的坑。要解决只能通过注入自己的代理、或者在A方法里手动设置上下文。代码这样写就能理解为什么网上总说“多数据源切换不能自调用”。
3.8 验证方法切换效果
可以简单写一个Controller或者测试类,分别调用两个Service方法,查询同样的表,观察返回数据是否来自不同库:
@RestController @RequestMapping("/user") public class UserController { @Resource private UserService userService; @GetMapping("/master") public List<User> master() { return userService.listMasterUsers(); } @GetMapping("/report") public List<User> report() { return userService.listReportUsers(); } }如果master_db里表数据是张三、李四,report_db里是隔壁老王,那么请求两个接口返回不同人名,说明切换成功。这里还可以在determineCurrentLookupKey()里打印日志,切到哪个key一目了然。但要注意打印日志别影响性能,最好用debug级别。
4. 常见问题与排查技巧实录
4.1 事务保存点与多数据源同时开启事务
如果一个业务方法同时操作多个库,就没有“一个事务”能跨两个库这种说法了。要么使用分布式事务方案(本地消息表、Seata),要么干脆接受最终一致。如果仍然使用@Transactional,它只能管住当前路由到的那个数据源。很多人误以为加上事务注解,所有数据源都处于同一个事务里,这是错误理解。
排查方法很直接:打开SQL日志,观察同一个请求里是否有多个库的连接切换,以及事务管理器到底绑定在哪个连接上。如果配置了DataSourceTransactionManager(primaryDataSource),那它管的是整个DynamicDataSource的抽象连接,但内部路由后其实已经变成具体库连接了,事务边界就会变得微妙。
如果确实需要同时修改两个库,建议把两个操作拆到独立的Service方法,或者显式地开启两个编程式事务,自己管理提交和回滚。
4.2 切换无效或偶尔串库的几个原因
第一,排查是不是自调用。Service内部方法互相调用时,AOP切面不生效。这种问题表现为:第一次调用正常,第二次调用从另一个入口进来又不正常;或者同一个方法里前一半代码走A库,后一半代码还在走A库,而不是预期B库。
第二,排查是否用了异步调用。@Async异步方法会在线程池中执行,而ThreadLocal只在提交任务的线程中传递,真实执行异步线程里ThreadLocal是空的,所以会默认走主库。此时需要用TaskDecorator包装Runnable,把父线程的key传给子线程。
第三,排查线程池复用时没有清理上下文。虽然切面清理了,但如果你有些业务代码里直接调用了DataSourceContextHolder.setDataSource(),没有在finally里清除,那么复用线程池时数据源key就会污染。
4.3 多数据源下MyBatis二级缓存问题
MyBatis的二级缓存默认是全局的,与SqlSessionFactory绑定,不管底层连接哪个库,同一个Mapper的缓存在不同数据源之间是共享的。这就意味着:如果你开了二级缓存,先查询A库的数据放进缓存,再切到B库查询同样的Mapper,结果可能直接被缓存命中,返回A库的旧数据。
在动态数据源场景下,强烈建议关掉二级缓存,或者至少保证使用同一个Mapper的查询都缓存了数据源标识。最简单的办法是mybatis.configuration.cache-enabled=false。如果必须使用缓存,那就为每个数据源准备独立的SqlSessionFactory,而不要用统一的MyBatis配置。
4.4 数据库连接池独立配置的重要性
使用动态数据源路由,从连接池层面看,主库和报表库实际上是两个独立连接池,配置的maximum-pool-size也是各自的。不要图省事在application.yml里把两者合起来配置。否则可能出现一种情况:报表库线程阻塞导致连接池全部占满,主库的查询也被波及。两个数据源连接池一定要分开调优,至少主库和报表库的连接池大小分配策略就不应该一样。
我见过一个项目,把所有数据源连接数都设成100,报表库慢查询一堆,把连接池占满,主库也疯狂超时。最后把报表库连接池降到20,主库保持100,应用立刻恢复正常。这种踩坑经验,只有在线上经历过才印象深刻。
4.5 分页插件与多数据源兼容
使用PageHelper分页插件时,需要额外注意它可能自动识别数据库方言。多数据源下,如果A库是MySQL,B库是PostgreSQL,PageHelper虽然支持方言自动识别,但有时候会因为首次加载路由信息而误判。稳妥做法是手动设置helper-dialect为固定的数据库类型,或者让插件依赖DataSource去动态判断。
同样道理,MyBatis-Plus的分页插件配置在MybatisPlusInterceptor里,需要对每个数据库类型的DbType做精确判断。如果两个库类型不一样,建议把@InterceptorIgnore或DbType判断逻辑写好,免得分页SQL生成错误。
4.6 数据源配置应该放在哪里
生产环境别把密码放在application.yml里。多数据源只是技术问题,但密码管理就是安全问题了。建议使用Jasypt加密,或者至少使用环境变量替换${DB_PASSWORD}。每个数据源的配置可以放在同一个命名空间下,便于阅读:
spring: datasource: master: url: ${MASTER_DB_URL} username: ${MASTER_DB_USERNAME} password: ${MASTER_DB_PASSWORD} report: url: ${REPORT_DB_URL} username: ${REPORT_DB_USERNAME} password: ${REPORT_DB_PASSWORD}这么写还有个好处:发版时只需要调整环境变量里的值,而不用去改配置文件。环境差异造成的困扰会少很多。
5. 从单一方案到整体架构的一点扩展思考
多数据源切换本身不算复杂,但它在整个系统架构中的位置很特别。它通常不是单独存在,而是嵌在读写分离、微服务、数据同步等多种场景之中。
5.1 读写分离场景下的切换策略
如果主库是写库,从库是读库,可以把路由规则定义为“强制走主库”和“默认走从库”。此时性能优化的核心不是切换本身,而是读的压力均衡。如果从库有多个,那么在determineCurrentLookupKey()里就不应该只返回一个固定key,而应该做负载均衡。简单的方式是按线程名取模,或者用一个AtomicInteger累加然后与从库数量取余。这种场景下,AbstractRoutingDataSource依然适用,只不过targetDataSources里要多放几个从库key。
5.2 将多数据源切换抽象为通用组件
当团队里多个项目都有切库需求时,完全可以把这个方案抽成一个spring-boot-starter。把DynamicDataSource、DataSourceContextHolder、DataSourceAspect全部打成通用jar包,只暴露一个@DataSource注解。这样每个业务项目接入时就两三分钟的事,不必重复造轮子。
这个方向我很推荐。多数据源组件化之后,还可以统一埋点,统计每个库的调用频次、慢SQL等,对运维监控都有很大帮助。不过做通用starter时要考虑兼容性,比如某些项目可能使用了mybatis-plus写法,这时切面切入点的包路径最好可以配置而不是写死。
5.3 与分布式事务的边界划分
当一个请求涉及多个库时,就得先想清楚“事务边界”到底在哪。动态数据源只是帮你把请求路由到对应的库,它并不负责跨库数据一致性。如果流程是:A库扣减库存,B库生成订单,那么任何一个失败都必须让另一个回滚,此时就需要Seata这类方案。
如果业务对强一致性要求没有那么高,更推荐使用本地消息表或者定时任务补偿,避免为了单纯的“跨两个库”引入一个分布式事务中间件,那是运维上的一个较大负担。权衡下来,多数据源的路由机制可以保持不变,只需要在业务代码里自行编排好顺序和补偿逻辑。
6. 最后分享一点实操感悟
我用这个方案在好几个项目里落地过,不能说没踩坑,但踩过的坑大多集中在“清理ThreadLocal”和“AOP顺序”上。只要把这两点刻在脑子里,大概率能避掉80%的问题。
写代码时还有一个习惯很值得推荐:给每个数据源加个自定义前缀,比如日志里输出[ds-key=master]。这样线上排查问题时,只要看到一行日志,马上就能确认当前请求走到了哪个库,不需要再通过数据内容反推。
如果后续有同事接手这套代码,建议把数据源标识常量和注解放到独立包里,命名上尽量不要出现master/slave这种容易在权限上引起误会的词,可以用primary/replica或者业务名来替代。
多数据源切换说到底就那么一点代码量,但它能很好地考验一个人对Spring生命周期、AOP代理、事务、连接池的整体理解。把这一套吃透了,再去接触ShardingSphere或者其他动态数据源框架,都会轻松很多。