当前位置: 首页 > article >正文

SpringBoot多租户系统的5种架构设计方案

多租户(Multi-tenancy)是一种软件架构模式,允许单个应用实例服务于多个客户(租户),同时保持租户数据的隔离性和安全性。

通过合理的多租户设计,企业可以显著降低运维成本、提升资源利用率,并实现更高效的服务交付。

本文将分享SpringBoot环境下实现多租户系统的5种架构设计方案

方案一:独立数据库模式

原理与特点

独立数据库模式为每个租户提供完全独立的数据库实例,是隔离级别最高的多租户方案。在这种模式下,租户数据完全分离,甚至可以部署在不同的服务器上。

实现步骤

  1. 创建多数据源配置:为每个租户配置独立的数据源
@Configuration
public class MultiTenantDatabaseConfig {@Autowiredprivate TenantDataSourceProperties properties;@Beanpublic DataSource dataSource() {AbstractRoutingDataSource multiTenantDataSource = new TenantAwareRoutingDataSource();Map<Object, Object> 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;}
}
  1. 实现租户感知的数据源路由
public class TenantAwareRoutingDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {return TenantContextHolder.getTenantId();}
}
  1. 租户上下文管理
public class TenantContextHolder {private static final ThreadLocal<String> 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();}
}
  1. 添加租户识别拦截器
@Component
public class TenantIdentificationInterceptor implements HandlerInterceptor {@Overridepublic 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;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {TenantContextHolder.clear();}private String extractTenantId(HttpServletRequest request) {// 从请求头中获取租户IDString tenantId = request.getHeader("X-TenantID");// 或者从子域名提取if (tenantId == null) {String host = request.getServerName();if (host.contains(".")) {tenantId = host.split("\.")[0];}}return tenantId;}
}
  1. 配置拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate TenantIdentificationInterceptor tenantInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(tenantInterceptor).addPathPatterns("/api/**");}
}
  1. 实现动态租户管理
@Entity
@Table(name = "tenant")
public class Tenant {@Idprivate 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;@Columnprivate boolean active = true;// getters and setters
}@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {List<Tenant> findByActive(boolean active);
}@Service
public class TenantManagementService {@Autowiredprivate TenantRepository tenantRepository;@Autowiredprivate DataSource dataSource;@Autowiredprivate ApplicationContext applicationContext;// 用ConcurrentHashMap存储租户数据源private final Map<String, DataSource> tenantDataSources = new ConcurrentHashMap<>();@PostConstructpublic void initializeTenants() {List<Tenant> 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;// 使用反射访问AbstractRoutingDataSource的targetDataSources字段Field targetDataSourcesField = AbstractRoutingDataSource.class.getDeclaredField("targetDataSources");targetDataSourcesField.setAccessible(true);Map<Object, Object> targetDataSources = new HashMap<>(tenantDataSources);targetDataSourcesField.set(routingDataSource, targetDataSources);// 调用afterPropertiesSet初始化数据源routingDataSource.afterPropertiesSet();} catch (Exception e) {throw new RuntimeException("Failed to update routing data source", e);}}
}
  1. 提供租户管理API
@RestController
@RequestMapping("/admin/tenants")
public class TenantAdminController {@Autowiredprivate TenantManagementService tenantService;@GetMappingpublic List<Tenant> getAllTenants() {return tenantService.getAllTenants();}@PostMappingpublic ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {tenantService.addTenant(tenant);return ResponseEntity.status(HttpStatus.CREATED).body(tenant);}@DeleteMapping("/{tenantId}")public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {tenantService.removeTenant(tenantId);return ResponseEntity.noContent().build();}
}

优缺点分析

优点:

  • 数据隔离级别最高,安全性最佳
  • 租户可以使用不同的数据库版本或类型
  • 易于实现租户特定的数据库优化
  • 故障隔离,一个租户的数据库问题不影响其他租户
  • 便于独立备份、恢复和迁移

缺点:

  • 资源利用率较低,成本较高
  • 运维复杂度高,需要管理多个数据库实例
  • 跨租户查询困难
  • 每增加一个租户需要创建新的数据库实例
  • 数据库连接池管理复杂

适用场景

  • 高要求的企业级SaaS应用
  • 租户数量相对较少但数据量大的场景
  • 租户愿意支付更高费用获得更好隔离性的场景

方案二:共享数据库,独立Schema模式

原理与特点

在这种模式下,所有租户共享同一个数据库实例,但每个租户拥有自己独立的Schema(在PostgreSQL中)或数据库(在MySQL中)。这种方式在资源共享和数据隔离之间取得了平衡。

