干掉單體!用 Spring Boot + PostgreSQL 搞定多租戶架構(gòu)
在現(xiàn)代 SaaS 系統(tǒng)中,多租戶架構(gòu)是支撐平臺高效運行的關(guān)鍵。傳統(tǒng)的單體數(shù)據(jù)庫設(shè)計一旦用戶量暴增,往往難以支撐隔離性、安全性和擴展性的需求。相比之下,多租戶架構(gòu)讓不同客戶的數(shù)據(jù)邏輯上分隔開,既能節(jié)省成本,又能提升靈活性。
本文將帶你從零開始,在 Spring Boot + PostgreSQL 項目中實現(xiàn) Schema-per-Tenant 的多租戶模式。我們會完整走通:依賴配置、核心代碼、數(shù)據(jù)庫建模和測試驗證,最后還會額外實現(xiàn)一個 動態(tài)創(chuàng)建租戶的服務(wù)。
多租戶實現(xiàn)的三種常見方式
在落地前,先快速對比一下三種主流多租戶實現(xiàn)思路:
- 數(shù)據(jù)庫級(Database-per-Tenant) 每個租戶獨立數(shù)據(jù)庫,隔離性最強,但資源開銷大,遷移和備份操作復(fù)雜。
- Schema級(Schema-per-Tenant) 共享同一個數(shù)據(jù)庫,每個租戶的數(shù)據(jù)放在獨立的 Schema 下。隔離性與資源利用率之間取得平衡,是企業(yè)實踐的主流方案。
- 表字段級(Discriminator Column) 所有租戶數(shù)據(jù)放在同一套表中,依靠 tenant_id 字段區(qū)分。實現(xiàn)簡單,但需要額外小心防止越權(quán)訪問。
本文聚焦于 Schema-per-Tenant 模式,這是在 PostgreSQL 下常見且高效的實現(xiàn)方式。
Schema-per-Tenant 思路概覽
實現(xiàn)思路大致如下:
- PostgreSQL 一個數(shù)據(jù)庫里存在多個 Schema(如 tenanta、tenantb)。
- Hibernate 配置成多租戶模式(SCHEMA)。
- 定義一個 自定義連接提供器,在連接獲取時執(zhí)行 SET search_path TO <schema>,保證 SQL 跑到正確的 Schema。
- 通過 Servlet Filter 攔截 HTTP 請求,從請求頭讀取 X-TenantID,并放入 ThreadLocal 上下文供 Hibernate 使用。
這樣,每個請求都會自動路由到對應(yīng)的 Schema,保證數(shù)據(jù)邏輯隔離。
項目依賴
項目使用 Gradle + Java 17,主要依賴如下:
- spring-boot-starter-web —— 構(gòu)建 REST API
- spring-boot-starter-data-jpa —— Hibernate ORM 支持
- postgresql —— PostgreSQL 驅(qū)動
- slf4j —— 日志框架
核心代碼實現(xiàn)
下面我們逐個文件梳理核心代碼
src/main/java/com/icoderoad/multitenant/context/AppTenantContext.java
作用: 攔截 HTTP 請求,解析請求頭 X-TenantID,寫入 ThreadLocal 并集成 MDC 日志上下文。
package com.icoderoad.multitenant.context;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Objects;
@Component
public class AppTenantContext implements Filter {
private static final String LOGGER_TENANT_ID = "tenant_id";
public static final String TENANT_HEADER = "X-TenantID";
private static final String DEFAULT_TENANT = "public";
private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
public static String getCurrentTenant() {
return Objects.requireNonNullElse(currentTenant.get(), DEFAULT_TENANT);
}
public static void setCurrentTenant(String tenant) {
MDC.put(LOGGER_TENANT_ID, tenant);
currentTenant.set(tenant);
}
public static void clear() {
MDC.clear();
currentTenant.remove();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String tenant = req.getHeader(TENANT_HEADER);
if (tenant != null) {
setCurrentTenant(tenant);
}
chain.doFilter(request, response);
clear();
}
}src/main/java/com/icoderoad/multitenant/resolver/CurrentTenantIdentifierResolverImpl.java
作用: 實現(xiàn) Hibernate 的 CurrentTenantIdentifierResolver,根據(jù)上下文獲取當前租戶。
package com.icoderoad.multitenant.resolver;
import com.icoderoad.multitenant.context.AppTenantContext;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import java.util.Objects;
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver<String> {
@Override
public String resolveCurrentTenantIdentifier() {
return Objects.requireNonNullElse(AppTenantContext.getCurrentTenant(), "public");
}
@Override
public boolean validateExistingCurrentSessions() {
return true;
}
}src/main/java/com/icoderoad/multitenant/provider/MultiTenantConnectionProviderImpl.java
作用: 在獲取連接時動態(tài)切換 Schema。
package com.icoderoad.multitenant.provider;
import org.hibernate.engine.jdbc.connections.spi.AbstractDataSourceBasedMultiTenantConnectionProviderImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
@Component
public class MultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {
private static final Logger logger = LoggerFactory.getLogger(MultiTenantConnectionProviderImpl.class);
private final DataSource dataSource;
public MultiTenantConnectionProviderImpl(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
protected DataSource selectAnyDataSource() {
return dataSource;
}
@Override
protected DataSource selectDataSource(Object tenantIdentifier) {
return dataSource;
}
@Override
public Connection getConnection(Object tenantIdentifier) throws SQLException {
String tenantId = tenantIdentifier != null ? tenantIdentifier.toString() : "public";
logger.info("Switching schema to {}", tenantId);
Connection connection = getAnyConnection();
try (Statement statement = connection.createStatement()) {
statement.execute(String.format("SET search_path TO %s;", tenantId));
}
return connection;
}
@Override
public void releaseConnection(Object tenantIdentifier, Connection connection) throws SQLException {
try (Statement statement = connection.createStatement()) {
statement.execute("SET search_path TO public;");
}
releaseAnyConnection(connection);
}
}src/main/java/com/icoderoad/multitenant/config/HibernateConfig.java
作用: 配置 Hibernate 的多租戶支持。
package com.icoderoad.multitenant.config;
import com.icoderoad.multitenant.provider.MultiTenantConnectionProviderImpl;
import com.icoderoad.multitenant.resolver.CurrentTenantIdentifierResolverImpl;
import org.hibernate.cfg.Environment;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class HibernateConfig {
private final JpaProperties jpaProperties;
public HibernateConfig(JpaProperties jpaProperties) {
this.jpaProperties = jpaProperties;
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
DataSource dataSource,
MultiTenantConnectionProviderImpl multiTenantConnectionProvider) {
Map<String, Object> props = new HashMap<>(jpaProperties.getProperties());
props.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
props.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, new CurrentTenantIdentifierResolverImpl());
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.icoderoad.multitenant.entity");
em.setJpaVendorAdapter(jpaVendorAdapter());
em.setJpaPropertyMap(props);
return em;
}
}配置文件 application.properties;
server.port=8082
spring.datasource.url=jdbc:postgresql://localhost:5432/testdb
spring.datasource.username=your_username
spring.datasource.password=your_password
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.multiTenancy=SCHEMA
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true數(shù)據(jù)庫準備:
CREATE DATABASE testdb;
CREATE SCHEMA tenanta;
CREATE SCHEMA tenantb;
GRANT USAGE ON SCHEMA tenanta TO your_username;
GRANT USAGE ON SCHEMA tenantb TO your_username;動態(tài)創(chuàng)建租戶 API;
//TenantService` & `TenantController
@Service
public class TenantService {
@Autowired
private JdbcTemplate jdbcTemplate;
public void createTenant(String tenantName) {
if (!tenantName.matches("^[a-zA-Z0-9_]+$")) {
throw new IllegalArgumentException("Invalid tenant name.");
}
jdbcTemplate.execute("CREATE SCHEMA IF NOT EXISTS " + tenantName);
}
}
@RestController
@RequestMapping("/tenants")
public class TenantController {
@Autowired
private TenantService tenantService;
@PostMapping("/create")
public String createTenant(@RequestParam("tenantName") String tenantName) {
tenantService.createTenant(tenantName);
return "Tenant " + tenantName + " created successfully";
}
}測試
在 Postman 發(fā)起請求時,帶上 Header:
X-TenantID: tenanta即可將數(shù)據(jù)寫入 tenanta Schema,切換成 tenantb 就能實現(xiàn)隔離存儲。
結(jié)論
通過 Spring Boot + PostgreSQL,我們成功構(gòu)建了一個 Schema-per-Tenant 的多租戶架構(gòu)。 這種方式兼顧了性能和隔離性,既避免了數(shù)據(jù)庫級方案的高昂成本,又優(yōu)于表字段區(qū)分的低隔離模式。
更進一步,我們還擴展了 動態(tài)創(chuàng)建租戶的能力,讓平臺具備了 SaaS 化的核心競爭力。
未來可以繼續(xù)在此基礎(chǔ)上增強:
- 租戶生命周期管理(創(chuàng)建、禁用、刪除)
- Schema 自動遷移與升級
- 多租戶下的監(jiān)控與審計
這樣,一個靈活、安全、可擴展的 多租戶 SaaS 平臺 才算真正搭建完成。



































