倩女幽魂游团手游
1.87 GB · 2025-11-15
在Spring Boot中实现数据源切换,除了AOP切面,还有几种非常灵活的策略。下面我用一个表格对比各种方案,然后重点介绍两种最实用的方法:
| 策略类型 | 实现方式 | 灵活性 | 代码侵入性 | 适用场景 |
|---|---|---|---|---|
| 注解驱动 | 使用@DS等注解标记方法/类 | 高 | 低 | 精确到方法级别的数据源控制 |
| 手动编程 | 在代码中调用API动态切换 | 最高 | 高 | 复杂业务逻辑、同一方法内多数据源 |
| 请求特征路由 | 基于HTTP头、参数等自动路由 | 中高 | 低 | 多租户、按客户分库等场景 |
| 方法规则路由 | 基于方法名约定自动切换 | 中 | 低 | 有固定命名规范的读写分离 |
这是目前最流行且优雅的方案,MyBatis-Plus的动态数据源组件提供了开箱即用的支持。
<dependency>
<groupId>com.baomidou</groupId>
- <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
- <version>3.5.0</version>
</dependency>
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
@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();
}
}
优点:声明式配置,代码侵入性低,支持类级别和方法级别的精细控制。
对于需要在同一方法内使用多个数据源的复杂场景,手动控制提供了最大灵活性。
@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();
}
}
}
通过过滤器或拦截器实现基于请求特征的路由:
@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()方法,可以实现基于业务逻辑的复杂路由。
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";
}
}
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确保每次数据源切换后都能正确清理上下文,防止内存泄漏和数据源污染。选择合适的数据源切换策略需要权衡灵活性和复杂性。对于大多数场景,注解驱动方案提供了最佳实践;对于复杂业务逻辑,手动编程控制不可或缺;而对于有固定路由规则的场景,自定义路由策略能够提供最优雅的解决方案。