🏢 多租户系统:一屋多户的魔法隔断术

61 阅读11分钟

知识点编号:266
难度系数:⭐⭐⭐⭐⭐
实用指数:💯💯💯💯💯


📖 开篇:一个房东的智慧

想象你是一个房东,有一栋10层的大楼 🏢。现在有三种出租方式:

🏠 方案A:独栋别墅(独立部署)

每个租客都给他盖一栋独立别墅。

  • 优点:完全隔离,互不干扰 ✅
  • 缺点:成本高,维护累,土地不够用 ❌

🏘️ 方案B:公寓楼(多租户)

把一栋楼分成多个房间,每个租客一间。

  • 优点:成本低,统一管理,资源利用率高 ✅
  • 缺点:需要做好隔离 ⚠️

🏨 方案C:胶囊旅馆(资源共享)

所有人睡一个大通铺。

  • 优点:成本最低 💰
  • 缺点:隐私?不存在的!❌

多租户系统就是方案B:在一套系统中服务多个客户(租户),既节约成本,又保证隔离!🎯


🎯 什么是多租户系统?

定义 📚

多租户(Multi-Tenant) 是一种软件架构,一套系统实例可以同时为多个客户(租户) 提供服务,每个租户的数据和配置相互隔离。

生活中的多租户 🌍

场景说明多租户体现
云盘 📦百度网盘、阿里云盘一套系统,每个用户独立空间
SaaS系统 💼钉钉、企业微信、Salesforce一套代码,服务成千上万企业
电商平台 🛒拼多多、淘宝一个平台,无数商家开店
在线教育 📚腾讯课堂一套系统,每个机构独立管理

🏗️ 多租户的三种隔离方案

┌──────────────────────────────────────────────────────────┐
│              多租户数据隔离三大流派                         │
└──────────────────────────────────────────────────────────┘

方案1: 独立数据库(Database Per Tenant)
┌─────────┐  ┌─────────┐  ┌─────────┐
│ 租户A   │  │ 租户B   │  │ 租户C   │
│ DB_A    │  │ DB_B    │  │ DB_C    │
└─────────┘  └─────────┘  └─────────┘

方案2: 共享数据库,独立Schema(Schema Per Tenant)
┌────────────────────────────────┐
│         数据库实例              │
│  ┌────────┐ ┌────────┐        │
│  │Schema A│ │Schema B│ ...    │
│  └────────┘ └────────┘        │
└────────────────────────────────┘

方案3: 共享数据库+Schema(Shared Database)
┌────────────────────────────────┐
│         数据库实例              │
│  ┌──────────────────────┐     │
│  │ user表                │     │
│  │ id | name | tenant_id│     │
│  │ 1  | 张三 | A         │     │
│  │ 2  | 李四 | B         │     │
│  └──────────────────────┘     │
└────────────────────────────────┘

🎨 方案一:独立数据库(壕无人性版)

架构图

                应用层
         ┌───────────────┐
         │  统一入口      │
         │  (路由分发)    │
         └───────┬───────┘
                 │
        ┌────────┴────────┐
        │  数据源路由器    │
        └────┬────┬───┬───┘
             │    │   │
      ┌──────┘    │   └──────┐
      ↓           ↓          ↓
  ┌───────┐  ┌───────┐  ┌───────┐
  │DB_A   │  │DB_B   │  │DB_C   │
  │       │  │       │  │       │
  │租户A  │  │租户B  │  │租户C  │
  └───────┘  └───────┘  └───────┘

生活比喻 🏘️

每个租客都有自己的独立别墅,想怎么装修就怎么装修,互不影响。

代码实现

1. 配置多数据源

# application.yml
spring:
  datasource:
    tenants:
      tenant-a:
        url: jdbc:mysql://localhost:3306/tenant_a
        username: root
        password: 123456
      tenant-b:
        url: jdbc:mysql://localhost:3306/tenant_b
        username: root
        password: 123456
      tenant-c:
        url: jdbc:mysql://localhost:3306/tenant_c
        username: root
        password: 123456

2. 动态数据源配置

@Configuration
public class DynamicDataSourceConfig {
    
