在微服务架构下,实现动态数据源路由是应对多租户、分库分表、读写分离等复杂场景的关键技术。它能显著提升系统的灵活性、可扩展性和数据处理能力。下面我将为您详细讲解几种主流的实现方案。
下表对比了三种主要的动态数据源路由方案,帮助您快速了解其核心特点:
| 方案类型 | 核心机制 | 适用场景 | 优点 | 考虑因素 |
|---|---|---|---|---|
基于AbstractRoutingDataSource的自定义路由 |
继承Spring抽象类,通过ThreadLocal上下文和AOP/注解动态路由 | 多租户(按租户ID分库)、业务分库、需要高度定制化路由逻辑的场景 | 灵活性极高,与Spring框架原生集成好,可完全掌控路由逻辑 | 需自行实现大部分代码(如AOP、上下文管理),复杂度较高 |
基于dynamic-datasource-spring-boot-starter组件 |
使用@DS注解在方法或类上声明式切换数据源 |
读写分离、一主多从、多种类型数据库混合连接 | 开箱即用,配置简单,内置负载均衡,极大减少编码量 | 路由逻辑的定制能力不如自定义方案灵活 |
| 集成ShardingSphere进行分库分表 | 作为客户端中间件,在JDBC层对SQL进行解析、路由、重写和结果归并 | 大规模数据分片、分库分表、读写分离+数据分片等复杂场景 | 功能强大,屏蔽底层数据库复杂性,支持分布式事务 | 架构较重,学习曲线较陡,对SQL有一定限制(如不支持跨库关联) |
💡 方案一:基于 AbstractRoutingDataSource的自定义路由
这是Spring框架提供的基础能力,非常适合需要精细控制路由逻辑的场景,例如根据登录用户信息或特定的业务规则来选择数据库。
实现步骤
-
定义数据源上下文持有者 :使用
ThreadLocal来确保每个线程的数据源选择是隔离的,这是实现动态路由的核心。typescriptpublic class DataSourceContextHolder { private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); public static void setDataSourceType(String dataSourceType) { CONTEXT_HOLDER.set(dataSourceType); } public static String getDataSourceType() { return CONTEXT_HOLDER.get(); } public static void clearDataSourceType() { CONTEXT_HOLDER.remove(); } } -
创建动态路由数据源 :继承
AbstractRoutingDataSource并重写determineCurrentLookupKey方法。scalapublic class DynamicRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { // 该方法会在每次数据库操作前被调用,决定使用哪个数据源 return DataSourceContextHolder.getDataSourceType(); } } -
配置多个数据源并启用路由 :在配置类中,将多个目标数据源(如
masterDataSource,slaveDataSource) 注入到一个Map中,并设置为动态数据源的目标数据源。less@Configuration public class DataSourceConfig { @Bean @Primary public DataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("master", masterDataSource); targetDataSources.put("slave", slaveDataSource); DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource(); routingDataSource.setDefaultTargetDataSource(masterDataSource); // 设置默认数据源 routingDataSource.setTargetDataSources(targetDataSources); return routingDataSource; } } -
使用AOP和自定义注解切换数据源 :可以定义一个注解(如
@DataSource),然后通过AOP切面,在方法执行前根据注解值切换数据源。java@Aspect @Component public class DataSourceAspect { @Before("@annotation(dataSource)") public void beforeSwitchDataSource(JoinPoint joinPoint, DataSource dataSource) { DataSourceContextHolder.setDataSourceType(dataSource.value()); } @After("@annotation(dataSource)") public void afterSwitchDataSource(JoinPoint joinPoint, DataSource dataSource) { DataSourceContextHolder.clearDataSourceType(); // 清理,防止内存泄漏 } }
🚀 方案二:使用 dynamic-datasource-spring-boot-starter组件
对于常见的读写分离、一主多从场景,这是一个非常高效的选择,它能让你通过简单的配置和注解完成工作。
实现步骤
-
引入依赖:
xml<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.5.0</version> <!-- 请使用最新版本 --> </dependency> -
配置数据源 :在
application.yml中配置主从数据源。配置项以下划线_分割,相同前缀的数据源会被自动分组。yamlspring: datasource: dynamic: primary: master # 设置默认数据源 strict: false # 是否严格匹配数据源 datasource: master: url: jdbc:mysql://ip1:3306/db username: root password: 123456 slave_1: url: jdbc:mysql://ip2:3306/db username: root password: 123456 slave_2: url: jdbc:mysql://ip3:3306/db username: root password: 123456 -
使用
@DS注解切换 :在Service层的方法或类上使用@DS注解,即可轻松切换。组件内置了同组数据源的负载均衡。less@Service @DS("slave") // 类级别注解,此类下所有方法默认使用slave组(随机选择slave_1或slave_2) public class UserServiceImpl implements UserService { @Override @DS("master") // 方法级别注解优先,此方法使用master数据源 public void updateUser(User user) { // ... 更新操作 } @Override // 未标注,使用类注解,即slave组 public User getUserById(Long id) { // ... 查询操作 } }
🔧 方案三:集成 ShardingSphere 进行分库分表
当单库单表成为性能瓶颈时,ShardingSphere提供了强大的分库分表、读写分离及分布式事务能力。
核心配置示例
其配置核心是定义分片规则,例如根据用户ID(user_id)进行分库分表:
yaml
spring:
shardingsphere:
rules:
sharding:
tables:
t_user:
# 指定实际的数据节点,表示数据分布在ds0和ds1两个库,每个库有t_user0到t_user3四张表
actual-data-nodes: ds$->{0..1}.t_user$->{0..3}
# 分库策略
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: database-inline
# 分表策略
table-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: table-inline
# 定义分片算法
sharding-algorithms:
database-inline:
type: INLINE
props:
algorithm-expression: ds$->{user_id % 2} # 分库算法:user_id % 2
table-inline:
type: INLINE
props:
algorithm-expression: t_user$->{user_id % 4} # 分表算法:user_id % 4
⚠️ 关键注意事项与实践建议
无论选择哪种方案,以下几点都需要特别关注:
- 事务管理 :在使用了动态数据源切换的方法中,尤其是跨不同物理数据库的操作,需要谨慎处理事务 。Spring的
@Transactional注解通常在一个事务内会使用同一个数据源连接。如果需要在事务中切换数据源,可能需要更复杂的处理,例如使用分布式事务(JTA)。 - 连接池配置 :为每个数据源配置合适的连接池参数(如
HikariCP或Druid的maximum-pool-size,connection-timeout),这对系统性能和稳定性至关重要。 - 配置信息加密与安全 :生产环境中,数据库密码等敏感信息不应明文写在配置文件中。可以使用
Jasypt等工具进行加密,或结合Spring Cloud Config、Nacos等配置中心统一管理加密后的配置。 - 链路追踪与监控 :在微服务架构下,一个请求可能涉及多个数据源的操作。可以集成Spring Cloud Sleuth等组件,将数据源切换的关键信息(如数据源名称)添加到链路追踪的Span标签中,便于问题排查和性能分析。
💎 总结与选择
选择哪种方案,最终取决于您的具体业务需求和技术考量:
- 如果只是简单的读写分离 或一主多从 ,追求快速实现,
dynamic-datasource-spring-boot-starter组件是首选。 - 如果路由逻辑非常个性化 (如根据复杂业务规则选择数据库),那么基于
AbstractRoutingDataSource的自定义路由提供了最大的灵活性。 - 如果面临海量数据 ,需要进行分库分表 ,那么ShardingSphere是更专业和强大的选择。