实现步骤

  1. 创建租户Schema配置
@Configuration
public class MultiTenantSchemaConfig {@Autowiredprivate DataSource dataSource;@Autowiredprivate TenantRepository tenantRepository;@PostConstructpublic void initializeSchemas() {for (Tenant tenant : tenantRepository.findByActive(true)) {createSchemaIfNotExists(tenant.getSchemaName());}}private void createSchemaIfNotExists(String schema) {try (Connection connection = dataSource.getConnection()) {// PostgreSQL语法,MySQL使用CREATE DATABASE IF NOT EXISTSString 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);}}
}
  1. 租户实体和存储
@Entity
@Table(name = "tenant")
public class Tenant {@Idprivate String id;@Column(nullable = false)private String name;@Column(nullable = false, unique = true)private String schemaName;@Columnprivate boolean active = true;// getters and setters
}@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {List<Tenant> findByActive(boolean active);Optional<Tenant> findBySchemaName(String schemaName);
}
  1. 配置Hibernate多租户支持
@Configuration
@EnableJpaRepositories(basePackages = "com.example.repository")
@EntityScan(basePackages = "com.example.entity")
public class JpaConfig {@Autowiredprivate DataSource dataSource;@Beanpublic LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder) {Map<String, Object> 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());// 其他Hibernate配置...return builder.dataSource(dataSource).packages("com.example.entity").properties(properties).build();}@Beanpublic MultiTenantConnectionProvider multiTenantConnectionProvider() {return new SchemaBasedMultiTenantConnectionProvider();}@Beanpublic CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {return new TenantSchemaIdentifierResolver();}
}
  1. 实现多租户连接提供者
public class SchemaBasedMultiTenantConnectionProvider implements MultiTenantConnectionProvider {private static final long serialVersionUID = 1L;@Autowiredprivate DataSource dataSource;@Overridepublic Connection getAnyConnection() throws SQLException {return dataSource.getConnection();}@Overridepublic void releaseAnyConnection(Connection connection) throws SQLException {connection.close();}@Overridepublic Connection getConnection(String tenantIdentifier) throws SQLException {final Connection connection = getAnyConnection();try {// PostgreSQL语法,MySQL使用USE database_nameconnection.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;}@Overridepublic void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {try {// 恢复到默认Schemaconnection.createStatement().execute("SET SCHEMA 'public'");} catch (SQLException e) {// 忽略错误,确保连接关闭}connection.close();}@Overridepublic boolean supportsAggressiveRelease() {return false;}@Overridepublic boolean isUnwrappableAs(Class unwrapType) {return false;}@Overridepublic <T> T unwrap(Class<T> unwrapType) {return null;}
}
  1. 实现租户标识解析器
public class TenantSchemaIdentifierResolver implements CurrentTenantIdentifierResolver {private static final String DEFAULT_TENANT = "public";@Overridepublic String resolveCurrentTenantIdentifier() {String tenantId = TenantContextHolder.getTenantId();return tenantId != null ? tenantId : DEFAULT_TENANT;}@Overridepublic boolean validateExistingCurrentSessions() {return true;}
}
  1. 动态租户管理服务
@Service
public class TenantSchemaManagementService {@Autowiredprivate TenantRepository tenantRepository;@Autowiredprivate DataSource dataSource;@Autowiredprivate EntityManagerFactory entityManagerFactory;public void createTenant(Tenant tenant) {// 1. 创建SchemacreateSchemaIfNotExists(tenant.getSchemaName());// 2. 保存租户信息tenantRepository.save(tenant);// 3. 初始化Schema的表结构initializeSchema(tenant.getSchemaName());}public void deleteTenant(String tenantId) {Tenant tenant = tenantRepository.findById(tenantId).orElseThrow(() -> new RuntimeException("Tenant not found: " + tenantId));// 1. 删除SchemadropSchema(tenant.getSchemaName());// 2. 删除租户信息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);// 使用JPA/Hibernate工具初始化Schema// 可以使用SchemaExport或更推荐使用Flyway/LiquibaseSession session = entityManagerFactory.createEntityManager().unwrap(Session.class);session.doWork(connection -> {// 执行DDL语句});} finally {// 恢复之前的租户上下文if (previousTenant != null) {TenantContextHolder.setTenantId(previousTenant);} else {TenantContextHolder.clear();}}}
}
  1. 租户管理API
@RestController
@RequestMapping("/admin/tenants")
public class TenantSchemaController {@Autowiredprivate TenantSchemaManagementService tenantService;@Autowiredprivate TenantRepository tenantRepository;@GetMappingpublic List<Tenant> getAllTenants() {return tenantRepository.findAll();}@PostMappingpublic ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {tenantService.createTenant(tenant);return ResponseEntity.status(HttpStatus.CREATED).body(tenant);}@DeleteMapping("/{tenantId}")public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {tenantService.deleteTenant(tenantId);return ResponseEntity.noContent().build();}
}

优缺点分析

优点:

