方案一:配置文件 + 硬编码切换(基础版)
你提到的第一种方式,是最基础、最直观的实现。它的核心流程是:
- 配置 :在
application.yml或mybatis-config.xml中定义多个数据源(如master,slave1,slave2)。 - 持有 :在代码中通过一个工具类(比如使用
ThreadLocal)持有当前线程要使用的数据源 key。 - 切换 :在业务代码中,手动调用工具类的方法(如
DataSourceContextHolder.setDataSource("slave1"))来切换。
| 维度 | 评价 |
|---|---|
| 优点 | 简单直接,容易理解,没有黑盒。适合业务逻辑简单、数据源切换点非常明确的场景。 |
| 缺点 | 切换逻辑和业务代码耦合,侵入性强。如果忘记在方法结束时清除上下文,可能导致后续逻辑使用了错误的数据源,产生难以追踪的 Bug。 |
方案二:AOP 切面编程(解耦版)
这是对方案一的优化,也是目前最常用的实践。它通过 Spring AOP 将"切换"这个动作从业务代码中抽离出来。
核心逻辑是:定义一个注解(如 @DataSource("slave1")),然后编写一个切面,拦截所有带有该注解的方法,在方法执行前解析注解并将数据源标识设置到 ThreadLocal 中,方法执行后在 @After 中清除标识。
| 维度 | 评价 |
|---|---|
| 优点 | 切换逻辑与业务解耦,代码更干净。只需在类或方法上加一个注解就能切换,可维护性高,是生产环境最推荐的实现方式。 |
| 缺点 | 需要编写切面类和注解,有一定学习成本。对 AOP 的作用范围(如 @Transactional 的优先级)需要特别注意,避免事务开启后再切换数据源导致切换失效。 |
方案三:继承 AbstractRoutingDataSource(动态路由版)
这是 Spring 提供的官方扩展点,也是实现方案一和方案二的底层基石。
AbstractRoutingDataSource 本身是一个数据源,它内部管理着一个 Map<Object, Object> 来存放真实数据源,并根据 determineCurrentLookupKey() 方法返回的 key 来决定路由到哪个真实数据源。我们只需要实现这个方法来返回我们通过 ThreadLocal 设置的 key 即可。
很多资料和第三方库(包括 MyBatis-Plus 的
dynamic-datasource)的底层,本质上都是对AbstractRoutingDataSource的封装和增强。
方案四:使用第三方库(成熟版)
你提到的 Druid(数据库连接池)和 HikariCP(连接池),它们本身并不直接提供数据源切换功能,但它们是这个方案中不可或缺的"高性能连接池"组件。
真正用于动态切换数据源的明星第三方库,是 MyBatis-Plus 生态下的 dynamic-datasource。
| 维度 | 评价 |
|---|---|
| 优点 | 开箱即用,功能强大 。支持数据源分组、密码解密、多主多从、敏感信息加密、集成 Seata 分布式事务等。只需引入依赖并在 application.yml 中配置,然后直接在方法上使用 @DS("slave1") 注解即可。 |
| 缺点 | 引入了一个外部依赖。如果项目没有使用 MyBatis-Plus,需要额外集成。对于非常简单的需求,可能显得有点"重"。 |
📊 最终选择指南
| 你的情况 | 推荐方案 |
|---|---|
| 项目简单,切换点极少(1-2个),且未来不会扩展 | 方案一(硬编码)即可。 |
| 标准企业级应用,使用 Spring Boot + MyBatis | 方案二(自研 AOP + AbstractRoutingDataSource) 是主流选择,平衡了灵活性与可控性。 |
| 已经使用 MyBatis-Plus,或希望最大程度简化配置 | 方案四(dynamic-datasource 插件) 是最高效的选择。 |
有非常复杂的动态路由规则(如根据 userId 哈希取模) |
方案三(继承 AbstractRoutingDataSource) 是最灵活、最底层的方式,可以在 determineCurrentLookupKey() 中实现任意逻辑。 |
对于大多数项目,方案二(AOP + 注解 + AbstractRoutingDataSource) 和 方案四(MyBatis-Plus 的 dynamic-datasource) 是最值得考虑的。
另外补充一点,无论选择哪种方案,都要注意:如果在 Spring 事务(@Transactional)中切换数据源,必须在事务开启之前进行 。因为事务管理器在开启时就已经确定了要使用的数据源,之后的切换将不会生效。如果需要在事务中切换数据源,可以考虑使用 @Transactional(propagation = Propagation.REQUIRES_NEW) 开启一个新的事务,或将切换逻辑放在一个没有事务的方法中调用。