在现代SaaS(Software as a Service)应用开发中,多租户架构是一个核心设计模式。它允许单一应用实例为多个租户(客户)提供服务,同时确保数据安全性、性能隔离和成本效益。本文将详细介绍如何在Java全栈项目中设计和实现多租户空间隔离架构。
什么是多租户架构?
多租户架构是一种软件架构模式,其中单个软件实例可以同时为多个租户提供服务。每个租户都拥有独立的数据空间和业务逻辑,但共享相同的基础设施和应用代码。
多租户架构的优势
- 成本效益:降低基础设施和维护成本
- 可扩展性:易于添加新租户和扩展资源
- 维护简化:统一的代码库和部署流程
- 资源利用率:更高效的资源共享和利用
多租户隔离策略
1. 数据库级隔离(Database per Tenant)
完全隔离方案
@Configuration
public class MultiTenantDatabaseConfig {
@Bean
@Primary
public DataSourceRouter dataSourceRouter() {
DataSourceRouter router = new DataSourceRouter();
Map<Object, Object> dataSources = new HashMap<>();
// 为每个租户配置独立数据源
dataSources.put("tenant1", createDataSource("tenant1_db"));
dataSources.put("tenant2", createDataSource("tenant2_db"));
router.setTargetDataSources(dataSources);
router.setDefaultTargetDataSource(dataSources.get("tenant1"));
return router;
}
private DataSource createDataSource(String databaseName) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/" + databaseName);
dataSource.setUsername("user");
dataSource.setPassword("password");
return dataSource;
}
}
2. Schema级隔离(Schema per Tenant)
中等隔离方案
@Component
public class TenantSchemaResolver {
public class TenantAwareDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String tenantId = TenantContext.getCurrentTenant();
return tenantId != null ? "schema_" + tenantId : "default_schema";
}
}
@PostConstruct
public void initializeSchemas() {
// 动态创建租户Schema
String createSchemaSql = "CREATE SCHEMA IF NOT EXISTS schema_%s";
// 执行Schema创建逻辑
}
}
3. 行级隔离(Row Level Security)
共享数据库方案
@Entity
@Table(name = "users")
@FilterDef(name = "tenantFilter", parameters = @ParamDef(name = "tenantId", type = "string"))
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "tenant_id", nullable = false)
private String tenantId;
@Column(name = "username")
private String username;
@Column(name = "email")
private String email;
// 其他字段和方法...
}
租户上下文管理
租户上下文类
public class TenantContext {
private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
public static void setCurrentTenant(String tenantId) {
CURRENT_TENANT.set(tenantId);
}
public static String getCurrentTenant() {
return CURRENT_TENANT.get();
}
public static void clear() {
CURRENT_TENANT.remove();
}
}
租户识别拦截器
@Component
public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String tenantId = extractTenantId(request);
if (tenantId != null && isValidTenant(tenantId)) {
TenantContext.setCurrentTenant(tenantId);
return true;
}
response.setStatus(HttpStatus.BAD_REQUEST.value());
return false;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) throws Exception {
TenantContext.clear();
}
private String extractTenantId(HttpServletRequest request) {
// 从Header中提取
String tenantFromHeader = request.getHeader("X-Tenant-ID");
if (tenantFromHeader != null) return tenantFromHeader;
// 从子域名提取
String serverName = request.getServerName();
if (serverName.contains(".")) {
return serverName.split("\\.")[0];
}
// 从路径参数提取
String pathInfo = request.getPathInfo();
if (pathInfo != null && pathInfo.startsWith("/tenant/")) {
return pathInfo.split("/")[2];
}
return null;
}
private boolean isValidTenant(String tenantId) {
// 验证租户ID的有效性
return tenantId.matches("^[a-zA-Z0-9_-]+$");
}
}
JPA多租户配置
多租户JPA配置
@Configuration
@EnableJpaRepositories(basePackages = "com.example.repository")
public class JpaMultiTenantConfig {
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
DataSource dataSource, JpaVendorAdapter jpaVendorAdapter) {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.example.entity");
em.setJpaVendorAdapter(jpaVendorAdapter);
Map<String, Object> properties = new HashMap<>();
properties.put("hibernate.multiTenancy", "SCHEMA");
properties.put("hibernate.multi_tenant_connection_provider",
multiTenantConnectionProvider());
properties.put("hibernate.tenant_identifier_resolver",
tenantIdentifierResolver());
em.setJpaPropertyMap(properties);
return em;
}
@Bean
public MultiTenantConnectionProvider multiTenantConnectionProvider() {
return new SchemaBasedMultiTenantConnectionProvider();
}
@Bean
public TenantIdentifierResolver tenantIdentifierResolver() {
return new TenantIdentifierResolverImpl();
}
}
自定义连接提供者
public class SchemaBasedMultiTenantConnectionProvider
implements MultiTenantConnectionProvider {
@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 tenantId) throws SQLException {
Connection connection = getAnyConnection();
try {
connection.createStatement()
.execute("USE schema_" + tenantId);
} catch (SQLException e) {
throw new HibernateException("无法切换到租户Schema: " + tenantId, e);
}
return connection;
}
@Override
public void releaseConnection(String tenantId, Connection connection)
throws SQLException {
releaseAnyConnection(connection);
}
}
服务层多租户支持
基础服务类
@Service
public abstract class BaseMultiTenantService<T, ID> {
protected abstract JpaRepository<T, ID> getRepository();
public List<T> findAll() {
enableTenantFilter();
return getRepository().findAll();
}
public Optional<T> findById(ID id) {
enableTenantFilter();
return getRepository().findById(id);
}
public T save(T entity) {
setTenantId(entity);
return getRepository().save(entity);
}
private void enableTenantFilter() {
String tenantId = TenantContext.getCurrentTenant();
if (tenantId != null) {
EntityManager em = getEntityManager();
Session session = em.unwrap(Session.class);
Filter filter = session.enableFilter("tenantFilter");
filter.setParameter("tenantId", tenantId);
}
}
private void setTenantId(T entity) {
String tenantId = TenantContext.getCurrentTenant();
if (tenantId != null && entity instanceof TenantAware) {
((TenantAware) entity).setTenantId(tenantId);
}
}
protected abstract EntityManager getEntityManager();
}
用户服务实现
@Service
@Transactional
public class UserService extends BaseMultiTenantService<User, Long> {
@Autowired
private UserRepository userRepository;
@PersistenceContext
private EntityManager entityManager;
@Override
protected JpaRepository<User, Long> getRepository() {
return userRepository;
}
@Override
protected EntityManager getEntityManager() {
return entityManager;
}
public User findByUsername(String username) {
enableTenantFilter();
return userRepository.findByUsername(username);
}
public List<User> findActiveUsers() {
enableTenantFilter();
return userRepository.findByActiveTrue();
}
}
前端多租户支持
Vue.js租户管理
// tenant-store.js
import { defineStore } from 'pinia'
export const useTenantStore = defineStore('tenant', {
state: () => ({
currentTenant: null,
tenantInfo: null,
availableTenants: []
}),
getters: {
isMultiTenant: (state) => state.availableTenants.length > 1,
tenantId: (state) => state.currentTenant,
tenantName: (state) => state.tenantInfo?.name || 'Unknown'
},
actions: {
setCurrentTenant(tenantId) {
this.currentTenant = tenantId
this.loadTenantInfo(tenantId)
// 更新HTTP请求Header
this.updateApiHeaders(tenantId)
},
async loadTenantInfo(tenantId) {
try {
const response = await api.get(`/tenants/${tenantId}`)
this.tenantInfo = response.data
} catch (error) {
console.error('加载租户信息失败:', error)
}
},
updateApiHeaders(tenantId) {
// 为所有API请求添加租户Header
api.defaults.headers.common['X-Tenant-ID'] = tenantId
}
}
})
HTTP拦截器
// api-interceptor.js
import axios from 'axios'
import { useTenantStore } from './tenant-store'
const api = axios.create({
baseURL: '/api',
timeout: 10000
})
// 请求拦截器
api.interceptors.request.use(
(config) => {
const tenantStore = useTenantStore()
const tenantId = tenantStore.tenantId
if (tenantId) {
config.headers['X-Tenant-ID'] = tenantId
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 400 &&
error.response?.data?.message === 'Invalid tenant') {
// 处理无效租户错误
console.error('无效的租户ID')
// 重定向到租户选择页面
}
return Promise.reject(error)
}
)
export default api
安全性考虑
租户数据隔离验证
@Component
public class TenantSecurityValidator {
@EventListener
public void handlePreUpdate(PreUpdateEvent event) {
validateTenantAccess(event.getEntity());
}
@EventListener
public void handlePreDelete(PreDeleteEvent event) {
validateTenantAccess(event.getEntity());
}
private void validateTenantAccess(Object entity) {
if (entity instanceof TenantAware) {
TenantAware tenantEntity = (TenantAware) entity;
String currentTenant = TenantContext.getCurrentTenant();
String entityTenant = tenantEntity.getTenantId();
if (!Objects.equals(currentTenant, entityTenant)) {
throw new SecurityException(
"租户 " + currentTenant + " 无权访问租户 " + entityTenant + " 的数据");
}
}
}
}
API安全控制
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
@PreAuthorize("hasRole('USER') and @tenantSecurityService.canAccessTenant(authentication, #tenantId)")
public ResponseEntity<List<User>> getUsers(
@RequestHeader("X-Tenant-ID") String tenantId) {
List<User> users = userService.findAll();
return ResponseEntity.ok(users);
}
@PostMapping
@PreAuthorize("hasRole('ADMIN') and @tenantSecurityService.canManageTenant(authentication)")
public ResponseEntity<User> createUser(@RequestBody User user) {
User savedUser = userService.save(user);
return ResponseEntity.ok(savedUser);
}
}
性能优化
连接池优化
@Configuration
public class MultiTenantDataSourceConfig {
@Bean
public HikariDataSource createOptimizedDataSource() {
HikariConfig config = new HikariConfig();
// 基础配置
config.setJdbcUrl("jdbc:mysql://localhost:3306/");
config.setUsername("user");
config.setPassword("password");
// 连接池优化
config.setMaximumPoolSize(20);
config.setMinimumIdle(5);
config.setConnectionTimeout(30000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
// 多租户优化
config.setLeakDetectionThreshold(60000);
config.addDataSourceProperty("useServerPrepStmts", "true");
config.addDataSourceProperty("cachePrepStmts", "true");
config.addDataSourceProperty("prepStmtCacheSize", "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
return new HikariDataSource(config);
}
}
缓存策略
@Service
public class TenantAwareCacheService {
private final Cache<String, Object> cache;
public TenantAwareCacheService() {
this.cache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofHours(1))
.build();
}
public <T> T get(String key, Class<T> type) {
String tenantKey = getTenantSpecificKey(key);
return type.cast(cache.getIfPresent(tenantKey));
}
public void put(String key, Object value) {
String tenantKey = getTenantSpecificKey(key);
cache.put(tenantKey, value);
}
private String getTenantSpecificKey(String key) {
String tenantId = TenantContext.getCurrentTenant();
return tenantId + ":" + key;
}
}
监控和运维
租户级别的监控
@Component
public class TenantMetrics {
private final MeterRegistry meterRegistry;
private final Counter requestCounter;
private final Timer responseTimer;
public TenantMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.requestCounter = Counter.builder("tenant.requests")
.description("租户请求计数")
.register(meterRegistry);
this.responseTimer = Timer.builder("tenant.response.time")
.description("租户响应时间")
.register(meterRegistry);
}
public void recordRequest(String tenantId) {
requestCounter.increment(Tags.of("tenant", tenantId));
}
public void recordResponseTime(String tenantId, Duration duration) {
responseTimer.record(duration, Tags.of("tenant", tenantId));
}
}
健康检查
@Component
public class TenantHealthIndicator implements HealthIndicator {
@Autowired
private DataSource dataSource;
@Override
public Health health() {
try {
Map<String, Object> details = new HashMap<>();
// 检查各租户数据库连接
List<String> activeTenants = getActiveTenants();
for (String tenant : activeTenants) {
boolean isHealthy = checkTenantHealth(tenant);
details.put("tenant_" + tenant, isHealthy ? "UP" : "DOWN");
}
return Health.up().withDetails(details).build();
} catch (Exception e) {
return Health.down().withException(e).build();
}
}
private boolean checkTenantHealth(String tenantId) {
try (Connection connection = dataSource.getConnection()) {
connection.createStatement()
.execute("SELECT 1 FROM schema_" + tenantId + ".users LIMIT 1");
return true;
} catch (SQLException e) {
return false;
}
}
private List<String> getActiveTenants() {
// 获取活跃租户列表的逻辑
return Arrays.asList("tenant1", "tenant2", "tenant3");
}
}
总结
多租户空间隔离架构是SaaS应用的核心技术挑战之一。通过合理的架构设计和技术选择,我们可以构建出既安全又高效的多租户系统。
关键要点:
- 选择合适的隔离级别:根据业务需求在安全性、性能和成本之间找到平衡
- 租户上下文管理:确保租户信息在整个请求生命周期中正确传递
- 数据安全:实施多层次的安全控制,防止数据泄露
- 性能优化:合理配置连接池和缓存策略
- 监控运维:建立完善的监控体系,确保系统稳定运行
通过本文介绍的架构模式和实现方案,您可以构建出产品级的多租户SaaS应用,为不同的客户提供安全、稳定、高效的服务。