SpringBoot實(shí)現(xiàn)多租戶系統(tǒng)架構(gòu)的5種設(shè)計(jì)方案介紹
多租戶(Multi-tenancy)是一種軟件架構(gòu)模式,允許單個(gè)應(yīng)用實(shí)例服務(wù)于多個(gè)客戶(租戶),同時(shí)保持租戶數(shù)據(jù)的隔離性和安全性。
通過合理的多租戶設(shè)計(jì),企業(yè)可以顯著降低運(yùn)維成本、提升資源利用率,并實(shí)現(xiàn)更高效的服務(wù)交付。
本文將分享SpringBoot環(huán)境下實(shí)現(xiàn)多租戶系統(tǒng)的5種架構(gòu)設(shè)計(jì)方案
方案一:獨(dú)立數(shù)據(jù)庫模式
原理與特點(diǎn)
獨(dú)立數(shù)據(jù)庫模式為每個(gè)租戶提供完全獨(dú)立的數(shù)據(jù)庫實(shí)例,是隔離級(jí)別最高的多租戶方案。在這種模式下,租戶數(shù)據(jù)完全分離,甚至可以部署在不同的服務(wù)器上。
實(shí)現(xiàn)步驟
1. 創(chuàng)建多數(shù)據(jù)源配置:為每個(gè)租戶配置獨(dú)立的數(shù)據(jù)源
@Configuration public class MultiTenantDatabaseConfig { @Autowired private TenantDataSourceProperties properties; @Bean public DataSource dataSource() { AbstractRoutingDataSource multiTenantDataSource = new TenantAwareRoutingDataSource(); Map<Object, Object> targetDataSources = new HashMap<>(); // 為每個(gè)租戶創(chuàng)建數(shù)據(jù)源 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. 實(shí)現(xiàn)租戶感知的數(shù)據(jù)源路由:
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. 添加租戶識(shí)別攔截器:
@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) { // 從請(qǐng)求頭中獲取租戶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. 實(shí)現(xiàn)動(dòng)態(tài)租戶管理:
@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存儲(chǔ)租戶數(shù)據(jù)源 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) { // 創(chuàng)建新的數(shù)據(jù)源 HikariDataSource dataSource = new HikariDataSource(); dataSource.setJdbcUrl(tenant.getDatabaseUrl()); dataSource.setUsername(tenant.getUsername()); dataSource.setPassword(tenant.getPassword()); dataSource.setDriverClassName(tenant.getDriverClassName()); // 存儲(chǔ)數(shù)據(jù)源 tenantDataSources.put(tenant.getId(), dataSource); // 更新路由數(shù)據(jù)源 updateRoutingDataSource(); // 保存租戶信息到數(shù)據(jù)庫 tenantRepository.save(tenant); } public void removeTenant(String tenantId) { DataSource dataSource = tenantDataSources.remove(tenantId); if (dataSource != null && dataSource instanceof HikariDataSource) { ((HikariDataSource) dataSource).close(); } // 更新路由數(shù)據(jù)源 updateRoutingDataSource(); // 從數(shù)據(jù)庫移除租戶 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); // 調(diào)用afterPropertiesSet初始化數(shù)據(jù)源 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("/{tenantId}") public ResponseEntity<Void> deleteTenant(@PathVariable String tenantId) { tenantService.removeTenant(tenantId); return ResponseEntity.noContent().build(); } }
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn):
• 數(shù)據(jù)隔離級(jí)別最高,安全性最佳
• 租戶可以使用不同的數(shù)據(jù)庫版本或類型
• 易于實(shí)現(xiàn)租戶特定的數(shù)據(jù)庫優(yōu)化
• 故障隔離,一個(gè)租戶的數(shù)據(jù)庫問題不影響其他租戶
• 便于獨(dú)立備份、恢復(fù)和遷移
缺點(diǎn):
• 資源利用率較低,成本較高
• 運(yùn)維復(fù)雜度高,需要管理多個(gè)數(shù)據(jù)庫實(shí)例
• 跨租戶查詢困難
• 每增加一個(gè)租戶需要?jiǎng)?chuàng)建新的數(shù)據(jù)庫實(shí)例
• 數(shù)據(jù)庫連接池管理復(fù)雜
適用場景
高要求的企業(yè)級(jí)SaaS應(yīng)用
租戶數(shù)量相對(duì)較少但數(shù)據(jù)量大的場景
租戶愿意支付更高費(fèi)用獲得更好隔離性的場景
方案二:共享數(shù)據(jù)庫,獨(dú)立Schema模式
原理與特點(diǎn)
在這種模式下,所有租戶共享同一個(gè)數(shù)據(jù)庫實(shí)例,但每個(gè)租戶擁有自己獨(dú)立的Schema(在PostgreSQL中)或數(shù)據(jù)庫(在MySQL中)。這種方式在資源共享和數(shù)據(jù)隔離之間取得了平衡。
實(shí)現(xiàn)步驟
1. 創(chuàng)建租戶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. 租戶實(shí)體和存儲(chǔ):
@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. 實(shí)現(xiàn)多租戶連接提供者:
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 { // 恢復(fù)到默認(rèn)Schema connection.createStatement().execute("SET SCHEMA 'public'"); } catch (SQLException e) { // 忽略錯(cuò)誤,確保連接關(guān)閉 } 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. 實(shí)現(xiàn)租戶標(biāo)識(shí)解析器:
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. 動(dòng)態(tài)租戶管理服務(wù):
@Service public class TenantSchemaManagementService { @Autowired private TenantRepository tenantRepository; @Autowired private DataSource dataSource; @Autowired private EntityManagerFactory entityManagerFactory; public void createTenant(Tenant tenant) { // 1. 創(chuàng)建Schema createSchemaIfNotExists(tenant.getSchemaName()); // 2. 保存租戶信息 tenantRepository.save(tenant); // 3. 初始化Schema的表結(jié)構(gòu) 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) { // 設(shè)置當(dāng)前租戶上下文 String previousTenant = TenantContextHolder.getTenantId(); try { TenantContextHolder.setTenantId(schemaName); // 使用JPA/Hibernate工具初始化Schema // 可以使用SchemaExport或更推薦使用Flyway/Liquibase Session session = entityManagerFactory.createEntityManager().unwrap(Session.class); session.doWork(connection -> { // 執(zhí)行DDL語句 }); } finally { // 恢復(fù)之前的租戶上下文 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(); } }
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn):
• 資源利用率高于獨(dú)立數(shù)據(jù)庫模式
• 較好的數(shù)據(jù)隔離性
• 運(yùn)維復(fù)雜度低于獨(dú)立數(shù)據(jù)庫模式
• 容易實(shí)現(xiàn)租戶特定的表結(jié)構(gòu)
• 數(shù)據(jù)庫級(jí)別的權(quán)限控制
缺點(diǎn):
• 數(shù)據(jù)庫管理復(fù)雜度增加
• 可能存在Schema數(shù)量限制
• 跨租戶查詢?nèi)匀焕щy
• 無法為不同租戶使用不同的數(shù)據(jù)庫類型
• 所有租戶共享數(shù)據(jù)庫資源,可能出現(xiàn)資源爭用
適用場景
中型SaaS應(yīng)用
租戶數(shù)量中等但增長較快的場景
需要較好數(shù)據(jù)隔離但成本敏感的應(yīng)用
PostgreSQL或MySQL等支持Schema/數(shù)據(jù)庫隔離的數(shù)據(jù)庫環(huán)境
方案三:共享數(shù)據(jù)庫,共享Schema,獨(dú)立表模式
原理與特點(diǎn)
在這種模式下,所有租戶共享同一個(gè)數(shù)據(jù)庫和Schema,但每個(gè)租戶有自己的表集合,通常通過表名前綴或后綴區(qū)分不同租戶的表。
實(shí)現(xiàn)步驟
1. 實(shí)現(xiàn)多租戶命名策略:
@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. 租戶實(shí)體和倉庫:
@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 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); // 使用JPA/Hibernate初始化表結(jié)構(gòu) // 在生產(chǎn)環(huán)境中,推薦使用Flyway或Liquibase進(jìn)行更精細(xì)的控制 Session session = entityManager.unwrap(Session.class); session.doWork(connection -> { // 執(zhí)行建表語句 // 這里可以使用Hibernate的SchemaExport,但為簡化,直接使用SQL // 示例:創(chuàng)建用戶表 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); // 創(chuàng)建其他表... } }); } finally { if (previousTenant != null) { TenantContextHolder.setTenantId(previousTenant); } else { TenantContextHolder.clear(); } } } public void dropTenantTables(String tenantId) { // 獲取數(shù)據(jù)庫中所有表 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. 租戶管理服務(wù):
@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(); } }
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn):
• 簡單易實(shí)現(xiàn),特別是對(duì)現(xiàn)有應(yīng)用的改造
• 資源利用率高
• 跨租戶查詢相對(duì)容易實(shí)現(xiàn)
• 維護(hù)成本低
• 租戶間表結(jié)構(gòu)可以不同
缺點(diǎn):
• 數(shù)據(jù)隔離級(jí)別較低
• 隨著租戶數(shù)量增加,表數(shù)量會(huì)急劇增長
• 數(shù)據(jù)庫對(duì)象(如表、索引)數(shù)量可能達(dá)到數(shù)據(jù)庫限制
• 備份和恢復(fù)單個(gè)租戶數(shù)據(jù)較為復(fù)雜
• 可能需要處理表名長度限制問題
適用場景
租戶數(shù)量適中且表結(jié)構(gòu)相對(duì)簡單的SaaS應(yīng)用
需要為不同租戶提供不同表結(jié)構(gòu)的場景
快速原型開發(fā)或MVP(最小可行產(chǎn)品)
從單租戶向多租戶過渡的系統(tǒng)
方案四:共享數(shù)據(jù)庫,共享Schema,共享表模式
原理與特點(diǎn)
這是隔離級(jí)別最低但資源效率最高的方案。所有租戶共享相同的數(shù)據(jù)庫、Schema和表,通過在每個(gè)表中添加"租戶ID"列來區(qū)分不同租戶的數(shù)據(jù)。
實(shí)現(xiàn)步驟
1. 創(chuàng)建租戶感知的實(shí)體基類:
@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. 租戶實(shí)體和倉庫:
@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. 實(shí)現(xiàn)租戶數(shù)據(jù)過濾器:
@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) { // 設(shè)置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. 為實(shí)體添加過濾器注解:
@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. 租戶管理服務(wù):
@Service public class SharedTableTenantService { @Autowired private TenantRepository tenantRepository; @Autowired private EntityManager entityManager; @Transactional public void createTenant(Tenant tenant) { // 直接保存租戶信息 tenantRepository.save(tenant); // 初始化租戶默認(rèn)數(shù)據(jù) initializeTenantData(tenant.getId()); } @Transactional public void deleteTenant(String tenantId) { // 刪除該租戶的所有數(shù)據(jù) deleteAllTenantData(tenantId); // 刪除租戶記錄 tenantRepository.deleteById(tenantId); } private void initializeTenantData(String tenantId) { String previousTenant = TenantContextHolder.getTenantId(); try { TenantContextHolder.setTenantId(tenantId); // 創(chuàng)建默認(rèn)用戶、角色等 // ... } finally { if (previousTenant != null) { TenantContextHolder.setTenantId(previousTenant); } else { TenantContextHolder.clear(); } } } private void deleteAllTenantData(String tenantId) { // 獲取所有帶有tenant_id列的表 List<String> tables = getTablesWithTenantIdColumn(); // 從每個(gè)表中刪除該租戶的數(shù)據(jù) 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(); } }
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn):
• 資源利用率最高
• 維護(hù)成本最低
• 實(shí)現(xiàn)簡單,對(duì)現(xiàn)有單租戶系統(tǒng)改造容易
• 跨租戶查詢簡單
• 節(jié)省存儲(chǔ)空間,特別是當(dāng)數(shù)據(jù)量小時(shí)
缺點(diǎn):
• 數(shù)據(jù)隔離級(jí)別最低
• 安全風(fēng)險(xiǎn)較高,一個(gè)錯(cuò)誤可能導(dǎo)致跨租戶數(shù)據(jù)泄露
• 所有租戶共享相同的表結(jié)構(gòu)
• 需要在所有數(shù)據(jù)訪問層強(qiáng)制租戶過濾
適用場景
租戶數(shù)量多但每個(gè)租戶數(shù)據(jù)量小的場景
成本敏感的應(yīng)用
原型驗(yàn)證或MVP階段
方案五:混合租戶模式
原理與特點(diǎn)
混合租戶模式結(jié)合了多種隔離策略,根據(jù)租戶等級(jí)、重要性或特定需求為不同租戶提供不同級(jí)別的隔離。例如,免費(fèi)用戶可能使用共享表模式,而付費(fèi)企業(yè)用戶可能使用獨(dú)立數(shù)據(jù)庫模式。
實(shí)現(xiàn)步驟
1. 租戶類型和存儲(chǔ):
@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. 創(chuàng)建租戶分類策略:
@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. 實(shí)現(xiàn)混合數(shù)據(jù)源路由:
@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) { // 對(duì)于獨(dú)立數(shù)據(jù)庫的租戶,查找或創(chuàng)建專用數(shù)據(jù)源 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. 混合租戶路由數(shù)據(jù)源:
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); // 根據(jù)隔離類型應(yīng)用不同策略 switch (isolationType) { case DEDICATED_DATABASE: // 已由數(shù)據(jù)源路由處理 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) { // 從請(qǐng)求中提取租戶ID的邏輯 return request.getHeader("X-TenantID"); } }
6. 綜合租戶管理服務(wù):
@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. 根據(jù)隔離類型初始化 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. 根據(jù)隔離類型清理資源 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); } // 獨(dú)立數(shù)據(jù)庫初始化 private void initializeDedicatedDatabase(Tenant tenant) { // 創(chuàng)建數(shù)據(jù)源 DataSource dedicatedDs = tenantRouter.getDataSourceForTenant(tenant.getId()); // 初始化數(shù)據(jù)庫結(jié)構(gòu) try (Connection conn = dedicatedDs.getConnection()) { // 執(zhí)行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()) { // 創(chuàng)建Schema try (Statement stmt = conn.createStatement()) { stmt.execute("CREATE SCHEMA IF NOT EXISTS " + tenant.getSchemaName()); } // 切換到該Schema conn.setSchema(tenant.getSchemaName()); // 創(chuàng)建表結(jié)構(gòu) // ... } catch (SQLException e) { throw new RuntimeException("Failed to initialize schema for tenant: " + tenant.getId(), e); } } // 獨(dú)立表初始化 private void initializeDedicatedTables(Tenant tenant) { // 設(shè)置線程上下文中的租戶ID以使用正確的表名前綴 String previousTenant = TenantContextHolder.getTenantId(); try { TenantContextHolder.setTenantId(tenant.getId()); // 創(chuàng)建表 // ... } finally { if (previousTenant != null) { TenantContextHolder.setTenantId(previousTenant); } else { TenantContextHolder.clear(); } } } // 共享表初始化 private void initializeSharedTables(Tenant tenant) { // 共享表模式下,只需插入租戶特定的初始數(shù)據(jù) String previousTenant = TenantContextHolder.getTenantId(); try { TenantContextHolder.setTenantId(tenant.getId()); // 插入初始數(shù)據(jù) // ... } finally { if (previousTenant != null) { TenantContextHolder.setTenantId(previousTenant); } else { TenantContextHolder.clear(); } } } // 清理方法 private void cleanupDedicatedDatabase(Tenant tenant) { // 關(guān)閉并移除數(shù)據(jù)源 tenantRouter.removeDedicatedDataSource(tenant.getId()); // 注意:通常不會(huì)自動(dòng)刪除實(shí)際的數(shù)據(jù)庫,這需要DBA手動(dòng)操作 } 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列的表中刪除該租戶的數(shù)據(jù) 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())); } }
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn):
• 最大的靈活性,可根據(jù)租戶需求提供不同隔離級(jí)別
• 可以實(shí)現(xiàn)資源和成本的平衡
• 可以根據(jù)業(yè)務(wù)價(jià)值分配資源
• 適應(yīng)不同客戶的安全和性能需求
缺點(diǎn):
• 實(shí)現(xiàn)復(fù)雜度最高
• 維護(hù)和測試成本高
• 需要處理多種數(shù)據(jù)訪問模式
• 可能引入不一致的用戶體驗(yàn)
• 錯(cuò)誤處理更加復(fù)雜
適用場景
需要提供靈活定價(jià)模型的應(yīng)用
資源需求差異大的租戶集合
方案對(duì)比
隔離模式 | 數(shù)據(jù)隔離級(jí)別 | 資源利用率 | 成本 | 復(fù)雜度 | 適用場景 |
獨(dú)立數(shù)據(jù)庫 | 最高 | 低 | 高 | 中 | 企業(yè)級(jí)應(yīng)用、金融/醫(yī)療行業(yè) |
獨(dú)立Schema | 高 | 中 | 中 | 中 | 中型SaaS、安全要求較高的場景 |
獨(dú)立表 | 中 | 中高 | 中低 | 低 | 中小型應(yīng)用、原型驗(yàn)證 |
共享表 | 低 | 最高 | 低 | 低 | 大量小租戶、成本敏感場景 |
混合模式 | 可變 | 可變 | 中高 | 高 | 多層級(jí)服務(wù)、復(fù)雜業(yè)務(wù)需求 |
總結(jié)
多租戶架構(gòu)是構(gòu)建現(xiàn)代SaaS應(yīng)用的關(guān)鍵技術(shù),選擇多租戶模式需要平衡數(shù)據(jù)隔離、資源利用、成本和復(fù)雜度等多種因素。
通過深入理解這些架構(gòu)模式及其權(quán)衡,可以根據(jù)實(shí)際情況選擇適合的多租戶架構(gòu),構(gòu)建可擴(kuò)展、安全且經(jīng)濟(jì)高效的企業(yè)級(jí)應(yīng)用。
以上就是SpringBoot實(shí)現(xiàn)多租戶系統(tǒng)架構(gòu)的5種設(shè)計(jì)方案介紹的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot多租戶架構(gòu)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
elasticsearch構(gòu)造Client實(shí)現(xiàn)java客戶端調(diào)用接口示例分析
這篇文章主要為大家介紹了elasticsearch構(gòu)造Client實(shí)現(xiàn)java客戶端調(diào)用接口示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04Springboot整合minio實(shí)現(xiàn)文件服務(wù)的教程詳解
這篇文章主要介紹了Springboot整合minio實(shí)現(xiàn)文件服務(wù)的教程,文中的示例代碼講解詳細(xì),對(duì)我們的工作或?qū)W習(xí)有一定幫助,需要的可以參考一下2022-06-06form表單回寫技術(shù)java實(shí)現(xiàn)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)form表單回寫技術(shù)的相關(guān)資料,需要的朋友可以參考下2016-04-04SpringBoot+WebSocket實(shí)現(xiàn)即時(shí)通訊功能(Spring方式)
今天給大家分享一個(gè)SpringBoot+WebSocket實(shí)現(xiàn)即時(shí)通訊功能(Spring方式),WebSocket是一種在單個(gè)TCP連接上進(jìn)行全雙工通信的協(xié)議,文章通過代碼示例給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-10-10Spring Boot構(gòu)建優(yōu)雅的RESTful接口過程詳解
這篇文章主要介紹了spring boot構(gòu)建優(yōu)雅的RESTful接口過程詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08Spring?security?oauth2以redis作為tokenstore及jackson序列化失敗問題
這篇文章主要介紹了Spring?security?oauth2以redis作為tokenstore及jackson序列化失敗問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教<BR>2024-04-04