文章目录
目录
- 多租户架构概述
- 多租户数据隔离策略
- 租户识别与路由机制
- [Spring MVC多租户实现](#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依赖配置
xml
<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>
多数据源配置
java
@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();
}
@Bean
public 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管理
java
@Component
public class TenantSchemaManager {
@Autowired
private JdbcTemplate jdbcTemplate;
public void createTenantSchema(String tenantId) {
String schemaName = "tenant_" + tenantId;
// 创建租户Schema
jdbcTemplate.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);
}
}
行级隔离实现
租户上下文管理
java
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();
}
}
租户实体基类
java
@MappedSuperclass
public abstract class TenantAwareEntity {
@Column(name = "tenant_id", nullable = false)
private String tenantId;
@PrePersist
protected void onCreate() {
if (tenantId == null) {
tenantId = TenantContext.getCurrentTenant();
}
}
// getter and setter
public String getTenantId() {
return tenantId;
}
public void setTenantId(String tenantId) {
this.tenantId = tenantId;
}
}
租户识别与路由机制
子域名识别
租户解析器
java
@Component
public class SubdomainTenantResolver implements TenantResolver {
@Override
public String resolveTenant(HttpServletRequest request) {
String serverName = request.getServerName();
// 解析子域名:tenant1.example.com -> tenant1
if (serverName.contains(".")) {
String[] parts = serverName.split("\\.");
if (parts.length >= 3) {
return parts[0];
}
}
return "default";
}
}
URL路径识别
路径租户解析器
java
@Component
public class PathTenantResolver implements TenantResolver {
@Override
public String resolveTenant(HttpServletRequest request) {
String requestURI = request.getRequestURI();
// 解析路径:/tenant/tenant1/users -> tenant1
Pattern pattern = Pattern.compile("^/tenant/([^/]+)/");
Matcher matcher = pattern.matcher(requestURI);
if (matcher.find()) {
return matcher.group(1);
}
return "default";
}
}
请求头识别
Header租户解析器
java
@Component
public class HeaderTenantResolver implements TenantResolver {
private static final String TENANT_HEADER = "X-Tenant-ID";
@Override
public String resolveTenant(HttpServletRequest request) {
String tenantId = request.getHeader(TENANT_HEADER);
return tenantId != null ? tenantId : "default";
}
}
租户拦截器
租户上下文拦截器
java
@Component
public class TenantContextInterceptor implements HandlerInterceptor {
@Autowired
private List<TenantResolver> tenantResolvers;
@Override
public 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;
}
@Override
public 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多租户实现
租户配置管理
租户配置实体
java
@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
}
租户配置服务
java
@Service
public class TenantConfigService {
@Autowired
private 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);
}
}
多租户控制器
租户感知控制器
java
@RestController
@RequestMapping("/api/tenant")
public class TenantAwareController {
@Autowired
private UserService userService;
@Autowired
private 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);
}
}
租户数据源路由
动态数据源路由
java
@Component
public class TenantDataSourceRouter {
@Autowired
private 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
java
@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实现
java
@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租户过滤器
java
@Component
public class TenantFilter {
@EventListener
public void handleTenantFilter(EntityManagerFactory emf) {
EntityManager em = emf.createEntityManager();
// 设置租户过滤条件
String tenantId = TenantContext.getCurrentTenant();
if (tenantId != null) {
em.setProperty("tenant.id", tenantId);
}
}
}
Hibernate过滤器配置
java
@Entity
@FilterDef(name = "tenantFilter", parameters = @ParamDef(name = "tenantId", type = "string"))
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
public class User extends TenantAwareEntity {
// entity fields
}
多租户事务管理
租户事务管理器
java
@Configuration
@EnableTransactionManagement
public class TenantTransactionConfig {
@Bean
public PlatformTransactionManager tenantTransactionManager() {
return new DataSourceTransactionManager(tenantDataSource());
}
@Bean
public TransactionTemplate tenantTransactionTemplate() {
return new TransactionTemplate(tenantTransactionManager());
}
}
缓存策略与租户隔离
租户缓存配置
Redis租户缓存配置
java
@Configuration
@EnableCaching
public class TenantCacheConfig {
@Bean
public 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()));
}
}
租户缓存键策略
租户缓存键生成器
java
@Component
public class TenantCacheKeyGenerator implements KeyGenerator {
@Override
public 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();
}
}
缓存服务实现
租户缓存服务
java
@Service
public class TenantCacheService {
@Autowired
private 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);
}
}
}
安全与权限控制
租户权限管理
租户权限实体
java
@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
}
租户权限服务
java
@Service
public class TenantPermissionService {
@Autowired
private 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() {
// 从安全上下文获取当前用户ID
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof UserDetails) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return Long.valueOf(userDetails.getUsername());
}
return null;
}
}
租户安全拦截器
租户权限拦截器
java
@Component
public class TenantSecurityInterceptor implements HandlerInterceptor {
@Autowired
private TenantPermissionService permissionService;
@Override
public 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();
}
}
性能优化与监控
租户性能监控
租户性能指标
java
@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();
}
}
租户资源限制
租户资源限制器
java
@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();
}
}
租户缓存预热
租户缓存预热服务
java
@Service
public class TenantCacheWarmupService {
@Autowired
private TenantCacheService cacheService;
@Autowired
private UserService userService;
@EventListener
public 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. 租户隔离最佳实践
数据隔离原则
java
// ✅ 好的实践:始终检查租户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. 性能优化最佳实践
连接池配置
yaml
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
缓存策略
java
@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. 安全最佳实践
租户验证
java
@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. 租户上下文丢失
问题描述:异步操作中租户上下文丢失
解决方案:
java
@Service
public class AsyncTenantService {
@Async
public CompletableFuture<Void> processAsync(String tenantId) {
// 手动设置租户上下文
TenantContext.setCurrentTenant(tenantId);
try {
// 执行业务逻辑
doProcess();
return CompletableFuture.completedFuture(null);
} finally {
TenantContext.clear();
}
}
}
2. 缓存污染
问题描述:不同租户数据在缓存中混合
解决方案:
java
@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. 事务回滚问题
问题描述:多租户事务回滚影响其他租户
解决方案:
java
@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应用奠定坚实基础。