  • 资源利用率高于独立数据库模式
  • 较好的数据隔离性
  • 运维复杂度低于独立数据库模式
  • 容易实现租户特定的表结构
  • 数据库级别的权限控制

缺点:

  • 数据库管理复杂度增加
  • 可能存在Schema数量限制
  • 跨租户查询仍然困难
  • 无法为不同租户使用不同的数据库类型
  • 所有租户共享数据库资源,可能出现资源争用

适用场景

  • 中型SaaS应用
  • 租户数量中等但增长较快的场景
  • 需要较好数据隔离但成本敏感的应用
  • PostgreSQL或MySQL等支持Schema/数据库隔离的数据库环境

方案三:共享数据库,共享Schema,独立表模式

原理与特点

在这种模式下,所有租户共享同一个数据库和Schema,但每个租户有自己的表集合,通常通过表名前缀或后缀区分不同租户的表。

实现步骤

  1. 实现多租户命名策略
@Component
public class TenantTableNameStrategy extends PhysicalNamingStrategyStandardImpl {private static final long serialVersionUID = 1L;@Overridepublic 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);}
}
  1. 配置Hibernate命名策略
@Configuration
@EnableJpaRepositories(basePackages = "com.example.repository")
@EntityScan(basePackages = "com.example.entity")
public class JpaConfig {@Autowiredprivate TenantTableNameStrategy tableNameStrategy;@Beanpublic LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder,DataSource dataSource) {Map<String, Object> properties = new HashMap<>();properties.put("hibernate.physical_naming_strategy", tableNameStrategy);// 其他Hibernate配置...return builder.dataSource(dataSource).packages("com.example.entity").properties(properties).build();}
}
  1. 租户实体和仓库
@Entity
@Table(name = "tenant_info") // 避免与租户表前缀冲突
public class Tenant {@Idprivate String id;@Column(nullable = false)private String name;@Columnprivate boolean active = true;// getters and setters
}@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {List<Tenant> findByActive(boolean active);
}
  1. 表初始化管理器
@Component
public class TenantTableManager {@Autowiredprivate EntityManagerFactory entityManagerFactory;@Autowiredprivate TenantRepository tenantRepository;@PersistenceContextprivate EntityManager entityManager;public void initializeTenantTables(String tenantId) {String previousTenant = TenantContextHolder.getTenantId();try {TenantContextHolder.setTenantId(tenantId);// 使用JPA/Hibernate初始化表结构// 在生产环境中,推荐使用Flyway或Liquibase进行更精细的控制Session session = entityManager.unwrap(Session.class);session.doWork(connection -> {// 执行建表语句// 这里可以使用Hibernate的SchemaExport,但为简化,直接使用SQL// 示例:创建用户表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<String> 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);}}
}
  1. 租户管理服务
@Service
public class TenantTableManagementService {@Autowiredprivate TenantRepository tenantRepository;@Autowiredprivate TenantTableManager tableManager;@PostConstructpublic void initializeAllTenants() {for (Tenant tenant : tenantRepository.findByActive(true)) {tableManager.initializeTenantTables(tenant.getId());}}@Transactionalpublic void createTenant(Tenant tenant) {// 1. 保存租户信息tenantRepository.save(tenant);// 2. 初始化租户表tableManager.initializeTenantTables(tenant.getId());}@Transactionalpublic void deleteTenant(String tenantId) {// 1. 删除租户表tableManager.dropTenantTables(tenantId);// 2. 删除租户信息tenantRepository.deleteById(tenantId);}
}
  1. 提供租户管理API
@RestController
@RequestMapping("/admin/tenants")
public class TenantTableController {@Autowiredprivate TenantTableManagementService tenantService;@Autowiredprivate TenantRepository tenantRepository;@GetMappingpublic List<Tenant> getAllTenants() {return tenantRepository.findAll();}@PostMappingpublic ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {tenantService.createTenant(tenant);return ResponseEntity.status(HttpStatus.CREATED).body(tenant);}@DeleteMapping("/{tenantId}")public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {tenantService.deleteTenant(tenantId);return ResponseEntity.noContent().build();}
}

优缺点分析

优点:

