“MybatisPlus 多租户架构(Multi-tenancy)实现详解”旨在为需要在一个应用中支持多个租户的开发人员提供一种解决方案。在这个架构中,多个租户可以共享相同的代码库和实例,并在逻辑上隔离数据。
实现多租户架构需要考虑以下三个方面:
- 租户隔离
使用 Mybatis-Plus 提供的 SqlParserInterceptor 对 SQL 进行拦截,通过替换表名、增加 WHERE 条件等方式实现租户隔离。具体实现代码如下:
public class MybatisPlusConfig {
@Bean
public SqlParserInterceptor sqlParserInterceptor() {
return new SqlParserInterceptor() {
@Override
public void prepare(Invocation invocation) {
if (TenantContextHolder.getTenantId() != null) {
MetaObject metaObject = SystemMetaObject.forObject(invocation.getTarget());
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
String sql = boundSql.getSql();
sql = sql.replaceAll(mappedStatement.getId() + ".", mappedStatement.getId() + "_" + TenantContextHolder.getTenantId() + ".");
sql = "select t.* from (" + sql + ") t where t.tenant_id = " + TenantContextHolder.getTenantId();
metaObject.setValue("delegate.boundSql.sql", sql);
}
}
};
}
}
- 租户切换
使用 ThreadLocal 实现当前租户ID在同一线程中的共享,在实际访问数据库时增加租户ID参数。具体实现代码如下:
public class TenantContextHolder {
private static final ThreadLocal<String> TENANT_ID = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
TENANT_ID.set(tenantId);
}
public static String getTenantId() {
return TENANT_ID.get();
}
public static void clearTenantId() {
TENANT_ID.remove();
}
}
- 租户配置
为每一个租户配置独立的数据源,使用 Mybatis-Plus 提供的 DynamicDataSource 实现动态数据源切换。具体实现代码如下:
public class DataSourceConfig {
private final static Map<String, DruidDataSource> DATA_SOURCE_MAP = new ConcurrentHashMap<>();
private final static String DEFAULT_TENANT_ID = "1";
@Bean("tenantDataSource")
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource tenantDataSource() {
return new DruidDataSource();
}
@Bean("dynamicDataSource")
public DynamicDataSource dataSource(@Qualifier("tenantDataSource") DataSource tenantDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DEFAULT_TENANT_ID, tenantDataSource);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(tenantDataSource);
dynamicDataSource.setTargetDataSources(targetDataSources);
DynamicDataSourceContextHolder.addDataSourceKeys(DEFAULT_TENANT_ID);
DATA_SOURCE_MAP.put(DEFAULT_TENANT_ID, (DruidDataSource) tenantDataSource);
return dynamicDataSource;
}
private static DruidDataSource createDataSource(String driverClassName, String url, String username, String password) {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
public static void addDataSource(String tenantId, String driverClassName, String url, String username, String password) {
DruidDataSource dataSource = createDataSource(driverClassName, url, username, password);
DATA_SOURCE_MAP.put(tenantId, dataSource);
DynamicDataSourceContextHolder.addDataSourceKeys(tenantId);
}
public static void removeDataSource(String tenantId) {
DATA_SOURCE_MAP.remove(tenantId);
DynamicDataSourceContextHolder.removeDataSourceKeys(tenantId);
}
public static void switchDataSource(String tenantId) throws SQLException {
if (!DATA_SOURCE_MAP.containsKey(tenantId)) {
throw new RuntimeException("tenantId not exists: " + tenantId);
}
DynamicDataSource dynamicDataSource = (DynamicDataSource) ApplicationContextHolder.getBean("dynamicDataSource");
dynamicDataSource.setTargetDataSource(DATA_SOURCE_MAP.get(tenantId));
TenantContextHolder.setTenantId(tenantId);
}
}
示例一:基于 Header 实现租户切换
在这个示例中,我们将在 HTTP Header 中增加 Tenant ID,通过定义 Filter 将该 Header 设置为当前租户ID。具体实现代码如下:
public class HeaderTenantFilter implements Filter {
private final static String TENANT_HEADER = "X-TENANT-ID";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String tenantId = httpServletRequest.getHeader(TENANT_HEADER);
TenantContextHolder.setTenantId(tenantId != null ? tenantId : DataSourceConfig.DEFAULT_TENANT_ID);
chain.doFilter(request, response);
TenantContextHolder.clearTenantId();
}
}
示例二:基于 sub-domain 实现租户切换
在这个示例中,我们将为每个租户分配独立的子域名,并通过定义 Servlet 拦截器解析当前访问的子域名并设置为当前租户ID。具体实现代码如下:
public class SubdomainTenantInterceptor extends HandlerInterceptorAdapter {
private final static String HTTP_SCHEMA = "http";
private final static String HTTPS_SCHEMA = "https";
private final static String DEFAULT_DOMAIN = "example.com";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String domain = (request.getServerName()).toLowerCase();
String schema = request.isSecure() ? HTTPS_SCHEMA : HTTP_SCHEMA;
String tenantId = getTenantIdBySubdomain(domain);
String redirectUrl = getRedirectUrl(schema, domain);
if (StringUtils.isBlank(tenantId)) {
response.sendRedirect(redirectUrl);
return false;
} else {
TenantContextHolder.setTenantId(tenantId);
return true;
}
}
private String getTenantIdBySubdomain(String domain) {
String tenantId = null;
if (StringUtils.isNotBlank(domain)) {
String[] subdomains = domain.split("\\.");
if (subdomains.length > 0) {
tenantId = subdomains[0];
}
}
return tenantId;
}
private String getRedirectUrl(String schema, String domain) {
return schema + "://" + DOMAIN + "/no-tenant";
}
}
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:MybatisPlus 多租户架构(Multi-tenancy)实现详解 - Python技术站