在SaaS系统的开发中,多租户是为不同企业用户提供独立数据空间的核心设计,而CQRS读写分离则是为了应对这类系统读多写少场景的性能优化方案,两者结合后,如何兼顾租户数据隔离和数据安全,是我们今天要拆解的核心内容。

一、为什么多租户+CQRS是现在系统的刚需?

现在市面上的SaaS系统,基本都要服务不同企业用户,这些企业的业务不能互相干扰,比如A公司的订单数据不能让B公司看到,这就是多租户要解决的问题。而这类系统的典型特点是“读多写少”:比如员工查看报表、浏览项目数据是高频读请求,每天可能几十万次;创建项目、修改订单是低频写请求,每天可能只有几千次。CQRS读写分离就是把读和写的请求分开,读请求发给性能更好的从库,写请求发给高可靠的主库,这样既提升了系统吞吐量,又降低了数据库成本,两者结合刚好匹配SaaS系统的核心需求。

二、核心实现思路拆解

2.1 多租户隔离的基础逻辑

多租户隔离的核心其实很简单:给每条业务数据加一个专属的“租户标签”,也就是tenant_id,不管是查询还是插入数据,都必须带上这个标签,避免跨租户数据泄露。我们不需要在每一个业务接口里都手动写where tenant_id=当前企业ID,可以用中间件自动帮我们完成,这样既减少了重复代码,又避免了遗漏条件的风险。

2.2 CQRS读写分离的落地方式

CQRS的读写分离,本质是把数据源分成两个:一个负责写的主库(master),一个负责读的从库(slave)。我们通过路由规则自动区分请求类型:写操作(新增、修改、删除)自动走主库,读操作(查询)自动走从库。这样读写操作不会互相影响,还可以通过横向扩展从库,支撑更高的读流量。

三、具体实现示例(技术栈:Java + Spring Boot + MyBatis-Plus)

3.1 环境准备与依赖引入

我们用Maven管理项目,需要引入Spring Web、MyBatis-Plus、动态数据源组件Druid这些依赖,配置如下:

<dependencies>
    <!-- Spring Web 基础依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- MyBatis-Plus 简化数据库操作 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.1</version>
    </dependency>
    <!-- 动态数据源支持读写分离 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
        <version>4.2.0</version>
    </dependency>
    <!-- Druid 数据库连接池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.16</version>
    </dependency>
</dependencies>

3.2 多租户自动隔离配置

这里用MyBatis-Plus的多租户插件,实现自动给SQL加tenant_id条件,配置类如下:

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// 多租户配置类
@Configuration
public class TenantConfig {

    // 配置租户拦截器,自动注入tenant_id
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加租户行级拦截器,自动处理tenant_id条件
        TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();
        // 设置租户处理器,从请求头获取当前租户ID
        tenantInterceptor.setTenantLineHandler(() -> {
            // 实际项目中可以从网关的请求头、或者当前登录用户的上下文中获取
            return "current_tenant_id";
        });
        interceptor.addInnerInterceptor(tenantInterceptor);
        return interceptor;
    }
}

3.3 读写分离数据源配置

配置主从两个数据源,写走主库,读走从库,配置文件如下:

spring:
  datasource:
    dynamic:
      primary: master # 默认写走主库
      datasource:
        # 主库(写库)配置
        master:
          url: jdbc:mysql://localhost:3306/db_master?useUnicode=true&characterEncoding=utf8
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        # 从库(读库)配置
        slave:
          url: jdbc:mysql://localhost:3306/db_slave?useUnicode=true&characterEncoding=utf8
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver

3.4 业务代码中的实际使用

在Service层通过注解指定数据源,实现读写分离,同时自动带上租户ID,示例代码如下:

import com.baomidou.dynamic.datasource.annotation.DS;
import org.springframework.stereotype.Service;

// 用户业务服务
@Service
public class UserService {

    // 查询用户(读操作,走从库)
    @DS("slave")
    public List<User> getUserList() {
        // MyBatis-Plus自动拼接tenant_id条件,不会查其他租户的数据
        return userMapper.selectList(null);
    }

    // 新增用户(写操作,走主库)
    @DS("master")
    public void addUser(User user) {
        userMapper.insert(user);
    }
}

四、数据安全风险的主要来源与防范措施

4.1 常见风险点

最核心的风险是跨租户数据泄露,比如恶意用户篡改请求头里的tenant_id,就能查到其他企业的数据;还有主从库同步延迟,刚写完的数据读库还没同步,可能读到旧数据;另外SQL注入可能绕过租户ID的自动条件,导致数据泄漏。

4.2 具体防范方法

首先要在网关层或者拦截器里校验tenant_id的合法性,只允许当前登录用户的租户ID,不能由前端随意修改;然后用半同步复制配置主从,减少数据延迟;还要开启MyBatis的预处理语句,防止SQL注入;另外日志里要脱敏,不记录tenant_id对应的业务数据,避免敏感信息泄露。

五、技术优缺点分析

这套架构的优点很明显:一是租户隔离性强,自动处理tenant_id,业务代码不用额外开发;二是读写分离提升性能,从库可以横向扩展,支撑高读流量;三是主库压力小,数据可靠性更高。缺点也存在:一是架构复杂度高,要维护多数据源和拦截逻辑,调试难度大;二是主从延迟的问题,对数据一致性要求高的场景不适合;三是运维成本增加,要同时维护主从库的监控和备份。

六、落地注意事项

首先要给tenant_id加数据库索引,不然查询会变慢;然后事务要注意,同一事务里的读操作不能走从库,不然会读到旧数据;还要监控主从延迟,延迟超过阈值要告警;另外超级管理员查询所有租户数据时,要跳过租户拦截,单独做权限控制,不能和普通租户共用逻辑。

七、总结

多租户系统结合CQRS读写分离,是现在SaaS系统的经典架构,核心是用中间件自动处理租户隔离和读写路由,减少业务代码的复杂度,同时提升系统性能。落地时要重点关注数据安全,从校验、同步、SQL防护几个方面入手,还要提前评估架构复杂度,适合中小SaaS系统的快速上线。