    @Bean
    public DataSource dynamicDataSource() {
        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        
        // 默认数据源
        dataSource.setDefaultTargetDataSource(tenantADataSource());
        
        // 租户数据源映射
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("tenant-a", tenantADataSource());
        targetDataSources.put("tenant-b", tenantBDataSource());
        targetDataSources.put("tenant-c", tenantCDataSource());
        
        dataSource.setTargetDataSources(targetDataSources);
        return dataSource;
    }
    
    private DataSource tenantADataSource() {
        return DataSourceBuilder.create()
            .url("jdbc:mysql://localhost:3306/tenant_a")
            .username("root")
            .password("123456")
            .build();
    }
    
    // tenantBDataSource()、tenantCDataSource() 类似...
}

3. 动态路由数据源

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        // 从上下文中获取当前租户ID
        return TenantContext.getCurrentTenant();
    }
}

4. 租户上下文

public class TenantContext {
    
    private static final ThreadLocal<String> TENANT_HOLDER = new ThreadLocal<>();
    
    public static void setCurrentTenant(String tenantId) {
        TENANT_HOLDER.set(tenantId);
    }
    
    public static String getCurrentTenant() {
        return TENANT_HOLDER.get();
    }
    
    public static void clear() {
        TENANT_HOLDER.remove();
    }
}

5. 租户拦截器

@Component
public class TenantInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) {
        // 从请求头获取租户ID
        String tenantId = request.getHeader("X-Tenant-Id");
        
        if (StringUtils.isEmpty(tenantId)) {
            // 或者从JWT Token中解析
            tenantId = getTenantFromToken(request);
        }
        
        // 设置到上下文
        TenantContext.setCurrentTenant(tenantId);
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, 
                               HttpServletResponse response, 
                               Object handler, 
                               Exception ex) {
        // 清理上下文
        TenantContext.clear();
    }
    
    private String getTenantFromToken(HttpServletRequest request) {
        String token = request.getHeader("Authorization");
        // 解析JWT获取tenantId
        return JwtUtil.getTenantId(token);
    }
}

6. 业务代码(无感知)

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    public List<User> getUsers() {
        // 自动路由到对应租户的数据库
        return userMapper.selectList(null);
    }
    
    public void addUser(User user) {
        // 写入当前租户的数据库
        userMapper.insert(user);
    }
}

优缺点分析

维度评价说明
隔离性⭐⭐⭐⭐⭐物理隔离,最安全
性能⭐⭐⭐⭐⭐互不影响
扩展性⭐⭐⭐⭐可独立扩展某个租户
成本⭐⭐数据库实例多,成本高
维护⭐⭐升级、备份麻烦
个性化⭐⭐⭐⭐⭐可定制表结构
适用场景💰大客户、高安全要求

🎪 方案二:独立Schema(中产阶级版)

架构图

       应用层
  ┌──────────────┐
  │  统一入口     │
  └──────┬───────┘
         │
    ┌────┴────┐
    │ DB实例  │
    │ ┌──────┴───────┐
    │ │ Schema A     │
    │ │  - user      │
    │ │  - order     │
    │ └──────────────┘
    │ ┌──────────────┐
    │ │ Schema B     │
    │ │  - user      │
    │ │  - order     │
    │ └──────────────┘
    └──────────────────┘

生活比喻 🏢

一栋楼里的不同楼层,每层都有独立的房间布局。

代码实现

1. 动态Schema切换

public class SchemaRoutingDataSource extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        String tenantId = TenantContext.getCurrentTenant();
        // 每个租户对应一个Schema
        return "schema_" + tenantId;
    }
    
    @Override
    public Connection getConnection() throws SQLException {
        Connection connection = super.getConnection();
        String schema = determineCurrentLookupKey().toString();
        
        // 切换Schema
        connection.setSchema(schema);
        // 或者执行SQL: USE schema_xxx
        
        return connection;
    }
}

2. MyBatis拦截器方式

@Intercepts({
    @Signature(type = Executor.class, method = "query", 
               args = {MappedStatement.class, Object.class, 
                       RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "update", 
               args = {MappedStatement.class, Object.class})
})
public class TenantSchemaInterceptor implements Interceptor {
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取当前租户
        String tenantId = TenantContext.getCurrentTenant();
        
        // 切换Schema
        Connection connection = getConnection(invocation);
        String schema = "tenant_" + tenantId;
        connection.createStatement().execute("USE " + schema);
        
        // 执行原SQL
        return invocation.proceed();
    }
}

