开发者

SpringBoot实现多租户系统架构的5种设计方案介绍

目录
  • 方案一:独立数据库模式
    • 原理与特点
    • 实现步骤
    • 优缺点分析
    • 适用场景
  • 方案二:共享数据库,独立Schema模式
    • 原理与特点
    • 实现步骤
    • 优缺点分析
    • 适用场景
  • 方案三:共享数据库,共享Schema,独立表模式
    • 原理与特点
    • 实现步骤
    • 优缺点分析
    • 适用场景
  • 方案四:共享数据库,共享Schema,共享表模式
    • 原理与特点
    • 实现步骤
    • 优缺点分析
    • 适用场景
  • 方案五:混合租户模式
    • 原理与特点
    • 实现步骤
    • 优缺点分析
    • 适用场景
  • 方案对比
    • 总结

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

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

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

      方案一:独立数据库模式

      原理与特点

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

      实现步骤

      1. 创建多数据源配置:为每个租户配置独立的数据源

      @Configuration
      public class MultiTenantDatabaseConfig {
          
          @Autowired
          private TenantDataSourceProperties properties;
          
          @Bean
          public 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;
          }
      }

      2. 实现租户感知的数据源路由

      public class TenantAwareRoutingDataSource extends AbstractRoutingDataSource {
          
          @Override
          protected Object determineCurrentLookupKey() {
              return TenantContextHolder.getTenantId();
          }
      }

      3. 租户上下文管理

      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();
          }
      }

      4. 添加租户识别拦截器

      @Component
      public 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) {
              // 从请求头中获取租户ID
              String tenantId = request.getHeader("X-TenantID");
              
              // 或者从子域名提取
              if (tenantId == null) {
                  String host = request.getServerName();
                  if (host.contains(".")) {
                      tenantId = host.split("\.")[0];
                  }
              }
              
              return tenantId;
          }
      }

      5. 配置拦截器

      @Configuration
      public class WebConfig implements WebMvcConfigurer {
          
          @Autowired
          private TenantIdentificationInterceptor tenantInterceptor;
          
          @Override
          public void addInterceptors(InterceptorRegistry registry) {
              registry.addInterceptor(tenantInterceptor)
                      .addPathPatterns("/api/**");
          }
      }

      6. 实现动态租户管理

      @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;
          
          // getters and setters
      }
      
      @Repository
      public interface TenantRepository extends JpaRepository<Tenant, String> {
          List<Tenant> findByActive(boolean active);
      }
      
      @Service
      public class TenantManagementService {
          
          @Autowired
          private TenantRepository tenantRepository;
          
          @Autowired
          private DataSource dataSource;
          
          @Autowired
          private ApplicationContext applicationContext;
          
          // 用ConcurrentHashMap存储租户数据源
          private final Map<String, DataSource> tenantDataSources = new ConcurrentHashMap<>();
          
          @PostConstruct
          public 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);
              }
          }
      }

      7. 提供租户管理API

      @RestController
      @RequestMapping("/admin/tenants")
      public class TenantAdminController {
          
          @Autowired
          private TenantManagementService tenantService;
          
          @GetMapping
          public List<Tenant> getAllTenants() {
              return tenantService.getAllTenants();
          }
          
          @PostMapping
          public ResponseEntity<Tenant> createTenant(@RequestBody Tenant tenant) {
              tenantService.addTenant(tenant);
              return ResponseEntity.status(HttpStatus.CREATED).body(tenant);
          }
          
          @DeleteMapping("/{jstenantId}")
          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 {
          
          @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()) {
                  // PostgreSQL语法,MySQL使用CREATE DATABASE IF NOT EXISTS
                  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);
              }
          }
      }

      2. 租户实体和存储

      @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;
          
          // getters and setters
      }
      
      @Repository
      public interface TenantRepository extends JpaRepository<Tenant, String> {
          List<Tenant> findByActive(boolean active);
          Optional<Tenant> findBySchemaName(String schemaName);
      }

      3. 配置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<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();
          }
          
          @Bean
          public MultiTenantConnectionProvider multiTenantConnectionProvider() {
              return new SchemaBasedMultiTenantConnectionProvider();
          }
          
          @Bean
          public CurrentTenantIdentifierResolver currentTenantIdentifierResolver() {
              return new TenantSchemaIdentifierResolver();
          }
      }

      4. 实现多租户连接提供者

      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 {
                  // PostgreSQL语法,MySQL使用USE database_name
                  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 {
                  // 恢复到默认Schema
                  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> T unwrap(Class<T> unwrapType) {
              return null;
          }
      }

      5. 实现租户标识解析器

      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;
          }
      }

      6. 动态租户管理服务

      @Service
      public class TenantSchemaManagementService {
          
          @Autowired
          private TenantRepository tenantRepository;
          
          @Autowired
          private DataSource dataSource;
          
          @Autowired
          private EntityManagerFactory entityManagerFactory;
          
          public void createTenant(Tenant tenant) {
              // 1. 创建Schema
              createSchemaIfNotExists(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. 删除Schema
              dropSchema(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/Liquibase
                  Session session = entityManagerFactory.createEntityManager().unwrap(Session.class);
                  session.doWork(connection -> {
                      // 执行DDL语句
                  });
                  
              } finally {
                  // 恢复之前的租户上下文
                  if (previousTenant != null) {
                      TenantContextHolder.setTenantId(previousTenant);
                  } else {
                      TenantContextHolder.clear();
                  }
              }
          }
      }

      7. 租户管理API

      @RestController
      @RequestMapping("/admin/tenants")
      public class TenantSchemaController {
          
          @Autowired
          private TenantSchemaManagementService tenantService;
          
          @Autowired
          private TenantRepository tenantRepository;
          
          @GetMapping
          public List<Tenant> getAllTenants() {
              return tenantRepository.findAll();
          }
          
          @PostMapping
          public 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;
          
          @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);
          }
      }

      2. 配置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<String, Object> properties = new HashMap<>();
              properties.put("hibernate.physical_naming_strategy", 
                      tableNameStrategy);
              
              // 其他Hibernate配置...
              
              return builder
                      .dataSource(dataSource)
                      .packages("com.example.entity")
                      .properties(properties)
                      .build();
          }
      }

      3. 租户实体和仓库

      @Entity
      @Table(name = "tenant_info") // 避免与租户表前缀冲突
      public class Tenant {
          
          @Id
          private String id;
          
          @Column(nullable = false)
          private String name;
          
          @Column
          private boolean active = true;
          
          // getters and setters
      }
      
      @Repository
      public interface TenantRepository extends JpaRepository<Tenant, String> {
          List<Tenant> findByActive(boolean active);
      }

      4. 表初始化管理器

      @Component
      public class Tenanthttp://www.devze.comTableManager {
          
          @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);
                  
                  // 使用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);
              }
          }
      }

      5. 租户管理服务

      @Service
      public 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) {
              // 1. 保存租户信息
              tenantRepository.save(tenant);
              
              // 2. 初始化租户表
              tableManager.initializeTenantTables(tenant.getId());
          }
          
          @Transactional
          public void deleteTenant(String tenantId) {
              // 1. 删除租户表
              tableManager.dropTenantTables(tenantId);
              
              // 2. 删除租户信息
              tenantRepository.deleteById(tenantId);
          }
      }

      6. 提供租户管理API

      @RestController
      @RequestMapping("/admin/tenants")
      public class TenantTableController {
          
          @Autowired
          private TenantTableManagementService tenantService;
          
          @Autowired
          private TenantRepository tenantRepository;
          
          @GetMapping
          public List<Tenant> getAllTenants() {
              return tenantRepository.findAll();
          }
          
          @PostMapping
          public 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和表,通过在每个表中添加python"租户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;
          
          @PrePersist
          public void onPrePersist() {
              tenantId = TenantContextHolder.getTenantId();
          }
      }

      2. 租户实体和仓库

      @Entity
      @Table(name = "tenants")
      public class Tenant {
          
          @Id
          private String id;
          
          @Column(nullable = false)
          private String name;
          
          @Column
          private boolean active = true;
          
          // getters and setters
      }
      
      @Repository
      public interface TenantRepository extends JpaRepository<Tenant, String> {
          List<Tenant> findByActive(boolean active);
      }

      3. 实现租户数据过滤器

      @Component
      public 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) {
                  // 设置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;
          }
          
          @Override
          public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                                     Object handler, Exception ex) {
              Session session = entityManager.unwrap(Session.class);
              session.disableFilter("tenantFilter");
          }
      }

      4. 为实体添加过滤器注解

      @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;
          
          // 其他字段和方法...
      }

      5. 租户管理服务

      @Service
      public 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 voiandroidd 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;
          }
      }

      6. 租户管理API

      @RestController
      @RequestMapping("/admin/tenants")
      public class SharedTableTenantController {
          
          @Autowired
          private SharedTableTenantService tenantService;
          
          @Autowired
          private TenantRepository tenantRepository;
          
          @GetMapping
          public List<Tenant> getAllTenants() {
              return tenantRepository.findAll();
          }
          
          @PostMapping
          public 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 {
          
          @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
          }
          
          // getters and setters
      }
      
      @Repository
      public interface TenantRepository extends JpaRepository<Tenant, String> {
          List<Tenant> findByActive(boolean active);
          List<Tenant> findByType(Tenant.TenantType type);
      }

      2. 创建租户分类策略

      @Component
      public class TenantIsolationStrategy {
          
          @Autowired
          private TenantRepository tenantRepository;
          
          private final Map<String, Tenant> 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);
          }
      }

      3. 实现混合数据源路由

      @Component
      public class HybridTenantRouter {
          
          @Autowired
          private TenantIsolationStrategy isolationStrategy;
          
          private final Map<String, DataSource> 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();
              }
          }
      }

      4. 混合租户路由数据源

      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);
          }
      }

      5. 混合租户拦截器

      @Component
      public 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) {
              // 从请求中提取租户ID的逻辑
              return request.getHeader("X-TenantID");
          }
      }

      6. 综合租户管理服务

      @Service
      public 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<Tenant.TenantType, TenantInitializer> 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) {
              // 1. 保存租户基本信息
              tenantRepository.save(tenant);
              
              // 2. 根据隔离类型初始化
              TenantInitializer initializer = initializers.get(tenant.getType());
              if (initializer != null) {
                  initializer.initialize(tenant);
              }
              
              // 3. 更新缓存
              isolationStrategy.evictFromCache(tenant.getId());
          }
          
          @Transactional
          public 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()) {
                  // 创建Schema
                  try (Statement stmt = conn.createStatement()) {
                      stmt.execute("CREATE SCHEMA IF NOT EXISTS " + tenant.getSchemaName());
                  }
                  
                  // 切换到该Schema
                  conn.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()
                      );
          }
          
          // 租户初始化策略接口
          @FunctionalInterface
          private interface TenantInitializer {
              void initialize(Tenant tenant);
          }
      }

      7. 提供租户管理API

      @RestController
      @RequestMapping("/admin/tenants")
      public class HybridTenantController {
          
          @Autowired
          private HybridTenantManagementService tenantService;
          
          @Autowired
          private TenantRepository tenantRepository;
          
          @GetMapping
          public List<Tenant> getAllTenants() {
              return tenantRepository.findAll();
          }
          
          @PostMapping
          public 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应用的关键技术,选择多租户模式需要平衡数据隔离、资源利用、成本和复杂度等多种因素。

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

      以上就是SpringBoot实现多租户系统架构的5种设计方案介绍的详细内容,更多关于SpringBoot多租户架构的资料请关注编程客栈(www.devze.com)其它相关文章!

      0

      上一篇:

      下一篇:

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      最新开发

      开发排行榜