  • 简单易实现,特别是对现有应用的改造
  • 资源利用率高
  • 跨租户查询相对容易实现
  • 维护成本低
  • 租户间表结构可以不同

缺点:

  • 数据隔离级别较低
  • 随着租户数量增加,表数量会急剧增长
  • 数据库对象(如表、索引)数量可能达到数据库限制
  • 备份和恢复单个租户数据较为复杂
  • 可能需要处理表名长度限制问题

适用场景

  • 租户数量适中且表结构相对简单的SaaS应用
  • 需要为不同租户提供不同表结构的场景
  • 快速原型开发或MVP(最小可行产品)
  • 从单租户向多租户过渡的系统

方案四:共享数据库,共享Schema,共享表模式

原理与特点

这是隔离级别最低但资源效率最高的方案。所有租户共享相同的数据库、Schema和表,通过在每个表中添加"租户ID"列来区分不同租户的数据。

实现步骤

  1. 创建租户感知的实体基类
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Data
public 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;@PrePersistpublic void onPrePersist() {tenantId = TenantContextHolder.getTenantId();}
}
  1. 租户实体和仓库
@Entity
@Table(name = "tenants")
public class Tenant {@Idprivate String id;@Column(nullable = false)private String name;@Columnprivate boolean active = true;// getters and setters
}@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {List<Tenant> findByActive(boolean active);
}
  1. 实现租户数据过滤器
@Component
public class TenantFilterInterceptor implements HandlerInterceptor {@Autowiredprivate EntityManager entityManager;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String tenantId = TenantContextHolder.getTenantId();if (tenantId != null) {// 设置Hibernate过滤器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;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {Session session = entityManager.unwrap(Session.class);session.disableFilter("tenantFilter");}
}
  1. 为实体添加过滤器注解
@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;// 其他字段和方法...
}
  1. 租户管理服务
@Service
public class SharedTableTenantService {@Autowiredprivate TenantRepository tenantRepository;@Autowiredprivate EntityManager entityManager;@Transactionalpublic void createTenant(Tenant tenant) {// 直接保存租户信息tenantRepository.save(tenant);// 初始化租户默认数据initializeTenantData(tenant.getId());}@Transactionalpublic 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) {// 获取所有带有tenant_id列的表List<String> tables = getTablesWithTenantIdColumn();// 从每个表中删除该租户的数据for (String table : tables) {entityManager.createNativeQuery("DELETE FROM " + table + " WHERE tenant_id = :tenantId").setParameter("tenantId", tenantId).executeUpdate();}}private List<String> getTablesWithTenantIdColumn() {List<String> 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");// 检查表是否有tenant_id列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;}
}
  1. 租户管理API
@RestController
@RequestMapping("/admin/tenants")
public class SharedTableTenantController {@Autowiredprivate SharedTableTenantService tenantService;@Autowiredprivate TenantRepository tenantRepository;@GetMappingpublic List<Tenant> getAllTenants() {return tenantRepository.findAll();}@PostMappingpublic ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {tenantService.createTenant(tenant);return ResponseEntity.status(HttpStatus.CREATED).body(tenant);}@DeleteMapping("/{tenantId}")public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {tenantService.deleteTenant(tenantId);return ResponseEntity.noContent().build();}
}

优缺点分析

优点:

  • 资源利用率最高
  • 维护成本最低
  • 实现简单,对现有单租户系统改造容易
  • 跨租户查询简单
  • 节省存储空间,特别是当数据量小时

缺点:

  • 数据隔离级别最低
  • 安全风险较高,一个错误可能导致跨租户数据泄露
  • 所有租户共享相同的表结构
  • 需要在所有数据访问层强制租户过滤

适用场景

  • 租户数量多但每个租户数据量小的场景
  • 成本敏感的应用
  • 原型验证或MVP阶段

方案五:混合租户模式

原理与特点

混合租户模式结合了多种隔离策略,根据租户等级、重要性或特定需求为不同租户提供不同级别的隔离。例如,免费用户可能使用共享表模式,而付费企业用户可能使用独立数据库模式。

实现步骤