3. 租户初始化

@Service
public class TenantInitService {
    
    @Autowired
    private DataSource dataSource;
    
    /**
     * 新租户注册,创建独立Schema
     */
    public void createTenant(String tenantId) throws SQLException {
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement()) {
            
            // 1. 创建Schema
            String schema = "tenant_" + tenantId;
            stmt.execute("CREATE SCHEMA " + schema);
            
            // 2. 切换到新Schema
            stmt.execute("USE " + schema);
            
            // 3. 创建表结构
            String[] initSqls = loadInitSql();
            for (String sql : initSqls) {
                stmt.execute(sql);
            }
            
            log.info("✅ 租户{}初始化成功!", tenantId);
        }
    }
    
    private String[] loadInitSql() {
        // 从文件或资源加载建表SQL
        return new String[]{
            "CREATE TABLE user (...)",
            "CREATE TABLE order (...)",
            // ...
        };
    }
}

优缺点分析

维度评价说明
隔离性⭐⭐⭐⭐Schema级别隔离
性能⭐⭐⭐⭐共享连接池,略有影响
扩展性⭐⭐⭐单实例有上限
成本⭐⭐⭐共享DB实例,成本适中
维护⭐⭐⭐统一升级,但Schema多
个性化⭐⭐⭐⭐可定制表结构
适用场景🏢中型企业,平衡之选

🎯 方案三:共享表+租户字段(经济适用版)

架构图

       应用层
  ┌──────────────┐
  │  统一入口     │
  └──────┬───────┘
         │
    ┌────┴────┐
    │ 数据库  │
    │         │
    │ ┌──────────────────────┐
    │ │ user表                │
    │ ├──────────────────────┤
    │ │ id | name | tenant_id│
    │ │ 1  | 张三 | A         │
    │ │ 2  | 李四 | A         │
    │ │ 3  | 王五 | B         │
    │ └──────────────────────┘
    └──────────────────────────┘

生活比喻 🏨

一个大通铺,但每个人的床位贴了名字标签。

数据库设计

1. 表结构设计

-- 用户表
CREATE TABLE user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL,
    password VARCHAR(100) NOT NULL,
    email VARCHAR(100),
    tenant_id VARCHAR(50) NOT NULL,  -- 租户ID ⭐
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    KEY idx_tenant (tenant_id),      -- 必须建索引!
    KEY idx_tenant_user (tenant_id, username)
);

-- 订单表
CREATE TABLE `order` (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(50) NOT NULL,
    user_id BIGINT NOT NULL,
    amount DECIMAL(10,2),
    tenant_id VARCHAR(50) NOT NULL,  -- 租户ID ⭐
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    KEY idx_tenant (tenant_id),
    KEY idx_tenant_order (tenant_id, order_no)
);

-- 所有业务表都要加 tenant_id 字段!

代码实现

1. 实体类设计

@Data
public class BaseEntity {
    @TableField(fill = FieldFill.INSERT)
    private String tenantId;  // 自动填充
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

@Data
@TableName("user")
public class User extends BaseEntity {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String username;
    private String password;
    private String email;
}

2. MyBatis-Plus自动填充

@Component
public class TenantMetaObjectHandler implements MetaObjectHandler {
    
    @Override
    public void insertFill(MetaObject metaObject) {
        // 插入时自动填充租户ID
        String tenantId = TenantContext.getCurrentTenant();
        this.strictInsertFill(metaObject, "tenantId", String.class, tenantId);
    }
    
    @Override
    public void updateFill(MetaObject metaObject) {
        // 更新不需要改租户ID
    }
}

3. MyBatis-Plus租户插件(自动拼接SQL)

@Configuration
public class MybatisPlusConfig {
    
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        
        // 添加租户插件
        TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();
        
        // 租户处理器
        tenantInterceptor.setTenantLineHandler(new TenantLineHandler() {
            @Override
            public Expression getTenantId() {
                // 当前租户ID
                String tenantId = TenantContext.getCurrentTenant();
                return new StringValue(tenantId);
            }
            
            @Override
            public String getTenantIdColumn() {
                // 租户字段名
                return "tenant_id";
            }
            
            @Override
            public boolean ignoreTable(String tableName) {
                // 哪些表不需要租户隔离
                return "tenant".equals(tableName) 
                    || "sys_config".equals(tableName);
            }
        });
        
