SpringBoot實現(xiàn)多租戶系統(tǒng)架構的5種設計方案介紹
多租戶(Multi-tenancy)是一種軟件架構模式,允許單個應用實例服務于多個客戶(租戶),同時保持租戶數(shù)據(jù)的隔離性和安全性。
通過合理的多租戶設計,企業(yè)可以顯著降低運維成本、提升資源利用率,并實現(xiàn)更高效的服務交付。
本文將分享SpringBoot環(huán)境下實現(xiàn)多租戶系統(tǒng)的5種架構設計方案
方案一:獨立數(shù)據(jù)庫模式
原理與特點
獨立數(shù)據(jù)庫模式為每個租戶提供完全獨立的數(shù)據(jù)庫實例,是隔離級別最高的多租戶方案。在這種模式下,租戶數(shù)據(jù)完全分離,甚至可以部署在不同的服務器上。
實現(xiàn)步驟
1. 創(chuàng)建多數(shù)據(jù)源配置:為每個租戶配置獨立的數(shù)據(jù)源
@Configuration
public class MultiTenantDatabaseConfig {
@Autowired
private TenantDataSourceProperties properties;
@Bean
public DataSource dataSource() {
AbstractRoutingDataSource multiTenantDataSource = new TenantAwareRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
// 為每個租戶創(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. 實現(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. 添加租戶識別攔截器:
@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. 實現(xiàn)動態(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存儲租戶數(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());
// 存儲數(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);
// 調用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)缺點分析
優(yōu)點:
• 數(shù)據(jù)隔離級別最高,安全性最佳
• 租戶可以使用不同的數(shù)據(jù)庫版本或類型
• 易于實現(xiàn)租戶特定的數(shù)據(jù)庫優(yōu)化
• 故障隔離,一個租戶的數(shù)據(jù)庫問題不影響其他租戶
• 便于獨立備份、恢復和遷移
缺點:
• 資源利用率較低,成本較高
• 運維復雜度高,需要管理多個數(shù)據(jù)庫實例
• 跨租戶查詢困難
• 每增加一個租戶需要創(chuàng)建新的數(shù)據(jù)庫實例
• 數(shù)據(jù)庫連接池管理復雜
適用場景
高要求的企業(yè)級SaaS應用
租戶數(shù)量相對較少但數(shù)據(jù)量大的場景
租戶愿意支付更高費用獲得更好隔離性的場景
方案二:共享數(shù)據(jù)庫,獨立Schema模式
原理與特點
在這種模式下,所有租戶共享同一個數(shù)據(jù)庫實例,但每個租戶擁有自己獨立的Schema(在PostgreSQL中)或數(shù)據(jù)庫(在MySQL中)。這種方式在資源共享和數(shù)據(jù)隔離之間取得了平衡。
實現(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. 租戶實體和存儲:
@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. 實現(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 {
// 恢復到默認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. 實現(xiàn)租戶標識解析器:
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. 動態(tài)租戶管理服務:
@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的表結構
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 -> {
// 執(zhí)行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();
}
}優(yōu)缺點分析
優(yōu)點:
• 資源利用率高于獨立數(shù)據(jù)庫模式
• 較好的數(shù)據(jù)隔離性
• 運維復雜度低于獨立數(shù)據(jù)庫模式
• 容易實現(xiàn)租戶特定的表結構
• 數(shù)據(jù)庫級別的權限控制
缺點:
• 數(shù)據(jù)庫管理復雜度增加
• 可能存在Schema數(shù)量限制
• 跨租戶查詢?nèi)匀焕щy
• 無法為不同租戶使用不同的數(shù)據(jù)庫類型
• 所有租戶共享數(shù)據(jù)庫資源,可能出現(xiàn)資源爭用
適用場景
中型SaaS應用
租戶數(shù)量中等但增長較快的場景
需要較好數(shù)據(jù)隔離但成本敏感的應用
PostgreSQL或MySQL等支持Schema/數(shù)據(jù)庫隔離的數(shù)據(jù)庫環(huán)境
方案三:共享數(shù)據(jù)庫,共享Schema,獨立表模式
原理與特點
在這種模式下,所有租戶共享同一個數(shù)據(jù)庫和Schema,但每個租戶有自己的表集合,通常通過表名前綴或后綴區(qū)分不同租戶的表。
實現(xiàn)步驟
1. 實現(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. 租戶實體和倉庫:
@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初始化表結構
// 在生產(chǎn)環(huán)境中,推薦使用Flyway或Liquibase進行更精細的控制
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. 租戶管理服務:
@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)缺點分析
優(yōu)點:
• 簡單易實現(xiàn),特別是對現(xiàn)有應用的改造
• 資源利用率高
• 跨租戶查詢相對容易實現(xiàn)
• 維護成本低
• 租戶間表結構可以不同
缺點:
• 數(shù)據(jù)隔離級別較低
• 隨著租戶數(shù)量增加,表數(shù)量會急劇增長
• 數(shù)據(jù)庫對象(如表、索引)數(shù)量可能達到數(shù)據(jù)庫限制
• 備份和恢復單個租戶數(shù)據(jù)較為復雜
• 可能需要處理表名長度限制問題
適用場景
租戶數(shù)量適中且表結構相對簡單的SaaS應用
需要為不同租戶提供不同表結構的場景
快速原型開發(fā)或MVP(最小可行產(chǎn)品)
從單租戶向多租戶過渡的系統(tǒng)
方案四:共享數(shù)據(jù)庫,共享Schema,共享表模式
原理與特點
這是隔離級別最低但資源效率最高的方案。所有租戶共享相同的數(shù)據(jù)庫、Schema和表,通過在每個表中添加"租戶ID"列來區(qū)分不同租戶的數(shù)據(jù)。
實現(xiàn)步驟
1. 創(chuàng)建租戶感知的實體基類:
@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. 實現(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) {
// 設置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);
// 初始化租戶默認數(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)建默認用戶、角色等
// ...
} finally {
if (previousTenant != null) {
TenantContextHolder.setTenantId(previousTenant);
} else {
TenantContextHolder.clear();
}
}
}
private void deleteAllTenantData(String tenantId) {
// 獲取所有帶有tenant_id列的表
List<String> tables = getTablesWithTenantIdColumn();
// 從每個表中刪除該租戶的數(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)缺點分析
優(yōu)點:
• 資源利用率最高
• 維護成本最低
• 實現(xiàn)簡單,對現(xiàn)有單租戶系統(tǒng)改造容易
• 跨租戶查詢簡單
• 節(jié)省存儲空間,特別是當數(shù)據(jù)量小時
缺點:
• 數(shù)據(jù)隔離級別最低
• 安全風險較高,一個錯誤可能導致跨租戶數(shù)據(jù)泄露
• 所有租戶共享相同的表結構
• 需要在所有數(shù)據(jù)訪問層強制租戶過濾
適用場景
租戶數(shù)量多但每個租戶數(shù)據(jù)量小的場景
成本敏感的應用
原型驗證或MVP階段
方案五:混合租戶模式
原理與特點
混合租戶模式結合了多種隔離策略,根據(jù)租戶等級、重要性或特定需求為不同租戶提供不同級別的隔離。例如,免費用戶可能使用共享表模式,而付費企業(yè)用戶可能使用獨立數(shù)據(jù)庫模式。
實現(xiàn)步驟
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. 創(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. 實現(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) {
// 對于獨立數(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ù)隔離類型應用不同策略
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) {
// 從請求中提取租戶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. 根據(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);
}
// 獨立數(shù)據(jù)庫初始化
private void initializeDedicatedDatabase(Tenant tenant) {
// 創(chuàng)建數(shù)據(jù)源
DataSource dedicatedDs = tenantRouter.getDataSourceForTenant(tenant.getId());
// 初始化數(shù)據(jù)庫結構
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)建表結構
// ...
} 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());
// 創(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) {
// 關閉并移除數(shù)據(jù)源
tenantRouter.removeDedicatedDataSource(tenant.getId());
// 注意:通常不會自動刪除實際的數(shù)據(jù)庫,這需要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列的表中刪除該租戶的數(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)缺點分析
優(yōu)點:
• 最大的靈活性,可根據(jù)租戶需求提供不同隔離級別
• 可以實現(xiàn)資源和成本的平衡
• 可以根據(jù)業(yè)務價值分配資源
• 適應不同客戶的安全和性能需求
缺點:
• 實現(xiàn)復雜度最高
• 維護和測試成本高
• 需要處理多種數(shù)據(jù)訪問模式
• 可能引入不一致的用戶體驗
• 錯誤處理更加復雜
適用場景
需要提供靈活定價模型的應用
資源需求差異大的租戶集合
方案對比
| 隔離模式 | 數(shù)據(jù)隔離級別 | 資源利用率 | 成本 | 復雜度 | 適用場景 |
| 獨立數(shù)據(jù)庫 | 最高 | 低 | 高 | 中 | 企業(yè)級應用、金融/醫(yī)療行業(yè) |
| 獨立Schema | 高 | 中 | 中 | 中 | 中型SaaS、安全要求較高的場景 |
| 獨立表 | 中 | 中高 | 中低 | 低 | 中小型應用、原型驗證 |
| 共享表 | 低 | 最高 | 低 | 低 | 大量小租戶、成本敏感場景 |
| 混合模式 | 可變 | 可變 | 中高 | 高 | 多層級服務、復雜業(yè)務需求 |
總結
多租戶架構是構建現(xiàn)代SaaS應用的關鍵技術,選擇多租戶模式需要平衡數(shù)據(jù)隔離、資源利用、成本和復雜度等多種因素。
通過深入理解這些架構模式及其權衡,可以根據(jù)實際情況選擇適合的多租戶架構,構建可擴展、安全且經(jīng)濟高效的企業(yè)級應用。
以上就是SpringBoot實現(xiàn)多租戶系統(tǒng)架構的5種設計方案介紹的詳細內(nèi)容,更多關于SpringBoot多租戶架構的資料請關注腳本之家其它相關文章!
相關文章
elasticsearch構造Client實現(xiàn)java客戶端調用接口示例分析
這篇文章主要為大家介紹了elasticsearch構造Client實現(xiàn)java客戶端調用接口示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-04-04
Springboot整合minio實現(xiàn)文件服務的教程詳解
這篇文章主要介紹了Springboot整合minio實現(xiàn)文件服務的教程,文中的示例代碼講解詳細,對我們的工作或學習有一定幫助,需要的可以參考一下2022-06-06
SpringBoot+WebSocket實現(xiàn)即時通訊功能(Spring方式)
今天給大家分享一個SpringBoot+WebSocket實現(xiàn)即時通訊功能(Spring方式),WebSocket是一種在單個TCP連接上進行全雙工通信的協(xié)議,文章通過代碼示例給大家介紹的非常詳細,需要的朋友可以參考下2023-10-10
Spring Boot構建優(yōu)雅的RESTful接口過程詳解
這篇文章主要介紹了spring boot構建優(yōu)雅的RESTful接口過程詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2019-08-08
Spring?security?oauth2以redis作為tokenstore及jackson序列化失敗問題
這篇文章主要介紹了Spring?security?oauth2以redis作為tokenstore及jackson序列化失敗問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教<BR>2024-04-04

