Java全栈SASS程序-设计多租户空间隔离架构

发布于:2025-09-03 ⋅ 阅读:(15) ⋅ 点赞:(0)

在现代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应用的核心技术挑战之一。通过合理的架构设计和技术选择,我们可以构建出既安全又高效的多租户系统。

关键要点

  1. 选择合适的隔离级别:根据业务需求在安全性、性能和成本之间找到平衡
  2. 租户上下文管理:确保租户信息在整个请求生命周期中正确传递
  3. 数据安全:实施多层次的安全控制,防止数据泄露
  4. 性能优化:合理配置连接池和缓存策略
  5. 监控运维:建立完善的监控体系,确保系统稳定运行

通过本文介绍的架构模式和实现方案,您可以构建出产品级的多租户SaaS应用,为不同的客户提供安全、稳定、高效的服务。



网站公告

今日签到

点亮在社区的每一天
去签到