        interceptor.addInnerInterceptor(tenantInterceptor);
        return interceptor;
    }
}

4. 业务代码(完全无感)

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    public List<User> getUsers() {
        // 原始SQL: SELECT * FROM user
        // 自动改写: SELECT * FROM user WHERE tenant_id = 'A'
        return userMapper.selectList(null);
    }
    
    public User getUserById(Long id) {
        // 原始SQL: SELECT * FROM user WHERE id = 1
        // 自动改写: SELECT * FROM user WHERE id = 1 AND tenant_id = 'A'
        return userMapper.selectById(id);
    }
    
    public void addUser(User user) {
        // 自动填充tenant_id
        userMapper.insert(user);
    }
}

5. SQL改写示例

改写前

SELECT * FROM user WHERE username = 'zhangsan';

改写后

SELECT * FROM user 
WHERE username = 'zhangsan' 
AND tenant_id = 'tenant-a';  ← 自动添加

复杂查询改写

-- 改写前
SELECT u.*, o.order_no 
FROM user u 
LEFT JOIN `order` o ON u.id = o.user_id
WHERE u.status = 1;

-- 改写后
SELECT u.*, o.order_no 
FROM user u 
LEFT JOIN `order` o ON u.id = o.user_id 
    AND o.tenant_id = 'tenant-a'  ← 自动添加
WHERE u.status = 1 
AND u.tenant_id = 'tenant-a';     ← 自动添加

优缺点分析

维度评价说明
隔离性⭐⭐逻辑隔离,有风险
性能⭐⭐⭐大租户影响小租户
扩展性⭐⭐数据量大后分表困难
成本⭐⭐⭐⭐⭐成本最低
维护⭐⭐⭐⭐⭐统一升级,简单
个性化无法定制
适用场景🏢小微企业,SaaS启动

🔐 多租户的安全问题

问题1:租户越权访问 🚨

场景:租户A的用户访问到租户B的数据!

攻击方式

# 正常请求
curl -H "X-Tenant-Id: tenant-a" \
     http://api.com/users/1

# 恶意请求(修改租户ID)
curl -H "X-Tenant-Id: tenant-b" \  ← 伪造租户ID
     http://api.com/users/1

解决方案

方案A:JWT中包含租户ID

public class JwtUtil {
    
    public static String generateToken(String userId, String tenantId) {
        return JWT.create()
            .withClaim("userId", userId)
            .withClaim("tenantId", tenantId)  // 租户ID写入Token
            .withExpiresAt(new Date(System.currentTimeMillis() + 3600000))
            .sign(Algorithm.HMAC256(SECRET));
    }
    
    public static String getTenantId(String token) {
        DecodedJWT jwt = JWT.require(Algorithm.HMAC256(SECRET))
            .build()
            .verify(token);
        return jwt.getClaim("tenantId").asString();
    }
}

@Component
public class TenantInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                            HttpServletResponse response, 
                            Object handler) {
        String token = request.getHeader("Authorization");
        
        // 从Token解析租户ID(不相信Header)
        String tenantId = JwtUtil.getTenantId(token);
        
        // Header中的租户ID(用户传的)
        String headerTenantId = request.getHeader("X-Tenant-Id");
        
        // 校验一致性
        if (!tenantId.equals(headerTenantId)) {
            throw new SecurityException("租户ID不匹配!");
        }
        
        TenantContext.setCurrentTenant(tenantId);
        return true;
    }
}

方案B:服务端验证

@Service
public class UserService {
    
    public User getUserById(Long id) {
        User user = userMapper.selectById(id);
        
        // 二次校验租户ID
        String currentTenant = TenantContext.getCurrentTenant();
        if (!currentTenant.equals(user.getTenantId())) {
            throw new SecurityException("无权访问该数据!");
        }
        
        return user;
    }
}

问题2:SQL注入绕过租户过滤

攻击SQL

-- 恶意构造的条件
WHERE id = 1 OR tenant_id = 'tenant-b'

解决方案:使用预编译,永远不要拼接SQL!

// ❌ 错误示范
String sql = "SELECT * FROM user WHERE id = " + id;

// ✅ 正确做法
String sql = "SELECT * FROM user WHERE id = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setLong(1, id);

🎨 多租户的高级特性

1️⃣ 租户个性化配置

