在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系统的快速上线。
Comments