在Spring Boot中实现数据源切换,除了AOP切面,还有几种非常灵活的策略。下面我用一个表格对比各种方案,然后重点介绍两种最实用的方法:
| 策略类型 | 实现方式 | 灵活性 | 代码侵入性 | 适用场景 |
|---|---|---|---|---|
| 注解驱动 | 使用@DS等注解标记方法/类 |
高 | 低 | 精确到方法级别的数据源控制 |
| 手动编程 | 在代码中调用API动态切换 | 最高 | 高 | 复杂业务逻辑、同一方法内多数据源 |
| 请求特征路由 | 基于HTTP头、参数等自动路由 | 中高 | 低 | 多租户、按客户分库等场景 |
| 方法规则路由 | 基于方法名约定自动切换 | 中 | 低 | 有固定命名规范的读写分离 |
🔁 注解驱动切换
这是目前最流行且优雅的方案,MyBatis-Plus的动态数据源组件提供了开箱即用的支持。
配置依赖
xml
<dependency>
<groupId>com.baomidou</groupId>
- <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
- <version>3.5.0</version>
</dependency>
配置文件
yaml
spring:
datasource:
dynamic:
primary: master # 默认数据源
strict: false # 是否严格匹配数据源
datasource:
master:
url: jdbc:mysql://localhost:3306/master
username: root
password: master_password
slave:
url: jdbc:mysql://localhost:3306/slave
username: root
password: slave_password
使用@DS注解
typescript
@Service
public class UserService {
// 默认使用主库(写操作)
public void createUser(User user) {
userMapper.insert(user);
}
// 显式指定从库(读操作)
@DS("slave")
public User getUserById(Long id) {
return userMapper.selectById(id);
}
// 在Mapper层直接指定数据源
@DS("slave")
public List<User> findActiveUsers() {
return userMapper.selectActiveUsers();
}
}
优点:声明式配置,代码侵入性低,支持类级别和方法级别的精细控制。
⚙️ 手动编程切换
对于需要在同一方法内使用多个数据源的复杂场景,手动控制提供了最大灵活性。
基本API使用
java
@Service
public class OrderService {
public void processOrder(Long orderId) {
// 第一阶段:从主库读取订单
DynamicDataSourceContextHolder.push("master");
try {
Order order = orderMapper.selectById(orderId);
// 业务处理...
} finally {
DynamicDataSourceContextHolder.poll();
}
// 第二阶段:写入从库进行数据分析
DynamicDataSourceContextHolder.push("slave");
try {
analysisMapper.insertOrderAnalysis(order);
} finally {
DynamicDataSourceContextHolder.poll();
}
}
}
基于HTTP请求的自动切换
通过过滤器或拦截器实现基于请求特征的路由:
typescript
@Component
@WebFilter(urlPatterns = "/*")
public class DataSourceFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 从请求头获取数据源标识(如多租户场景)
String tenantId = httpRequest.getHeader("X-Tenant-Id");
// 或从请求参数判断业务类型
String businessType = httpRequest.getParameter("bizType");
String dataSourceKey = determineDataSource(tenantId, businessType);
DynamicDataSourceContextHolder.push(dataSourceKey);
try {
chain.doFilter(request, response);
} finally {
DynamicDataSourceContextHolder.poll();
}
}
private String determineDataSource(String tenantId, String businessType) {
// 复杂的路由逻辑
if ("report".equals(businessType)) {
return "report_db";
} else if (tenantId != null) {
return "tenant_" + tenantId;
}
return "master"; // 默认数据源
}
}
🧠 自定义路由规则
通过继承AbstractRoutingDataSource并重写determineCurrentLookupKey()方法,可以实现基于业务逻辑的复杂路由。
基于方法名的路由示例
scala
public class BusinessRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 获取当前方法名进行路由决策
String methodName = getCurrentMethodName();
if (methodName.startsWith("find") || methodName.startsWith("query")
|| methodName.startsWith("select") || methodName.startsWith("get")) {
return "slave"; // 读操作路由到从库
} else if (methodName.startsWith("insert") || methodName.startsWith("update")
|| methodName.startsWith("delete") || methodName.startsWith("save")) {
return "master"; // 写操作路由到主库
}
// 默认路由策略
return isReadOperation(methodName) ? "slave" : "master";
}
}
基于参数值的路由
scala
public class ParameterBasedRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 从线程上下文或参数中获取路由信息
String businessUnit = RequestContextHolder.getBusinessUnit();
String dataType = RequestContextHolder.getDataType();
// 组合路由逻辑
if ("finance".equals(businessUnit) && "sensitive".equals(dataType)) {
return "finance_secure_db";
} else if ("report".equals(dataType)) {
return "reporting_db";
}
return "default_db";
}
}
⚠️ 实战注意事项
- 事务管理 :在手动切换数据源时,确保同一个事务内使用同一数据源,避免跨数据源事务导致的数据不一致问题。
- 资源清理 :使用
try-finally确保每次数据源切换后都能正确清理上下文,防止内存泄漏 和数据源污染。 - 性能考量 :频繁的数据源切换会带来性能开销,在高性能场景下应考虑批量操作 或连接复用。
- 降级策略 :始终配置合理的主从延迟处理 和故障降级机制,确保主库不可用时系统的可用性。
💎 总结
选择合适的数据源切换策略需要权衡灵活性和复杂性。对于大多数场景,注解驱动方案 提供了最佳实践;对于复杂业务逻辑,手动编程控制 不可或缺;而对于有固定路由规则的场景,自定义路由策略能够提供最优雅的解决方案。