  1. 租户类型和存储
@Entity
@Table(name = "tenants")
public class Tenant {@Idprivate String id;@Column(nullable = false)private String name;@Enumerated(EnumType.STRING)@Column(nullable = false)private TenantType type;@Columnprivate String databaseUrl;@Columnprivate String username;@Columnprivate String password;@Columnprivate String driverClassName;@Columnprivate String schemaName;@Columnprivate boolean active = true;public enum TenantType {DEDICATED_DATABASE,DEDICATED_SCHEMA,DEDICATED_TABLE,SHARED_TABLE}// getters and setters
}@Repository
public interface TenantRepository extends JpaRepository<Tenant, String> {List<Tenant> findByActive(boolean active);List<Tenant> findByType(Tenant.TenantType type);
}
  1. 创建租户分类策略
@Component
public class TenantIsolationStrategy {@Autowiredprivate TenantRepository tenantRepository;private final Map<String, Tenant> tenantCache = new ConcurrentHashMap<>();@PostConstructpublic 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);}
}
  1. 实现混合数据源路由
@Component
public class HybridTenantRouter {@Autowiredprivate TenantIsolationStrategy isolationStrategy;private final Map<String, DataSource> dedicatedDataSources = new ConcurrentHashMap<>();@Autowiredprivate 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();}}
}
  1. 混合租户路由数据源
public class HybridRoutingDataSource extends AbstractRoutingDataSource {@Autowiredprivate HybridTenantRouter tenantRouter;@Autowiredprivate TenantIsolationStrategy isolationStrategy;@Overrideprotected 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";}@Overrideprotected DataSource determineTargetDataSource() {String tenantId = TenantContextHolder.getTenantId();if (tenantId == null) {return super.determineTargetDataSource();}return tenantRouter.getDataSourceForTenant(tenantId);}
}
  1. 混合租户拦截器
@Component
public class HybridTenantInterceptor implements HandlerInterceptor {@Autowiredprivate TenantIsolationStrategy isolationStrategy;@Autowiredprivate EntityManager entityManager;@Overridepublic 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;}@Overridepublic 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) {// 从请求中提取租户ID的逻辑return request.getHeader("X-TenantID");}
}
  1. 综合租户管理服务
@Service
public class HybridTenantManagementService {@Autowiredprivate TenantRepository tenantRepository;@Autowiredprivate TenantIsolationStrategy isolationStrategy;@Autowiredprivate HybridTenantRouter tenantRouter;@Autowiredprivate EntityManager entityManager;@Autowiredprivate DataSource dataSource;// 不同隔离类型的初始化策略private final Map<Tenant.TenantType, TenantInitializer> initializers = new HashMap<>();@PostConstructpublic 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);}@Transactionalpublic void createTenant(Tenant tenant) {// 1. 保存租户基本信息tenantRepository.save(tenant);// 2. 根据隔离类型初始化TenantInitializer initializer = initializers.get(tenant.getType());if (initializer != null) {initializer.initialize(tenant);}// 3. 更新缓存isolationStrategy.evictFromCache(tenant.getId());}@Transactionalpublic void deleteTenant(String tenantId) {Tenant tenant = tenantRepository.findById(tenantId).orElseThrow(() -> new RuntimeException("Tenant not found: " + tenantId));// 1. 根据隔离类型清理资源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;}// 2. 删除租户信息tenantRepository.delete(tenant);// 3. 更新缓存isolationStrategy.evictFromCache(tenantId);}// 独立数据库初始化private void initializeDedicatedDatabase(Tenant tenant) {// 创建数据源DataSource dedicatedDs = tenantRouter.getDataSourceForTenant(tenant.getId());// 初始化数据库结构try (Connection conn = dedicatedDs.getConnection()) {// 执行DDL脚本// ...} catch (SQLException e) {throw new RuntimeException("Failed to initialize database for tenant: " + tenant.getId(), e);}}// Schema初始化private void initializeDedicatedSchema(Tenant tenant) {try (Connection conn = dataSource.getConnection()) {// 创建Schematry (Statement stmt = conn.createStatement()) {stmt.execute("CREATE SCHEMA IF NOT EXISTS " + tenant.getSchemaName());}// 切换到该Schemaconn.setSchema(tenant.getSchemaName());// 创建表结构// ...} catch (SQLException e) {throw new RuntimeException("Failed to initialize schema for tenant: " + tenant.getId(), e);}}// 独立表初始化private void initializeDedicatedTables(Tenant tenant) {// 设置线程上下文中的租户ID以使用正确的表名前缀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());// 注意:通常不会自动删除实际的数据库,这需要DBA手动操作}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) {// 从所有带有tenant_id列的表中删除该租户的数据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());}// 租户初始化策略接口@FunctionalInterfaceprivate interface TenantInitializer {void initialize(Tenant tenant);}
}
  1. 提供租户管理API