@Data
@TableName("tenant_config")
public class TenantConfig {
    @TableId
    private String tenantId;
    
    // 个性化配置
    private String logo;              // 租户Logo
    private String primaryColor;      // 主题色
    private Integer maxUsers;         // 最大用户数
    private Boolean enableSms;        // 是否开通短信
    private String customDomain;      // 自定义域名
    
    // 功能开关
    private JSONObject features;      // {"报表": true, "导出": false}
}

@Service
public class TenantConfigService {
    
    @Autowired
    private TenantConfigMapper configMapper;
    
    @Cacheable(value = "tenant-config", key = "#tenantId")
    public TenantConfig getConfig(String tenantId) {
        return configMapper.selectById(tenantId);
    }
    
    public boolean isFeatureEnabled(String featureName) {
        String tenantId = TenantContext.getCurrentTenant();
        TenantConfig config = getConfig(tenantId);
        
        JSONObject features = config.getFeatures();
        return features.getBooleanValue(featureName);
    }
}

// 业务中使用
@RestController
public class ReportController {
    
    @Autowired
    private TenantConfigService configService;
    
    @GetMapping("/report/export")
    public void export() {
        // 检查租户是否开通导出功能
        if (!configService.isFeatureEnabled("导出")) {
            throw new BizException("您的套餐不支持此功能");
        }
        
        // 导出逻辑...
    }
}

2️⃣ 租户级别的数据统计

@Service
public class TenantStatService {
    
    /**
     * 统计租户使用情况
     */
    public TenantStat getStat(String tenantId) {
        TenantStat stat = new TenantStat();
        
        // 切换到对应租户
        TenantContext.setCurrentTenant(tenantId);
        
        try {
            // 用户数
            stat.setUserCount(userMapper.selectCount(null));
            
            // 订单数
            stat.setOrderCount(orderMapper.selectCount(null));
            
            // 存储空间
            stat.setStorageUsed(calculateStorage(tenantId));
            
            // API调用次数
            stat.setApiCalls(getApiCalls(tenantId));
            
            return stat;
        } finally {
            TenantContext.clear();
        }
    }
}

3️⃣ 租户生命周期管理

@Service
public class TenantLifecycleService {
    
    /**
     * 创建租户
     */
    @Transactional
    public void createTenant(TenantCreateDTO dto) {
        // 1. 创建租户记录
        Tenant tenant = new Tenant();
        tenant.setTenantId(generateTenantId());
        tenant.setName(dto.getName());
        tenant.setStatus(TenantStatus.ACTIVE);
        tenant.setExpireTime(calculateExpireTime(dto.getPackageType()));
        tenantMapper.insert(tenant);
        
        // 2. 初始化数据库(如果是独立DB方案)
        if (isIndependentDbMode()) {
            tenantInitService.createTenantDatabase(tenant.getTenantId());
        }
        
        // 3. 创建管理员账号
        createAdminUser(tenant.getTenantId(), dto.getAdminEmail());
        
        // 4. 初始化默认配置
        initDefaultConfig(tenant.getTenantId());
        
        log.info("✅ 租户{}创建成功", tenant.getTenantId());
    }
    
    /**
     * 冻结租户(欠费)
     */
    public void freezeTenant(String tenantId) {
        Tenant tenant = tenantMapper.selectById(tenantId);
        tenant.setStatus(TenantStatus.FROZEN);
        tenantMapper.updateById(tenant);
        
        // 清除缓存
        cacheManager.getCache("tenant-config").evict(tenantId);
        
        log.warn("⚠️ 租户{}已被冻结", tenantId);
    }
    
    /**
     * 删除租户
     */
    @Transactional
    public void deleteTenant(String tenantId) {
        // 1. 备份数据
        backupTenantData(tenantId);
        
        // 2. 删除业务数据
        TenantContext.setCurrentTenant(tenantId);
        try {
            userMapper.delete(null);    // 删除所有用户
            orderMapper.delete(null);   // 删除所有订单
            // ... 删除其他业务数据
        } finally {
            TenantContext.clear();
        }
        
        // 3. 删除租户记录
        tenantMapper.deleteById(tenantId);
        
        // 4. 删除数据库(独立DB方案)
        if (isIndependentDbMode()) {
            dropTenantDatabase(tenantId);
        }
        
        log.info("✅ 租户{}已删除", tenantId);
    }
}

