使用AbstractRoutingDataSource实现数据源的动态切换
AbstractRoutingDataSource
是 Spring 框架提供的一个抽象类,用于实现动态数据源路由。这个类主要用于多数据源场景,其中可以根据不同的条件动态地切换到不同的数据源。
实现原理
AbstractRoutingDataSource
继承自 Spring 的 AbstractDataSource
类,并实现了 DataSource
接口。它内部维护了一个映射(Map),用于存储数据源标识和对应的数据源实例。当需要获取连接时,AbstractRoutingDataSource
会调用一个抽象方法 determineCurrentLookupKey()
来确定当前的数据源标识,然后根据这个标识从映射中获取对应的数据源,并从该数据源中获取连接。
关键方法解析
-
determineCurrentLookupKey(): 这是一个抽象方法,需要用户自己实现。这个方法用于确定当前的数据源标识,其返回值将用作查找映射中对应数据源的关键字。通常,可以在这个方法中获取当前线程的一些状态或上下文信息,来动态决定使用哪个数据源。
-
afterPropertiesSet(): 这个方法是在设置完所有属性后被调用的。在这个方法中,
AbstractRoutingDataSource
会验证数据源映射是否已经被正确设置,并且如果设置了默认数据源,也会进行检查。 -
getConnection() 和 getConnection(String username, String password): 这两个方法是
DataSource
接口要求实现的方法。在AbstractRoutingDataSource
中,这两个方法的实现逻辑是先调用determineCurrentLookupKey()
方法获取当前数据源标识,然后根据这个标识从映射中找到对应的数据源,并调用该数据源的getConnection()
方法获取连接。
场景实例
我们设计一个这样的场景,比如一个多租户(multi-tenant)应用,其中每个租户都有自己的数据库。在这个场景中,系统根据当前用户的租户ID动态切换数据源。我们将使用 Spring Boot、Spring Data JPA 和 AbstractRoutingDataSource
来实现这个场景。
步骤 1: 定义租户识别逻辑
首先,我们需要一个机制来识别当前请求属于哪个租户。假设每个请求的HTTP头部都包含一个X-TenantID
字段来标识租户ID:
@Component
public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String tenantId = request.getHeader("X-TenantID");
if (tenantId != null) {
TenantContext.setCurrentTenant(tenantId);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
TenantContext.clear();
}
}
在这里,TenantContext
是一个存储当前线程租户ID的类:
public class TenantContext {
private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
public static void setCurrentTenant(String tenantId) {
currentTenant.set(tenantId);
}
public static String getCurrentTenant() {
return currentTenant.get();
}
public static void clear() {
currentTenant.remove();
}
}
步骤 2: 配置动态数据源
接下来,我们需要配置动态数据源来根据租户ID选择正确的数据库:
public class MultiTenantDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TenantContext.getCurrentTenant();
}
}
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
MultiTenantDataSource dataSource = new MultiTenantDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
// 假设已经有一个方法来获取所有租户的数据源配置
Map<String, DataSource> tenantDataSources = tenantDataSources();
targetDataSources.putAll(tenantDataSources);
dataSource.setTargetDataSources(targetDataSources);
dataSource.setDefaultTargetDataSource(defaultDataSource()); // 默认数据源
dataSource.afterPropertiesSet();
return dataSource;
}
private Map<String, DataSource> tenantDataSources() {
// 这里应该从配置文件或数据库中加载所有租户的数据源配置
// 举例,假设有两个租户tenantA和tenantB
Map<String, DataSource> result = new HashMap<>();
result.put("tenantA", createDataSource("jdbc:mysql://localhost:3306/tenantA"));
result.put("tenantB", createDataSource("jdbc:mysql://localhost:3306/tenantB"));
return result;
}
private DataSource createDataSource(String url) {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl(url);
dataSource.setUsername("username");
dataSource.setPassword("password");
return dataSource;
}
private DataSource defaultDataSource() {
// 定义默认数据源
return createDataSource("jdbc:mysql://localhost:3306/defaultDb");
}
}
步骤 3: 集成到Spring MVC
最后,我们需要确保每个请求都会通过我们的TenantInterceptor
,以便正确设置租户上下文:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private TenantInterceptor tenantInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tenantInterceptor);
}
}
转载自:https://juejin.cn/post/7344259963629846568