多租户(Multi-tenancy)是一种软件架构模式,允许单个应用实例服务于多个客户(租户),同时保持租户数据的隔离性和安全性。
通过合理的多租户设计,企业可以显著降低运维成本、提升资源利用率,并实现更高效的服务交付。
本文将分享SpringBoot环境下实现多租户系统的5种架构设计方案
方案一:独立数据库模式
原理与特点
独立数据库模式为每个租户提供完全独立的数据库实例,是隔离级别最高的多租户方案。在这种模式下,租户数据完全分离,甚至可以部署在不同的服务器上。
实现步骤
- 创建多数据源配置:为每个租户配置独立的数据源
@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;
}
}
- 实现租户感知的数据源路由:
public class TenantAwareRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TenantContextHolder.getTenantId();
}
}
- 租户上下文管理:
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();
}
}
- 添加租户识别拦截器:
@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;
}
}
- 配置拦截器:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TenantIdentificationInterceptor tenantInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tenantInterceptor)
.addPathPatterns("/api/**");
}
}
- 实现动态租户管理:
@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);
}
}
}
- 提供租户管理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();
}
}
优缺点分析
优点:
- 数据隔离级别最高,安全性最佳
- 租户可以使用不同的数据库版本或类型
- 易于实现租户特定的数据库优化
- 故障隔离,一个租户的数据库问题不影响其他租户
- 便于独立备份、恢复和迁移
缺点:
- 资源利用率较低,成本较高
- 运维复杂度高,需要管理多个数据库实例
- 跨租户查询困难
- 每增加一个租户需要创建新的数据库实例
- 数据库连接池管理复杂
适用场景
- 高要求的企业级SaaS应用
- 租户数量相对较少但数据量大的场景
- 租户愿意支付更高费用获得更好隔离性的场景
方案二:共享数据库,独立Schema模式
原理与特点
在这种模式下,所有租户共享同一个数据库实例,但每个租户拥有自己独立的Schema(在PostgreSQL中)或数据库(在MySQL中)。这种方式在资源共享和数据隔离之间取得了平衡。
实现步骤
- 创建租户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);
}
}
}
- 租户实体和存储:
@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);
}
- 配置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();
}
}
- 实现多租户连接提供者:
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;
}
}
- 实现租户标识解析器:
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;
}
}
- 动态租户管理服务:
@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();
}
}
}
}
- 租户管理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,但每个租户有自己的表集合,通常通过表名前缀或后缀区分不同租户的表。
实现步骤
- 实现多租户命名策略:
@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);
}
}
- 配置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();
}
}
- 租户实体和仓库:
@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);
}
- 表初始化管理器:
@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初始化表结构
// 在生产环境中,推荐使用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);
}
}
}
- 租户管理服务:
@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);
}
}
- 提供租户管理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和表,通过在每个表中添加"租户ID"列来区分不同租户的数据。
实现步骤
- 创建租户感知的实体基类:
@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();
}
}
- 租户实体和仓库:
@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);
}
- 实现租户数据过滤器:
@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");
}
}
- 为实体添加过滤器注解:
@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;
// 其他字段和方法...
}
- 租户管理服务:
@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 void initializeTenantData(String tenantId) {
String previousTenant = TenantContextHolder.getTenantId();
try {
TenantContextHolder.setTenantId(tenantId);
// 创建默认用户、角色等
// ...
} finally {
if (previousTenant != null) {
TenantContextHolder.setTenantId(previousTenant);
} else {
TenantContextHolder.clear();
}
}
}
private void deleteAllTenantData(String tenantId) {
// 获取所有带有tenant_id列的表
List<String> tables = getTablesWithTenantIdColumn();
// 从每个表中删除该租户的数据
for (String table : tables) {
entityManager.createNativeQuery("DELETE FROM " + table + " WHERE tenant_id = :tenantId")
.setParameter("tenantId", tenantId)
.executeUpdate();
}
}
private List<String> getTablesWithTenantIdColumn() {
List<String> tables = new ArrayList<>();
try (Connection connection = entityManager.unwrap(SessionImplementor.class).connection()) {
DatabaseMetaData metaData = connection.getMetaData();
try (ResultSet rs = metaData.getTables(
connection.getCatalog(), connection.getSchema(), "%", new String[]{"TABLE"})) {
while (rs.next()) {
String tableName = rs.getString("TABLE_NAME");
// 检查表是否有tenant_id列
try (ResultSet columns = metaData.getColumns(
connection.getCatalog(), connection.getSchema(), tableName, "tenant_id")) {
if (columns.next()) {
tables.add(tableName);
}
}
}
}
} catch (SQLException e) {
throw new RuntimeException("Failed to get tables with tenant_id column", e);
}
return tables;
}
}
- 租户管理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阶段
方案五:混合租户模式
原理与特点
混合租户模式结合了多种隔离策略,根据租户等级、重要性或特定需求为不同租户提供不同级别的隔离。例如,免费用户可能使用共享表模式,而付费企业用户可能使用独立数据库模式。
实现步骤
- 租户类型和存储:
@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);
}
- 创建租户分类策略:
@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);
}
}
- 实现混合数据源路由:
@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();
}
}
}
- 混合租户路由数据源:
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);
}
}
- 混合租户拦截器:
@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");
}
}
- 综合租户管理服务:
@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);
}
}
- 提供租户管理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应用的关键技术,选择多租户模式需要平衡数据隔离、资源利用、成本和复杂度等多种因素。
通过深入理解这些架构模式及其权衡,可以根据实际情况选择适合的多租户架构,构建可扩展、安全且经济高效的企业级应用。