@RestController
@RequestMapping("/admin/tenants")
public class HybridTenantController {@Autowiredprivate HybridTenantManagementService tenantService;@Autowiredprivate TenantRepository tenantRepository;@GetMappingpublic List<Tenant> getAllTenants() {return tenantRepository.findAll();}@PostMappingpublic ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {tenantService.createTenant(tenant);return ResponseEntity.status(HttpStatus.CREATED).body(tenant);}@PutMapping("/{tenantId}")public ResponseEntity<Tenant> updateTenant(@PathVariable String tenantId, @RequestBody Tenant tenant) {tenant.setId(tenantId);tenantService.updateTenant(tenant);return ResponseEntity.ok(tenant);}@DeleteMapping("/{tenantId}")public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) {tenantService.deleteTenant(tenantId);return ResponseEntity.noContent().build();}@GetMapping("/types")public ResponseEntity<List<Tenant.TenantType>> getTenantTypes() {return ResponseEntity.ok(Arrays.asList(Tenant.TenantType.values()));}
}

优缺点分析

优点:

  • 最大的灵活性,可根据租户需求提供不同隔离级别
  • 可以实现资源和成本的平衡
  • 可以根据业务价值分配资源
  • 适应不同客户的安全和性能需求

缺点:

  • 实现复杂度最高
  • 维护和测试成本高
  • 需要处理多种数据访问模式
  • 可能引入不一致的用户体验
  • 错误处理更加复杂

适用场景

  • 需要提供灵活定价模型的应用
  • 资源需求差异大的租户集合

方案对比

隔离模式数据隔离级别资源利用率成本复杂度适用场景
独立数据库最高企业级应用、金融/医疗行业
独立Schema中型SaaS、安全要求较高的场景
独立表中高中低中小型应用、原型验证
共享表最高大量小租户、成本敏感场景
混合模式可变可变中高多层级服务、复杂业务需求

总结

多租户架构是构建现代SaaS应用的关键技术,选择多租户模式需要平衡数据隔离、资源利用、成本和复杂度等多种因素。

通过深入理解这些架构模式及其权衡,可以根据实际情况选择适合的多租户架构,构建可扩展、安全且经济高效的企业级应用。

http://www.lryc.cn/news/2395338.html

相关文章:

  • 数据分析实战1(Excel制作报表)
  • 本地部署大模型llm+RAG向量检索问答系统 deepseek chatgpt
  • 设备健康管理的战略升维:用预测性维护重构企业竞争力
  • Redis事务详解:原理、使用与注意事项
  • 提升 GitHub Stats 的 6 个关键策略
  • CSS Animation 详解
  • LabVIEW 中内存释放相关问题
  • 【HarmonyOS 5】鸿蒙中的UIAbility详解(三)
  • 基于内存高效算法的 LLM Token 优化:一个有效降低 API 成本的技术方案
  • vue-11(命名路由和命名视图)
  • (附代码)自定义 LangChain 文档分割器,深入探索 LangChain 文档分割策略与应用
  • Python打卡训练营Day42
  • 基于微信小程序的scratch学习系统
  • MATLAB实战:机器学习分类回归示例
  • 动态库导出符号与extern “C“
  • 小知识:STM32 printf 重定向(串口输出)--让数据 “开口说话” 的关键技巧
  • `docker commit` 和 `docker save`区别
  • 【C++ 多态】—— 礼器九鼎,釉下乾坤,多态中的 “风水寻龙诀“
  • SCSAI平台面向对象建模技术的设计与实现
  • pikachu通关教程-CSRF
  • 智能体觉醒:AI开始自己“动手”了-自主进化开启任务革命时代
  • Python爬虫实战:研究Aiohttp库相关技术
  • 【C++指南】C++ list容器完全解读(二):list模拟实现,底层架构揭秘
  • [神经网络]使用olivettiface数据集进行训练并优化,观察对比loss结果
  • 小明的Java面试奇遇之智能家装平台架构设计与JVM调优实战
  • n8n:技术团队的智能工作流自动化助手
  • Flink 核心机制与源码剖析系列
  • 华院计算出席信创论坛,分享AI教育创新实践并与燧原科技共同推出教育一体机
  • 华为OD机试真题——会议接待 /代表团坐车(2025A卷:200分)Java/python/JavaScript/C++/C语言/GO六种最佳实现
  • LabVIEW Val (Sgnl) 属性