摘要:#技术分享创建多数据源配置:为每个租户配置独立的数据源@Configurationpublic class MultiTenantDatabaseConfig { @Autowired private TenantDataSourceProperties pr
多租户(Multi-tenancy)是一种软件架构模式,允许单个应用实例服务于多个客户(租户),同时保持租户数据的隔离性和安全性。
通过合理的多租户设计,企业可以显著降低运维成本、提升资源利用率,并实现更高效的服务交付。
本文将分享 SpringBoot 环境下实现多租户系统的5种架构设计方案
独立数据库模式为每个租户提供完全独立的数据库实例,是隔离级别最高的多租户方案。在这种模式下,租户数据完全分离,甚至可以部署在不同的服务器上。
#技术分享创建多数据源配置 :为每个租户配置独立的数据源@Configurationpublic class MultiTenantDatabaseConfig { @Autowired private TenantDataSourceProperties properties; @Bean public DataSource dataSource { AbstractRoutingDataSource multiTenantDataSource = new TenantAwareRoutingDataSource; Map targetDataSources = new HashMap; for (TenantDataSourceProperties.TenantProperties tenant : properties.getTenants) { DataSource tenantDataSource = createDataSource(tenant); targetDataSources.put(tenant.getTenantId, tenantDataSource); } multiTenantDataSource.setTargetDataSources(targetDataSources); return multiTenantDataSource; } private DataSource createDataSource(TenantDataSourceProperties.TenantProperties tenant) { HikariDataSource dataSource = new HikariDataSource; dataSource.setJdbcUrl(tenant.getUrl); dataSource.setUsername(tenant.getUsername); dataSource.setPassword(tenant.getPassword); dataSource.setDriverClassName(tenant.getDriverClassName); return dataSource; }}实现租户感知的数据源路由 :public class TenantAwareRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey { return TenantContextHolder.getTenantId; }}租户上下文管理 :public class TenantContextHolder { private static final ThreadLocal CONTEXT = new ThreadLocal; public static void setTenantId(String tenantId) { CONTEXT.set(tenantId); } public static String getTenantId { return CONTEXT.get; } public static void clear { CONTEXT.remove; }}添加租户识别拦截器 :@Componentpublic class TenantIdentificationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String tenantId = extractTenantId(request); if (tenantId != null) { TenantContextHolder.setTenantId(tenantId); return true; } response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return false; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { TenantContextHolder.clear; } private String extractTenantId(HttpServletRequest request) { String tenantId = request.getHeader("X-TenantID"); if (tenantId == null) { String host = request.getServerName; if (host.contains(".")) { tenantId = host.split("\.")[0]; } } return tenantId; }}配置拦截器 :@Configurationpublic class WebConfig implements WebMvcConfigurer { @Autowired private TenantIdentificationInterceptor tenantInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(tenantInterceptor) .addPathPatterns("/api/**"); }}实现动态租户管理 :@Entity@Table(name = "tenant")public class Tenant { @Id private String id; @Column(nullable = false) private String name; @Column(nullable = false) private String databaseUrl; @Column(nullable = false) private String username; @Column(nullable = false) private String password; @Column(nullable = false) private String driverClassName; @Column private boolean active = true;}@Repository public interface TenantRepository extends JpaRepository { List findByActive(boolean active); }@Service public class TenantManagementService { @Autowired private TenantRepository tenantRepository; @Autowired private DataSource dataSource; @Autowired private ApplicationContext applicationContext; private final Map tenantDataSources = new ConcurrentHashMap; @PostConstruct public void initializeTenants { List activeTenants = tenantRepository.findByActive(true); for (Tenant tenant : activeTenants) { addTenant(tenant); } } public void addTenant(Tenant tenant) { HikariDataSource dataSource = new HikariDataSource; dataSource.setJdbcUrl(tenant.getDatabaseUrl); dataSource.setUsername(tenant.getUsername); dataSource.setPassword(tenant.getPassword); dataSource.setDriverClassName(tenant.getDriverClassName); tenantDataSources.put(tenant.getId, dataSource); updateRoutingDataSource; tenantRepository.save(tenant); } public void removeTenant(String tenantId) { DataSource dataSource = tenantDataSources.remove(tenantId); if (dataSource != null && dataSource instanceof HikariDataSource) { ((HikariDataSource) dataSource).close; } updateRoutingDataSource; tenantRepository.deleteById(tenantId); } private void updateRoutingDataSource { try { TenantAwareRoutingDataSource routingDataSource = (TenantAwareRoutingDataSource) dataSource; Field targetDataSourcesField = AbstractRoutingDataSource.class.getDeclaredField("targetDataSources"); targetDataSourcesField.setAccessible(true); Map targetDataSources = new HashMap(tenantDataSources); targetDataSourcesField.set(routingDataSource, targetDataSources); routingDataSource.afterPropertiesSet; } catch (Exception e) { throw new RuntimeException("Failed to update routing data source", e); } } }提供租户管理API :@RestController@RequestMapping("/admin/tenants")public class TenantAdminController { @Autowired private TenantManagementService tenantService; @GetMapping public List getAllTenants { return tenantService.getAllTenants; } @PostMapping public ResponseEntity createTenant(@RequestBody Tenant tenant) { tenantService.addTenant(tenant); return ResponseEntity.status(HttpStatus.CREATED).body(tenant); } @DeleteMapping("/{tenantId}") public ResponseEntity deleteTenant(@PathVariable String tenantId) { tenantService.removeTenant(tenantId); return ResponseEntity.noContent.build; }}优点:
数据隔离级别最高,安全性最佳租户可以使用不同的数据库版本或类型易于实现租户特定的数据库优化故障隔离,一个租户的数据库问题不影响其他租户便于独立备份、恢复和迁移缺点:
资源利用率较低,成本较高运维复杂度高,需要管理多个数据库实例跨租户查询困难每增加一个租户需要创建新的数据库实例数据库连接池管理复杂在这种模式下,所有租户共享同一个数据库实例,但每个租户拥有自己独立的 Schema(在 PostgreSQL 中)或数据库(在 MySQL 中)。这种方式在资源共享和数据隔离之间取得了平衡。
创建租户Schema配置 :@Configurationpublic class MultiTenantSchemaConfig { @Autowired private DataSource dataSource; @Autowired private TenantRepository tenantRepository; @PostConstruct public void initializeSchemas { for (Tenant tenant : tenantRepository.findByActive(true)) { createSchemaIfNotExists(tenant.getSchemaName); } } private void createSchemaIfNotExists(String schema) { try (Connection connection = dataSource.getConnection) { String sql = "CREATE SCHEMA IF NOT EXISTS " + schema; try (Statement stmt = connection.createStatement) { stmt.execute(sql); } } catch (SQLException e) { throw new RuntimeException("Failed to create schema: " + schema, e); } }}租户实体和存储 :@Entity@Table(name = "tenant")public class Tenant { @Id private String id; @Column(nullable = false) private String name; @Column(nullable = false, unique = true) private String schemaName; @Column private boolean active = true;}@Repository public interface TenantRepository extends JpaRepository { List findByActive(boolean active); Optional findBySchemaName(String schemaName); }配置Hibernate多租户支持 :@Configuration@EnableJpaRepositories(basePackages = "com.example.repository")@EntityScan(basePackages = "com.example.entity")public class JpaConfig { @Autowired private DataSource dataSource; @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory( EntityManagerFactoryBuilder builder) { Map properties = new HashMap; properties.put(org.hibernate.cfg.Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA); properties.put(org.hibernate.cfg.Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider); properties.put(org.hibernate.cfg.Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver); return builder .dataSource(dataSource) .packages("com.example.entity") .properties(properties) .build; } @Bean public MultiTenantConnectionProvider multiTenantConnectionProvider { return new SchemaBasedMultiTenantConnectionProvider; } @Bean public CurrentTenantIdentifierResolver currentTenantIdentifierResolver { return new TenantSchemaIdentifierResolver; }}实现多租户连接提供者 :public class SchemaBasedMultiTenantConnectionProvider implements MultiTenantConnectionProvider { private static final long serialVersionUID = 1L; @Autowired private DataSource dataSource; @Override public Connection getAnyConnection throws SQLException { return dataSource.getConnection; } @Override public void releaseAnyConnection(Connection connection) throws SQLException { connection.close; } @Override public Connection getConnection(String tenantIdentifier) throws SQLException { final Connection connection = getAnyConnection; try { connection.createStatement .execute(String.format("SET SCHEMA '%s'", tenantIdentifier)); } catch (SQLException e) { throw new HibernateException("Could not alter JDBC connection to schema [" + tenantIdentifier + "]", e); } return connection; } @Override public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException { try { connection.createStatement.execute("SET SCHEMA 'public'"); } catch (SQLException e) { } connection.close; } @Override public boolean supportsAggressiveRelease { return false; } @Override public boolean isUnwrappableAs(Class unwrapType) { return false; } @Override public T unwrap(Class unwrapType) { return null; }}实现租户标识解析器 :public class TenantSchemaIdentifierResolver implements CurrentTenantIdentifierResolver { private static final String DEFAULT_TENANT = "public"; @Override public String resolveCurrentTenantIdentifier { String tenantId = TenantContextHolder.getTenantId; return tenantId != null ? tenantId : DEFAULT_TENANT; } @Override public boolean validateExistingCurrentSessions { return true; }}动态租户管理服务 :@Servicepublic class TenantSchemaManagementService { @Autowired private TenantRepository tenantRepository; @Autowired private DataSource dataSource; @Autowired private EntityManagerFactory entityManagerFactory; public void createTenant(Tenant tenant) { createSchemaIfNotExists(tenant.getSchemaName); tenantRepository.save(tenant); initializeSchema(tenant.getSchemaName); } public void deleteTenant(String tenantId) { Tenant tenant = tenantRepository.findById(tenantId) .orElseThrow( -> new RuntimeException("Tenant not found: " + tenantId)); dropSchema(tenant.getSchemaName); tenantRepository.delete(tenant); } private void createSchemaIfNotExists(String schema) { try (Connection connection = dataSource.getConnection) { String sql = "CREATE SCHEMA IF NOT EXISTS " + schema; try (Statement stmt = connection.createStatement) { stmt.execute(sql); } } catch (SQLException e) { throw new RuntimeException("Failed to create schema: " + schema, e); } } private void dropSchema(String schema) { try (Connection connection = dataSource.getConnection) { String sql = "DROP SCHEMA IF EXISTS " + schema + " CASCADE"; try (Statement stmt = connection.createStatement) { stmt.execute(sql); } } catch (SQLException e) { throw new RuntimeException("Failed to drop schema: " + schema, e); } } private void initializeSchema(String schemaName) { String previousTenant = TenantContextHolder.getTenantId; try { TenantContextHolder.setTenantId(schemaName); Session session = entityManagerFactory.createEntityManager.unwrap(Session.class); session.doWork(connection -> { }); } finally { if (previousTenant != null) { TenantContextHolder.setTenantId(previousTenant); } else { TenantContextHolder.clear; } } }}租户管理API :@RestController@RequestMapping("/admin/tenants")public class TenantSchemaController { @Autowired private TenantSchemaManagementService tenantService; @Autowired private TenantRepository tenantRepository; @GetMapping public List getAllTenants { return tenantRepository.findAll; } @PostMapping public ResponseEntity createTenant(@RequestBody Tenant tenant) { tenantService.createTenant(tenant); return ResponseEntity.status(HttpStatus.CREATED).body(tenant); } @DeleteMapping("/{tenantId}") public ResponseEntity deleteTenant(@PathVariable String tenantId) { tenantService.deleteTenant(tenantId); return ResponseEntity.noContent.build; }}优点:
资源利用率高于独立数据库模式较好的数据隔离性运维复杂度低于独立数据库模式容易实现租户特定的表结构数据库级别的权限控制缺点:
数据库管理复杂度增加可能存在Schema数量限制跨租户查询仍然困难无法为不同租户使用不同的数据库类型所有租户共享数据库资源,可能出现资源争用适用场景在这种模式下,所有租户共享同一个数据库和 Schema,但每个租户有自己的表集合,通常通过表名前缀或后缀区分不同租户的表。
实现步骤实现多租户命名策略 :@Componentpublic class TenantTableNameStrategy extends PhysicalNamingStrategyStandardImpl { private static final long serialVersionUID = 1L; @Override public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) { String tenantId = TenantContextHolder.getTenantId; if (tenantId != null && !tenantId.isEmpty) { String tablePrefix = tenantId + "_"; return new Identifier(tablePrefix + name.getText, name.isQuoted); } return super.toPhysicalTableName(name, context); }}配置Hibernate命名策略 :@Configuration@EnableJpaRepositories(basePackages = "com.example.repository")@EntityScan(basePackages = "com.example.entity")public class JpaConfig { @Autowired private TenantTableNameStrategy tableNameStrategy; @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory( EntityManagerFactoryBuilder builder, DataSource dataSource) { Map properties = new HashMap; properties.put("hibernate.physical_naming_strategy", tableNameStrategy); return builder .dataSource(dataSource) .packages("com.example.entity") .properties(properties) .build; }}租户实体和仓库 :@Entity@Table(name = "tenant_info")public class Tenant { @Id private String id; @Column(nullable = false) private String name; @Column private boolean active = true;}@Repository public interface TenantRepository extends JpaRepository { List findByActive(boolean active); }表初始化管理器 :@Componentpublic class TenantTableManager { @Autowired private EntityManagerFactory entityManagerFactory; @Autowired private TenantRepository tenantRepository; @PersistenceContext private EntityManager entityManager; public void initializeTenantTables(String tenantId) { String previousTenant = TenantContextHolder.getTenantId; try { TenantContextHolder.setTenantId(tenantId); Session session = entityManager.unwrap(Session.class); session.doWork(connection -> { String createUserTable = "CREATE TABLE IF NOT EXISTS " + tenantId + "_users (" + "id BIGINT NOT NULL AUTO_INCREMENT, " + "username VARCHAR(255) NOT NULL, " + "email VARCHAR(255) NOT NULL, " + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " + "PRIMARY KEY (id)" + ")"; try (Statement stmt = connection.createStatement) { stmt.execute(createUserTable); } }); } finally { if (previousTenant != null) { TenantContextHolder.setTenantId(previousTenant); } else { TenantContextHolder.clear; } } } public void dropTenantTables(String tenantId) { try (Connection connection = entityManager.unwrap(SessionImplementor.class).connection) { DatabaseMetaData metaData = connection.getMetaData; String tablePrefix = tenantId + "_"; try (ResultSet tables = metaData.getTables( connection.getCatalog, connection.getSchema, tablePrefix + "%", new String{"TABLE"})) { List tablesToDrop = new ArrayList; while (tables.next) { tablesToDrop.add(tables.getString("TABLE_NAME")); } for (String tableName : tablesToDrop) { try (Statement stmt = connection.createStatement) { stmt.execute("DROP TABLE " + tableName); } } } } catch (SQLException e) { throw new RuntimeException("Failed to drop tenant tables", e); } }}租户管理服务 :@Servicepublic class TenantTableManagementService { @Autowired private TenantRepository tenantRepository; @Autowired private TenantTableManager tableManager; @PostConstruct public void initializeAllTenants { for (Tenant tenant : tenantRepository.findByActive(true)) { tableManager.initializeTenantTables(tenant.getId); } } @Transactional public void createTenant(Tenant tenant) { tenantRepository.save(tenant); tableManager.initializeTenantTables(tenant.getId); } @Transactional public void deleteTenant(String tenantId) { tableManager.dropTenantTables(tenantId); tenantRepository.deleteById(tenantId); }}提供租户管理API :@RestController@RequestMapping("/admin/tenants")public class TenantTableController { @Autowired private TenantTableManagementService tenantService; @Autowired private TenantRepository tenantRepository; @GetMapping public List getAllTenants { return tenantRepository.findAll; } @PostMapping public ResponseEntity createTenant(@RequestBody Tenant tenant) { tenantService.createTenant(tenant); return ResponseEntity.status(HttpStatus.CREATED).body(tenant); } @DeleteMapping("/{tenantId}") public ResponseEntity deleteTenant(@PathVariable String tenantId) { tenantService.deleteTenant(tenantId); return ResponseEntity.noContent.build; }}优点:
简单易实现,特别是对现有应用的改造资源利用率高跨租户查询相对容易实现维护成本低租户间表结构可以不同缺点:
数据隔离级别较低随着租户数量增加,表数量会急剧增长数据库对象(如表、索引)数量可能达到数据库限制备份和恢复单个租户数据较为复杂可能需要处理表名长度限制问题适用场景这是隔离级别最低但资源效率最高的方案。所有租户共享相同的数据库、Schema 和表,通过在每个表中添加"租户 ID"列来区分不同租户的数据。
实现步骤创建租户感知的实体基类 :@MappedSuperclass@EntityListeners(AuditingEntityListener.class)@Datapublic abstract class TenantAwareEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "tenant_id", nullable = false) private String tenantId; @CreatedDate @Column(name = "created_at", updatable = false) private LocalDateTime createdAt; @LastModifiedDate @Column(name = "updated_at") private LocalDateTime updatedAt; @PrePersist public void onPrePersist { tenantId = TenantContextHolder.getTenantId; }}租户实体和仓库 :@Entity@Table(name = "tenants")public class Tenant { @Id private String id; @Column(nullable = false) private String name; @Column private boolean active = true;}@Repository public interface TenantRepository extends JpaRepository { List findByActive(boolean active); }实现租户数据过滤器 :@Componentpublic class TenantFilterInterceptor implements HandlerInterceptor { @Autowired private EntityManager entityManager; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String tenantId = TenantContextHolder.getTenantId; if (tenantId != null) { Session session = entityManager.unwrap(Session.class); Filter filter = session.enableFilter("tenantFilter"); filter.setParameter("tenantId", tenantId); return true; } response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return false; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { Session session = entityManager.unwrap(Session.class); session.disableFilter("tenantFilter"); }}为实体添加过滤器注解 :@Entity@Table(name = "users")@FilterDef(name = "tenantFilter", parameters = { @ParamDef(name = "tenantId", type = "string")})@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")public class User extends TenantAwareEntity { @Column(name = "username", nullable = false) private String username; @Column(name = "email", nullable = false) private String email;}租户管理服务 :@Servicepublic class SharedTableTenantService { @Autowired private TenantRepository tenantRepository; @Autowired private EntityManager entityManager; @Transactional public void createTenant(Tenant tenant) { tenantRepository.save(tenant); initializeTenantData(tenant.getId); } @Transactional public void deleteTenant(String tenantId) { deleteAllTenantData(tenantId); tenantRepository.deleteById(tenantId); } private void initializeTenantData(String tenantId) { String previousTenant = TenantContextHolder.getTenantId; try { TenantContextHolder.setTenantId(tenantId); } finally { if (previousTenant != null) { TenantContextHolder.setTenantId(previousTenant); } else { TenantContextHolder.clear; } } } private void deleteAllTenantData(String tenantId) { List tables = getTablesWithTenantIdColumn; for (String table : tables) { entityManager.createNativeQuery("DELETE FROM " + table + " WHERE tenant_id = :tenantId") .setParameter("tenantId", tenantId) .executeUpdate; } } private List getTablesWithTenantIdColumn { List tables = new ArrayList; try (Connection connection = entityManager.unwrap(SessionImplementor.class).connection) { DatabaseMetaData metaData = connection.getMetaData; try (ResultSet rs = metaData.getTables( connection.getCatalog, connection.getSchema, "%", new String{"TABLE"})) { while (rs.next) { String tableName = rs.getString("TABLE_NAME"); try (ResultSet columns = metaData.getColumns( connection.getCatalog, connection.getSchema, tableName, "tenant_id")) { if (columns.next) { tables.add(tableName); } } } } } catch (SQLException e) { throw new RuntimeException("Failed to get tables with tenant_id column", e); } return tables; }}租户管理API :@RestController@RequestMapping("/admin/tenants")public class SharedTableTenantController { @Autowired private SharedTableTenantService tenantService; @Autowired private TenantRepository tenantRepository; @GetMapping public List getAllTenants { return tenantRepository.findAll; } @PostMapping public ResponseEntity createTenant(@RequestBody Tenant tenant) { tenantService.createTenant(tenant); return ResponseEntity.status(HttpStatus.CREATED).body(tenant); } @DeleteMapping("/{tenantId}") public ResponseEntity deleteTenant(@PathVariable String tenantId) { tenantService.deleteTenant(tenantId); return ResponseEntity.noContent.build; }}优点:
资源利用率最高维护成本最低实现简单,对现有单租户系统改造容易跨租户查询简单节省存储空间,特别是当数据量小时缺点:
数据隔离级别最低安全风险较高,一个错误可能导致跨租户数据泄露所有租户共享相同的表结构需要在所有数据访问层强制租户过滤适用场景混合租户模式结合了多种隔离策略,根据租户等级、重要性或特定需求为不同租户提供不同级别的隔离。例如,免费用户可能使用共享表模式,而付费企业用户可能使用独立数据库模式。
实现步骤租户类型和存储 :@Entity@Table(name = "tenants")public class Tenant { @Id private String id; @Column(nullable = false) private String name; @Enumerated(EnumType.STRING) @Column(nullable = false) private TenantType type; @Column private String databaseUrl; @Column private String username; @Column private String password; @Column private String driverClassName; @Column private String schemaName; @Column private boolean active = true; public enum TenantType { DEDICATED_DATABASE, DEDICATED_SCHEMA, DEDICATED_TABLE, SHARED_TABLE }}@Repository public interface TenantRepository extends JpaRepository { List findByActive(boolean active); List findByType(Tenant.TenantType type); }创建租户分类策略 :@Componentpublic class TenantIsolationStrategy { @Autowired private TenantRepository tenantRepository; private final Map tenantCache = new ConcurrentHashMap; @PostConstruct public void loadTenants { tenantRepository.findByActive(true).forEach(tenant ->tenantCache.put(tenant.getId, tenant)); } public Tenant.TenantType getIsolationTypeForTenant(String tenantId) { Tenant tenant = tenantCache.get(tenantId); if (tenant == null) { tenant = tenantRepository.findById(tenantId) .orElseThrow( -> new RuntimeException("Tenant not found: " + tenantId)); tenantCache.put(tenantId, tenant); } return tenant.getType; } public Tenant getTenant(String tenantId) { Tenant tenant = tenantCache.get(tenantId); if (tenant == null) { tenant = tenantRepository.findById(tenantId) .orElseThrow( -> new RuntimeException("Tenant not found: " + tenantId)); tenantCache.put(tenantId, tenant); } return tenant; } public void evictFromCache(String tenantId) { tenantCache.remove(tenantId); } }实现混合数据源路由 :@Componentpublic class HybridTenantRouter { @Autowired private TenantIsolationStrategy isolationStrategy; private final Map dedicatedDataSources = new ConcurrentHashMap; @Autowired private DataSource sharedDataSource; public DataSource getDataSourceForTenant(String tenantId) { Tenant.TenantType isolationType = isolationStrategy.getIsolationTypeForTenant(tenantId); if (isolationType == Tenant.TenantType.DEDICATED_DATABASE) { return dedicatedDataSources.computeIfAbsent(tenantId, this::createDedicatedDataSource); } return sharedDataSource; } private DataSource createDedicatedDataSource(String tenantId) { Tenant tenant = isolationStrategy.getTenant(tenantId); HikariDataSource dataSource = new HikariDataSource; dataSource.setJdbcUrl(tenant.getDatabaseUrl); dataSource.setUsername(tenant.getUsername); dataSource.setPassword(tenant.getPassword); dataSource.setDriverClassName(tenant.getDriverClassName); return dataSource; } public void removeDedicatedDataSource(String tenantId) { DataSource dataSource = dedicatedDataSources.remove(tenantId); if (dataSource instanceof HikariDataSource) { ((HikariDataSource) dataSource).close; } }}混合租户路由数据源 :public class HybridRoutingDataSource extends AbstractRoutingDataSource { @Autowired private HybridTenantRouter tenantRouter; @Autowired private TenantIsolationStrategy isolationStrategy; @Override protected Object determineCurrentLookupKey { String tenantId = TenantContextHolder.getTenantId; if (tenantId == null) { return "default"; } Tenant.TenantType isolationType = isolationStrategy.getIsolationTypeForTenant(tenantId); if (isolationType == Tenant.TenantType.DEDICATED_DATABASE) { return tenantId; } return "shared"; } @Override protected DataSource determineTargetDataSource { String tenantId = TenantContextHolder.getTenantId; if (tenantId == null) { return super.determineTargetDataSource; } return tenantRouter.getDataSourceForTenant(tenantId); }}混合租户拦截器 :@Componentpublic class HybridTenantInterceptor implements HandlerInterceptor { @Autowired private TenantIsolationStrategy isolationStrategy; @Autowired private EntityManager entityManager; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String tenantId = extractTenantId(request); if (tenantId != null) { TenantContextHolder.setTenantId(tenantId); Tenant.TenantType isolationType = isolationStrategy.getIsolationTypeForTenant(tenantId); switch (isolationType) { case DEDICATED_DATABASE: break; case DEDICATED_SCHEMA: setSchema(isolationStrategy.getTenant(tenantId).getSchemaName); break; case DEDICATED_TABLE: break; case SHARED_TABLE: enableTenantFilter(tenantId); break; } return true; } response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return false; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { String tenantId = TenantContextHolder.getTenantId; if (tenantId != null) { Tenant.TenantType isolationType = isolationStrategy.getIsolationTypeForTenant(tenantId); if (isolationType == Tenant.TenantType.SHARED_TABLE) { disableTenantFilter; } } TenantContextHolder.clear; } private void setSchema(String schema) { try { entityManager.createNativeQuery("SET SCHEMA '" + schema + "'").executeUpdate; } catch (Exception e) { } } private void enableTenantFilter(String tenantId) { Session session = entityManager.unwrap(Session.class); Filter filter = session.enableFilter("tenantFilter"); filter.setParameter("tenantId", tenantId); } private void disableTenantFilter { Session session = entityManager.unwrap(Session.class); session.disableFilter("tenantFilter"); } private String extractTenantId(HttpServletRequest request) { return request.getHeader("X-TenantID"); }}综合租户管理服务 :@Servicepublic class HybridTenantManagementService { @Autowired private TenantRepository tenantRepository; @Autowired private TenantIsolationStrategy isolationStrategy; @Autowired private HybridTenantRouter tenantRouter; @Autowired private EntityManager entityManager; @Autowired private DataSource dataSource; private final Map initializers = new HashMap; @PostConstruct public void init { initializers.put(Tenant.TenantType.DEDICATED_DATABASE, this::initializeDedicatedDatabase); initializers.put(Tenant.TenantType.DEDICATED_SCHEMA, this::initializeDedicatedSchema); initializers.put(Tenant.TenantType.DEDICATED_TABLE, this::initializeDedicatedTables); initializers.put(Tenant.TenantType.SHARED_TABLE, this::initializeSharedTables); } @Transactional public void createTenant(Tenant tenant) { tenantRepository.save(tenant); TenantInitializer initializer = initializers.get(tenant.getType); if (initializer != null) { initializer.initialize(tenant); } isolationStrategy.evictFromCache(tenant.getId); } @Transactional public void deleteTenant(String tenantId) { Tenant tenant = tenantRepository.findById(tenantId) .orElseThrow( -> new RuntimeException("Tenant not found: " + tenantId)); switch (tenant.getType) { case DEDICATED_DATABASE: cleanupDedicatedDatabase(tenant); break; case DEDICATED_SCHEMA: cleanupDedicatedSchema(tenant); break; case DEDICATED_TABLE: cleanupDedicatedTables(tenant); break; case SHARED_TABLE: cleanupSharedTables(tenant); break; } tenantRepository.delete(tenant); isolationStrategy.evictFromCache(tenantId); } private void initializeDedicatedDatabase(Tenant tenant) { DataSource dedicatedDs = tenantRouter.getDataSourceForTenant(tenant.getId); try (Connection conn = dedicatedDs.getConnection) { } catch (SQLException e) { throw new RuntimeException("Failed to initialize database for tenant: " + tenant.getId, e); } } private void initializeDedicatedSchema(Tenant tenant) { try (Connection conn = dataSource.getConnection) { try (Statement stmt = conn.createStatement) { stmt.execute("CREATE SCHEMA IF NOT EXISTS " + tenant.getSchemaName); } conn.setSchema(tenant.getSchemaName); } catch (SQLException e) { throw new RuntimeException("Failed to initialize schema for tenant: " + tenant.getId, e); } } private void initializeDedicatedTables(Tenant tenant) { String previousTenant = TenantContextHolder.getTenantId; try { TenantContextHolder.setTenantId(tenant.getId); } finally { if (previousTenant != null) { TenantContextHolder.setTenantId(previousTenant); } else { TenantContextHolder.clear; } } } private void initializeSharedTables(Tenant tenant) { String previousTenant = TenantContextHolder.getTenantId; try { TenantContextHolder.setTenantId(tenant.getId); } finally { if (previousTenant != null) { TenantContextHolder.setTenantId(previousTenant); } else { TenantContextHolder.clear; } } } private void cleanupDedicatedDatabase(Tenant tenant) { tenantRouter.removeDedicatedDataSource(tenant.getId); } private void cleanupDedicatedSchema(Tenant tenant) { try (Connection conn = dataSource.getConnection) { try (Statement stmt = conn.createStatement) { stmt.execute("DROP SCHEMA IF EXISTS " + tenant.getSchemaName + " CASCADE"); } } catch (SQLException e) { throw new RuntimeException("Failed to drop schema for tenant: " + tenant.getId, e); } } private void cleanupDedicatedTables(Tenant tenant) { try (Connection conn = dataSource.getConnection) { DatabaseMetaData metaData = conn.getMetaData; String tablePrefix = tenant.getId + "_"; try (ResultSet tables = metaData.getTables( conn.getCatalog, conn.getSchema, tablePrefix + "%", new String{"TABLE"})) { while (tables.next) { String tableName = tables.getString("TABLE_NAME"); try (Statement stmt = conn.createStatement) { stmt.execute("DROP TABLE " + tableName); } } } } catch (SQLException e) { throw new RuntimeException("Failed to drop tables for tenant: " + tenant.getId, e); } } private void cleanupSharedTables(Tenant tenant) { entityManager.createNativeQuery( "SELECT table_name FROM information_schema.columns " + "WHERE column_name = 'tenant_id'") .getResultList .forEach(tableName ->entityManager.createNativeQuery( "DELETE FROM " + tableName + " WHERE tenant_id = :tenantId") .setParameter("tenantId", tenant.getId) .executeUpdate ); } @FunctionalInterface private interface TenantInitializer { void initialize(Tenant tenant); } }提供租户管理API :@RestController@RequestMapping("/admin/tenants")public class HybridTenantController { @Autowired private HybridTenantManagementService tenantService; @Autowired private TenantRepository tenantRepository; @GetMapping public List getAllTenants { return tenantRepository.findAll; } @PostMapping public ResponseEntity createTenant(@RequestBody Tenant tenant) { tenantService.createTenant(tenant); return ResponseEntity.status(HttpStatus.CREATED).body(tenant); } @PutMapping("/{tenantId}") public ResponseEntity updateTenant( @PathVariable String tenantId, @RequestBody Tenant tenant) { tenant.setId(tenantId); tenantService.updateTenant(tenant); return ResponseEntity.ok(tenant); } @DeleteMapping("/{tenantId}") public ResponseEntity deleteTenant(@PathVariable String tenantId) { tenantService.deleteTenant(tenantId); return ResponseEntity.noContent.build; } @GetMapping("/types") public ResponseEntity> getTenantTypes { return ResponseEntity.ok(Arrays.asList(Tenant.TenantType.values)); }}优缺点分析优点:
最大的灵活性,可根据租户需求提供不同隔离级别可以实现资源和成本的平衡可以根据业务价值分配资源适应不同客户的安全和性能需求缺点:
实现复杂度最高维护和测试成本高需要处理多种数据访问模式可能引入不一致的用户体验错误处理更加复杂多租户架构是构建现代 SaaS 应用的关键技术,选择多租户模式需要平衡数据隔离、资源利用、成本和复杂度等多种因素。
通过深入理解这些架构模式及其权衡,可以根据实际情况选择适合的多租户架构,构建可扩展、安全且经济高效的企业级应用。
来源:墨码行者