🚀 性能优化

1️⃣ 租户配置缓存

@Configuration
@EnableCaching
public class CacheConfig {
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1))  // 缓存1小时
            .serializeValuesWith(RedisSerializationContext
                .SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        
        return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .build();
    }
}

@Service
public class TenantConfigService {
    
    @Cacheable(value = "tenant-config", key = "#tenantId")
    public TenantConfig getConfig(String tenantId) {
        return configMapper.selectById(tenantId);
    }
    
    @CacheEvict(value = "tenant-config", key = "#tenantId")
    public void updateConfig(String tenantId, TenantConfig config) {
        configMapper.updateById(config);
    }
}

2️⃣ 连接池隔离(防止大租户占满连接)

@Configuration
public class DataSourceConfig {
    
    @Bean
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/db");
        config.setUsername("root");
        config.setPassword("123456");
        
        // 总连接池大小
        config.setMaximumPoolSize(100);
        
        // 每个租户最多占用20个连接(需自定义实现)
        config.addDataSourceProperty("maxConnectionsPerTenant", 20);
        
        return new HikariDataSource(config);
    }
}

3️⃣ 索引优化

-- ❌ 错误:只有tenant_id索引
CREATE INDEX idx_tenant ON user(tenant_id);

-- 查询时:
SELECT * FROM user WHERE tenant_id = 'A' AND username = 'zhangsan';
-- 索引只用到tenant_id,username仍需全表扫描

-- ✅ 正确:联合索引
CREATE INDEX idx_tenant_username ON user(tenant_id, username);

-- 查询时:
SELECT * FROM user WHERE tenant_id = 'A' AND username = 'zhangsan';
-- 完美利用索引,快如闪电!⚡

🎓 面试题解析

Q1:如何防止租户之间的数据泄露?

  1. Token绑定租户ID:不信任客户端传参
  2. SQL自动改写:MyBatis-Plus租户插件
  3. 二次校验:关键数据访问时再次验证
  4. 审计日志:记录跨租户访问尝试
  5. 定期巡检:检查是否有tenant_id为空的脏数据

Q2:大租户影响小租户性能怎么办?

  1. 独立数据库:大客户升级到独立DB
  2. 读写分离:大租户走从库
  3. 限流:每个租户QPS限制
  4. 降级:大租户高峰期降级非核心功能
  5. 分库分表:按租户维度分片

Q3:租户数据如何迁移?

@Service
public class TenantMigrationService {
    
    /**
     * 从共享表迁移到独立DB
     */
    public void migrateToIndependentDb(String tenantId) {
        // 1. 创建目标数据库
        createTargetDatabase(tenantId);
        
        // 2. 导出数据
        TenantContext.setCurrentTenant(tenantId);
        List<User> users = userMapper.selectList(null);
        List<Order> orders = orderMapper.selectList(null);
        // ...
        
        // 3. 导入目标库
        DataSource targetDs = getTargetDataSource(tenantId);
        importData(targetDs, users, orders);
        
        // 4. 数据校验
        if (verifyData(tenantId)) {
            // 5. 切换数据源配置
            updateTenantConfig(tenantId, "DB_MODE", "INDEPENDENT");
            
            // 6. 清理原数据
            cleanupOldData(tenantId);
        }
    }
}

📝 总结

三种方案选择指南

┌─────────────────────────────────────────────────────┐
│              租户隔离方案选型决策树                  │
└─────────────────────────────────────────────────────┘

                    租户数量?
                        │
        ┌───────────────┼───────────────┐
        ↓               ↓               ↓
      < 10            10-100          > 100
        │               │               │
    独立数据库      独立Schema       共享表
    (方案1)         (方案2)         (方案3)
        │               │               │
    成本高           成本中           成本低
    隔离好           隔离好           隔离弱
    可定制           可定制           难定制

核心要点 🎯

  1. 隔离三件套

    • 数据隔离(核心)
    • 功能隔离(配置)
    • 性能隔离(限流)
  2. 安全第一

    • Token绑定租户
    • SQL自动改写
    • 定期巡检
  3. 渐进式架构

    • 起步:共享表(方案3)
    • 成长:独立Schema(方案2)
    • 成熟:混合模式(大客户独立DB)

加油,SaaS创业者! 🚀🚀🚀