Spring MVC 多租户架构与数据隔离教程
文章目录
- 目录
- 多租户架构概述
- 多租户架构定义
- 多租户架构优势
- 多租户架构模式
- 多租户数据隔离策略
- 数据库级隔离实现
- 表级隔离实现
- 行级隔离实现
- 租户识别与路由机制
- 子域名识别
- URL路径识别
- 请求头识别
- 租户拦截器
- Spring MVC多租户实现
- 租户配置管理
- 多租户控制器
- 租户数据源路由
- 数据访问层多租户设计
- 租户感知Repository
- 租户数据过滤器
- 多租户事务管理
- 缓存策略与租户隔离
- 租户缓存配置
- 租户缓存键策略
- 缓存服务实现
- 安全与权限控制
- 租户权限管理
- 租户安全拦截器
- 性能优化与监控
- 租户性能监控
- 租户资源限制
- 租户缓存预热
- 最佳实践
- 1. 租户隔离最佳实践
- 2. 性能优化最佳实践
- 3. 安全最佳实践
- 常见问题解决
- 1. 租户上下文丢失
- 2. 缓存污染
- 3. 事务回滚问题
- 总结
目录
- 多租户架构概述
- 多租户数据隔离策略
- 租户识别与路由机制
- Spring MVC多租户实现
- 数据访问层多租户设计
- 缓存策略与租户隔离
- 安全与权限控制
- 性能优化与监控
- 最佳实践
- 常见问题解决
- 总结
多租户架构概述
多租户架构定义
多租户架构(Multi-Tenancy)是一种软件架构模式,其中单个应用程序实例为多个租户(客户、组织或用户组)提供服务,同时确保租户之间的数据隔离和安全性。
多租户架构优势
1. 成本效益
- 共享基础设施,降低运营成本
- 统一维护和升级
- 资源利用率高
2. 可扩展性
- 支持大量租户
- 弹性扩展能力
- 快速部署新租户
3. 维护便利
- 统一代码库
- 集中式管理
- 标准化流程
多租户架构模式
1. 数据库级隔离(Database per Tenant)
租户A → 数据库A
租户B → 数据库B
租户C → 数据库C
2. 表级隔离(Schema per Tenant)
数据库
├── tenant_a_schema
├── tenant_b_schema
└── tenant_c_schema
3. 行级隔离(Row Level Security)
用户表
├── id, name, tenant_id
├── 1, Alice, tenant_a
├── 2, Bob, tenant_b
└── 3, Carol, tenant_a
多租户数据隔离策略
数据库级隔离实现
Maven依赖配置
<dependencies><!-- Spring Boot Starter --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Data JPA --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><!-- 多数据源支持 --><dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>3.5.2</version></dependency><!-- 租户上下文 --><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId></dependency>
</dependencies>
多数据源配置
@Configuration
@EnableTransactionManagement
public class MultiTenantDataSourceConfig {@Bean@ConfigurationProperties("spring.datasource.master")public DataSource masterDataSource() {return DataSourceBuilder.create().build();}@Bean@ConfigurationProperties("spring.datasource.tenant")public DataSource tenantDataSource() {return DataSourceBuilder.create().build();}@Beanpublic DataSource routingDataSource() {DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();Map<Object, Object> dataSourceMap = new HashMap<>();dataSourceMap.put("master", masterDataSource());dataSourceMap.put("tenant", tenantDataSource());routingDataSource.setDefaultTargetDataSource(masterDataSource());routingDataSource.setTargetDataSources(dataSourceMap);return routingDataSource;}
}
表级隔离实现
租户Schema管理
@Component
public class TenantSchemaManager {@Autowiredprivate JdbcTemplate jdbcTemplate;public void createTenantSchema(String tenantId) {String schemaName = "tenant_" + tenantId;// 创建租户SchemajdbcTemplate.execute("CREATE SCHEMA IF NOT EXISTS " + schemaName);// 创建租户表createTenantTables(schemaName);}private void createTenantTables(String schemaName) {String createUserTable = """CREATE TABLE IF NOT EXISTS %s.users (id BIGINT PRIMARY KEY AUTO_INCREMENT,username VARCHAR(50) NOT NULL,email VARCHAR(100) NOT NULL,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)""".formatted(schemaName);jdbcTemplate.execute(createUserTable);}public void switchToTenantSchema(String tenantId) {String schemaName = "tenant_" + tenantId;jdbcTemplate.execute("USE " + schemaName);}
}
行级隔离实现
租户上下文管理
public class TenantContext {private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();public static void setCurrentTenant(String tenantId) {currentTenant.set(tenantId);}public static String getCurrentTenant() {return currentTenant.get();}public static void clear() {currentTenant.remove();}
}
租户实体基类
@MappedSuperclass
public abstract class TenantAwareEntity {@Column(name = "tenant_id", nullable = false)private String tenantId;@PrePersistprotected void onCreate() {if (tenantId == null) {tenantId = TenantContext.getCurrentTenant();}}// getter and setterpublic String getTenantId() {return tenantId;}public void setTenantId(String tenantId) {this.tenantId = tenantId;}
}
租户识别与路由机制
子域名识别
租户解析器
@Component
public class SubdomainTenantResolver implements TenantResolver {@Overridepublic String resolveTenant(HttpServletRequest request) {String serverName = request.getServerName();// 解析子域名:tenant1.example.com -> tenant1if (serverName.contains(".")) {String[] parts = serverName.split("\\.");if (parts.length >= 3) {return parts[0];}}return "default";}
}
URL路径识别
路径租户解析器
@Component
public class PathTenantResolver implements TenantResolver {@Overridepublic String resolveTenant(HttpServletRequest request) {String requestURI = request.getRequestURI();// 解析路径:/tenant/tenant1/users -> tenant1Pattern pattern = Pattern.compile("^/tenant/([^/]+)/");Matcher matcher = pattern.matcher(requestURI);if (matcher.find()) {return matcher.group(1);}return "default";}
}
请求头识别
Header租户解析器
@Component
public class HeaderTenantResolver implements TenantResolver {private static final String TENANT_HEADER = "X-Tenant-ID";@Overridepublic String resolveTenant(HttpServletRequest request) {String tenantId = request.getHeader(TENANT_HEADER);return tenantId != null ? tenantId : "default";}
}
租户拦截器
租户上下文拦截器
@Component
public class TenantContextInterceptor implements HandlerInterceptor {@Autowiredprivate List<TenantResolver> tenantResolvers;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String tenantId = resolveTenant(request);if (tenantId == null || tenantId.isEmpty()) {response.setStatus(HttpStatus.BAD_REQUEST.value());response.getWriter().write("Tenant ID is required");return false;}// 验证租户是否存在if (!isValidTenant(tenantId)) {response.setStatus(HttpStatus.FORBIDDEN.value());response.getWriter().write("Invalid tenant ID");return false;}TenantContext.setCurrentTenant(tenantId);return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {TenantContext.clear();}private String resolveTenant(HttpServletRequest request) {for (TenantResolver resolver : tenantResolvers) {String tenantId = resolver.resolveTenant(request);if (tenantId != null && !tenantId.equals("default")) {return tenantId;}}return "default";}private boolean isValidTenant(String tenantId) {// 实现租户验证逻辑return tenantService.exists(tenantId);}
}
Spring MVC多租户实现
租户配置管理
租户配置实体
@Entity
@Table(name = "tenant_configs")
public class TenantConfig {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(name = "tenant_id", nullable = false)private String tenantId;@Column(name = "config_key", nullable = false)private String configKey;@Column(name = "config_value")private String configValue;@Column(name = "config_type")private String configType;// constructors, getters, setters
}
租户配置服务
@Service
public class TenantConfigService {@Autowiredprivate TenantConfigRepository configRepository;public String getConfig(String key, String defaultValue) {String tenantId = TenantContext.getCurrentTenant();Optional<TenantConfig> config = configRepository.findByTenantIdAndConfigKey(tenantId, key);return config.map(TenantConfig::getConfigValue).orElse(defaultValue);}public void setConfig(String key, String value) {String tenantId = TenantContext.getCurrentTenant();TenantConfig config = configRepository.findByTenantIdAndConfigKey(tenantId, key).orElse(new TenantConfig());config.setTenantId(tenantId);config.setConfigKey(key);config.setConfigValue(value);configRepository.save(config);}
}
多租户控制器
租户感知控制器
@RestController
@RequestMapping("/api/tenant")
public class TenantAwareController {@Autowiredprivate UserService userService;@Autowiredprivate TenantConfigService configService;@GetMapping("/users")public ResponseEntity<List<User>> getUsers() {// 自动使用当前租户上下文List<User> users = userService.findAll();return ResponseEntity.ok(users);}@PostMapping("/users")public ResponseEntity<User> createUser(@RequestBody User user) {// 租户ID会自动设置User savedUser = userService.save(user);return ResponseEntity.ok(savedUser);}@GetMapping("/config/{key}")public ResponseEntity<String> getConfig(@PathVariable String key) {String value = configService.getConfig(key, "");return ResponseEntity.ok(value);}
}
租户数据源路由
动态数据源路由
@Component
public class TenantDataSourceRouter {@Autowiredprivate DataSource masterDataSource;private final Map<String, DataSource> tenantDataSources = new ConcurrentHashMap<>();public DataSource getDataSource(String tenantId) {if ("master".equals(tenantId)) {return masterDataSource;}return tenantDataSources.computeIfAbsent(tenantId, this::createTenantDataSource);}private DataSource createTenantDataSource(String tenantId) {// 根据租户ID创建数据源HikariConfig config = new HikariConfig();config.setJdbcUrl("jdbc:mysql://localhost:3306/tenant_" + tenantId);config.setUsername("tenant_user");config.setPassword("tenant_password");return new HikariDataSource(config);}
}
数据访问层多租户设计
租户感知Repository
基础租户Repository
@NoRepositoryBean
public interface TenantAwareRepository<T, ID> extends JpaRepository<T, ID> {@Query("SELECT e FROM #{#entityName} e WHERE e.tenantId = :tenantId")List<T> findByTenantId(@Param("tenantId") String tenantId);@Query("SELECT e FROM #{#entityName} e WHERE e.tenantId = :tenantId AND e.id = :id")Optional<T> findByTenantIdAndId(@Param("tenantId") String tenantId, @Param("id") ID id);@Modifying@Query("DELETE FROM #{#entityName} e WHERE e.tenantId = :tenantId")void deleteByTenantId(@Param("tenantId") String tenantId);
}
用户Repository实现
@Repository
public interface UserRepository extends TenantAwareRepository<User, Long> {List<User> findByTenantIdAndUsernameContaining(String tenantId, String username);@Query("SELECT u FROM User u WHERE u.tenantId = :tenantId AND u.email = :email")Optional<User> findByTenantIdAndEmail(@Param("tenantId") String tenantId, @Param("email") String email);
}
租户数据过滤器
JPA租户过滤器
@Component
public class TenantFilter {@EventListenerpublic void handleTenantFilter(EntityManagerFactory emf) {EntityManager em = emf.createEntityManager();// 设置租户过滤条件String tenantId = TenantContext.getCurrentTenant();if (tenantId != null) {em.setProperty("tenant.id", tenantId);}}
}
Hibernate过滤器配置
@Entity
@FilterDef(name = "tenantFilter", parameters = @ParamDef(name = "tenantId", type = "string"))
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class User extends TenantAwareEntity {// entity fields
}
多租户事务管理
租户事务管理器
@Configuration
@EnableTransactionManagement
public class TenantTransactionConfig {@Beanpublic PlatformTransactionManager tenantTransactionManager() {return new DataSourceTransactionManager(tenantDataSource());}@Beanpublic TransactionTemplate tenantTransactionTemplate() {return new TransactionTemplate(tenantTransactionManager());}
}
缓存策略与租户隔离
租户缓存配置
Redis租户缓存配置
@Configuration
@EnableCaching
public class TenantCacheConfig {@Beanpublic CacheManager cacheManager(RedisConnectionFactory connectionFactory) {RedisCacheManager.Builder builder = RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory).cacheDefaults(cacheConfiguration());return builder.build();}private RedisCacheConfiguration cacheConfiguration() {return RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(60)).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));}
}
租户缓存键策略
租户缓存键生成器
@Component
public class TenantCacheKeyGenerator implements KeyGenerator {@Overridepublic Object generate(Object target, Method method, Object... params) {String tenantId = TenantContext.getCurrentTenant();String className = target.getClass().getSimpleName();String methodName = method.getName();StringBuilder keyBuilder = new StringBuilder();keyBuilder.append(tenantId).append(":");keyBuilder.append(className).append(":");keyBuilder.append(methodName);for (Object param : params) {keyBuilder.append(":").append(param.toString());}return keyBuilder.toString();}
}
缓存服务实现
租户缓存服务
@Service
public class TenantCacheService {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;public void put(String key, Object value) {String tenantId = TenantContext.getCurrentTenant();String tenantKey = tenantId + ":" + key;redisTemplate.opsForValue().set(tenantKey, value);}public Object get(String key) {String tenantId = TenantContext.getCurrentTenant();String tenantKey = tenantId + ":" + key;return redisTemplate.opsForValue().get(tenantKey);}public void evict(String key) {String tenantId = TenantContext.getCurrentTenant();String tenantKey = tenantId + ":" + key;redisTemplate.delete(tenantKey);}public void evictByPattern(String pattern) {String tenantId = TenantContext.getCurrentTenant();String tenantPattern = tenantId + ":" + pattern;Set<String> keys = redisTemplate.keys(tenantPattern);if (!keys.isEmpty()) {redisTemplate.delete(keys);}}
}
安全与权限控制
租户权限管理
租户权限实体
@Entity
@Table(name = "tenant_permissions")
public class TenantPermission {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(name = "tenant_id", nullable = false)private String tenantId;@Column(name = "user_id", nullable = false)private Long userId;@Column(name = "resource", nullable = false)private String resource;@Column(name = "action", nullable = false)private String action;@Column(name = "granted", nullable = false)private Boolean granted;// constructors, getters, setters
}
租户权限服务
@Service
public class TenantPermissionService {@Autowiredprivate TenantPermissionRepository permissionRepository;public boolean hasPermission(String resource, String action) {String tenantId = TenantContext.getCurrentTenant();Long userId = getCurrentUserId();Optional<TenantPermission> permission = permissionRepository.findByTenantIdAndUserIdAndResourceAndAction(tenantId, userId, resource, action);return permission.map(TenantPermission::getGranted).orElse(false);}public void grantPermission(Long userId, String resource, String action) {String tenantId = TenantContext.getCurrentTenant();TenantPermission permission = new TenantPermission();permission.setTenantId(tenantId);permission.setUserId(userId);permission.setResource(resource);permission.setAction(action);permission.setGranted(true);permissionRepository.save(permission);}private Long getCurrentUserId() {// 从安全上下文获取当前用户IDAuthentication authentication = SecurityContextHolder.getContext().getAuthentication();if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {UserDetails userDetails = (UserDetails) authentication.getPrincipal();return Long.valueOf(userDetails.getUsername());}return null;}
}
租户安全拦截器
租户权限拦截器
@Component
public class TenantSecurityInterceptor implements HandlerInterceptor {@Autowiredprivate TenantPermissionService permissionService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String tenantId = TenantContext.getCurrentTenant();String resource = getResourceFromRequest(request);String action = getActionFromRequest(request);if (!permissionService.hasPermission(resource, action)) {response.setStatus(HttpStatus.FORBIDDEN.value());response.getWriter().write("Access denied for tenant: " + tenantId);return false;}return true;}private String getResourceFromRequest(HttpServletRequest request) {String requestURI = request.getRequestURI();// 解析资源名称return requestURI.split("/")[2]; // 假设格式为 /api/tenant/{resource}}private String getActionFromRequest(HttpServletRequest request) {String method = request.getMethod();return method.toLowerCase();}
}
性能优化与监控
租户性能监控
租户性能指标
@Component
public class TenantPerformanceMetrics {private final MeterRegistry meterRegistry;private final Map<String, Timer> tenantTimers = new ConcurrentHashMap<>();public TenantPerformanceMetrics(MeterRegistry meterRegistry) {this.meterRegistry = meterRegistry;}public void recordRequest(String tenantId, String endpoint, Duration duration) {Timer timer = tenantTimers.computeIfAbsent(tenantId, id -> Timer.builder("tenant.request.duration").tag("tenant", id).register(meterRegistry));timer.record(duration);// 记录租户特定指标Counter.builder("tenant.request.count").tag("tenant", tenantId).tag("endpoint", endpoint).register(meterRegistry).increment();}public void recordError(String tenantId, String errorType) {Counter.builder("tenant.error.count").tag("tenant", tenantId).tag("error.type", errorType).register(meterRegistry).increment();}
}
租户资源限制
租户资源限制器
@Component
public class TenantResourceLimiter {private final Map<String, AtomicInteger> tenantRequestCounts = new ConcurrentHashMap<>();private final Map<String, Long> tenantLastReset = new ConcurrentHashMap<>();public boolean isWithinLimits(String tenantId, int maxRequestsPerMinute) {long currentTime = System.currentTimeMillis();long oneMinuteAgo = currentTime - 60000;// 重置计数器if (tenantLastReset.getOrDefault(tenantId, 0L) < oneMinuteAgo) {tenantRequestCounts.put(tenantId, new AtomicInteger(0));tenantLastReset.put(tenantId, currentTime);}AtomicInteger count = tenantRequestCounts.computeIfAbsent(tenantId, k -> new AtomicInteger(0));return count.incrementAndGet() <= maxRequestsPerMinute;}public void recordRequest(String tenantId) {tenantRequestCounts.computeIfAbsent(tenantId, k -> new AtomicInteger(0)).incrementAndGet();}
}
租户缓存预热
租户缓存预热服务
@Service
public class TenantCacheWarmupService {@Autowiredprivate TenantCacheService cacheService;@Autowiredprivate UserService userService;@EventListenerpublic void handleTenantActivation(TenantActivatedEvent event) {String tenantId = event.getTenantId();// 预热用户数据List<User> users = userService.findAll();cacheService.put("users", users);// 预热配置数据Map<String, String> configs = loadTenantConfigs(tenantId);cacheService.put("configs", configs);}private Map<String, String> loadTenantConfigs(String tenantId) {// 加载租户配置return new HashMap<>();}
}
最佳实践
1. 租户隔离最佳实践
数据隔离原则
// ✅ 好的实践:始终检查租户ID
@Service
public class UserService {public User findById(Long id) {String tenantId = TenantContext.getCurrentTenant();return userRepository.findByTenantIdAndId(tenantId, id).orElseThrow(() -> new UserNotFoundException(id));}
}// ❌ 不好的实践:忽略租户隔离
@Service
public class BadUserService {public User findById(Long id) {return userRepository.findById(id) // 可能返回其他租户的数据.orElseThrow(() -> new UserNotFoundException(id));}
}
2. 性能优化最佳实践
连接池配置
spring:datasource:hikari:maximum-pool-size: 20minimum-idle: 5connection-timeout: 30000idle-timeout: 600000max-lifetime: 1800000
缓存策略
@Service
public class OptimizedUserService {@Cacheable(value = "users", key = "#tenantId + ':' + #id")public User findById(String tenantId, Long id) {return userRepository.findByTenantIdAndId(tenantId, id).orElseThrow(() -> new UserNotFoundException(id));}@CacheEvict(value = "users", key = "#tenantId + ':' + #user.id")public User updateUser(String tenantId, User user) {return userRepository.save(user);}
}
3. 安全最佳实践
租户验证
@Component
public class TenantValidator {public void validateTenantAccess(String tenantId, String resourceId) {String currentTenant = TenantContext.getCurrentTenant();if (!currentTenant.equals(tenantId)) {throw new TenantAccessDeniedException("Access denied: tenant mismatch");}}public void validateResourceOwnership(String tenantId, String resourceId) {// 验证资源是否属于当前租户if (!isResourceOwnedByTenant(resourceId, tenantId)) {throw new ResourceAccessDeniedException("Resource does not belong to tenant");}}
}
常见问题解决
1. 租户上下文丢失
问题描述:异步操作中租户上下文丢失
解决方案:
@Service
public class AsyncTenantService {@Asyncpublic CompletableFuture<Void> processAsync(String tenantId) {// 手动设置租户上下文TenantContext.setCurrentTenant(tenantId);try {// 执行业务逻辑doProcess();return CompletableFuture.completedFuture(null);} finally {TenantContext.clear();}}
}
2. 缓存污染
问题描述:不同租户数据在缓存中混合
解决方案:
@Component
public class TenantAwareCacheManager {public void put(String key, Object value) {String tenantId = TenantContext.getCurrentTenant();String tenantKey = tenantId + ":" + key;cacheManager.getCache("default").put(tenantKey, value);}public Object get(String key) {String tenantId = TenantContext.getCurrentTenant();String tenantKey = tenantId + ":" + key;return cacheManager.getCache("default").get(tenantKey, Object.class);}
}
3. 事务回滚问题
问题描述:多租户事务回滚影响其他租户
解决方案:
@Service
@Transactional
public class TenantAwareService {@Transactional(rollbackFor = Exception.class)public void processTenantData(String tenantId) {try {TenantContext.setCurrentTenant(tenantId);// 执行业务逻辑doProcess();} catch (Exception e) {// 只回滚当前租户的数据throw new TenantProcessingException("Failed to process tenant data", e);} finally {TenantContext.clear();}}
}
总结
Spring MVC多租户架构与数据隔离是现代SaaS应用的核心技术。通过合理的设计和实现,可以实现:
核心优势:
- 数据安全:租户间数据完全隔离
- 性能优化:共享基础设施,降低成本
- 可扩展性:支持大量租户并发访问
- 维护便利:统一代码库,集中管理
关键技术点:
- 租户识别与路由机制
- 数据隔离策略选择
- 缓存策略与租户隔离
- 安全与权限控制
- 性能监控与优化
实施建议:
- 根据业务需求选择合适的隔离策略
- 建立完善的租户管理机制
- 实施严格的安全控制
- 持续监控和优化性能
- 制定详细的运维规范
通过本教程的学习,小伙伴们将掌握Spring MVC多租户架构的设计与实现,为构建企业级SaaS应用奠定坚实基础。