第五章:[高级] 企业级核心特性
第五章:[高级] 企业级核心特性
Prorise第五章:[架构] 多租户与数据架构实战
摘要: 跨越了基础的 CRUD 和单表插件后,本章将视角拉升至 系统架构 层面。我们将攻克企业级 SaaS 应用的三大核心难题:如何通过 多租户插件 实现行级数据隔离?如何利用 流式查询 解决百万级数据导出时的内存溢出(OOM)问题?以及如何通过 动态数据源 实现数据库的读写分离与分库分表。
本章学习路径
- SaaS 隔离:配置
TenantLineInnerInterceptor,实现无感知的行级多租户隔离。 - 内存救星:掌握 MyBatis 原生
ResultHandler流式查询,能够低内存处理百万级数据。 - 读写架构:集成
dynamic-datasource,通过注解灵活切换主从库,分摊数据库压力。
5.1. 多租户架构 (Multi-Tenancy)
痛点背景:在开发 SaaS(软件即服务)平台时,我们需要将一套系统卖给多个公司(租户)使用。数据存储通常有两种方案:
- 独立数据库:每个租户一个库,成本极高,维护困难。
- 共享数据库:所有租户共用一张表,通过
tenant_id字段区分。
方案 2 是主流,但开发极其痛苦。程序员必须在 每一条 SQL(查询、更新、删除)后面都手动加上 WHERE tenant_id = ?。一旦漏写,A 公司就能看到 B 公司的数据,这是 最高级别的安全事故。
解决方案:
Mybatis-Plus 提供了 多租户插件 (TenantLineInnerInterceptor)。它能在 SQL 执行前,自动在 WHERE 子句中拼接 tenant_id 条件,让开发者感觉像是在操作单租户系统一样。
5.1.1. 准备工作
步骤 1:数据库变更
我们需要在 tb_user 表中添加租户字段。
1 | -- 添加租户 ID 字段 |
步骤 2:实体类变更
注意:实体类中 不需要 添加 tenantId 属性!因为这个字段是由插件自动管理的,业务代码通常不需要感知它(除非你需要手动读取它)。当然,为了调试方便,加上也无妨,但我们演示“无感隔离”,故暂不添加。
5.1.2. 配置多租户插件
我们需要实现 TenantLineHandler 接口,告诉 MP 如何获取当前登录用户的租户 ID。
文件路径: src/main/java/com/example/mpstudy/config/MybatisPlusConfig.java
1 |
|
5.1.3. 隔离效果验证
我们不需要修改任何业务代码,直接运行之前的查询测试。
文件路径: src/test/java/com/example/mpstudy/TenantTest.java
1 |
|
1
2
3
4
----- 多租户隔离测试 -----
-- ==> Preparing: SELECT id, name... FROM tb_user WHERE tenant_id = 1001
-- ==> Parameters:
-- 自动拼接了 tenant_id = 1001,只能查到当前租户的数据
插入时的黑科技:当你执行 userMapper.insert(new UserDO()) 时,插件也会 自动 将 tenant_id 字段填充为 1001,你甚至都不需要在 Java 对象里 set 这个值。
5.2. 海量数据流式查询 (Stream Query)
痛点背景:业务方提出需求:“我要导出所有用户的 Excel,大概有 100 万条。”
新手做法:List<User> list = userMapper.selectList(null);后果:JVM 瞬间尝试将 100 万个对象加载到堆内存,直接报 OOM (Out Of Memory) 错误,服务崩溃。
解决方案:
MyBatis 原生支持 流式查询。配合 MP,我们可以通过 ResultHandler 接口,让数据“像水流一样”一条条流过内存,处理完一条扔一条,内存占用极低。
5.2.1. 自定义流式 Mapper 方法
MP 的 BaseMapper 没有直接暴露流式接口,我们需要在自定义 Mapper 中声明。
文件路径: src/main/java/com/example/mpstudy/mapper/UserMapper.java
1 | public interface UserMapper extends BaseMapper<UserDO> { |
原理解析:
fetchSize = 1000:这是 JDBC 的黑魔法。它告诉数据库驱动:“不要一次把 100 万条全给我,每次给我 1000 条。”ResultSetType.FORWARD_ONLY:游标只向前滚动,节省资源。
5.2.2. 业务层调用实战
在 Service 层,我们通过回调函数处理每一条数据。
文件路径: src/test/java/com/example/mpstudy/StreamTest.java
1 |
|
效果:无论数据库里有 1 万条还是 1000 万条数据,这段代码对 JVM 内存的占用始终维持在极低水平(仅取决于 fetchSize 的大小),彻底解决了 OOM 问题。
5.3. 动态数据源 (Dynamic Datasource)
在第四章中我们学习了单数据源的配置。但在企业级架构中,读写分离(主从架构)是提升数据库并发能力的必经之路。
MP 生态提供了 dynamic-datasource,它是目前 Java 界最优秀的动态数据源组件。
5.3.1. 依赖与配置
步骤 1:引入依赖
1 | <dependency> |
步骤 2:配置主从库application.yml 的配置结构发生完全变化,不再使用 spring.datasource,而是 spring.datasource.dynamic。
1 | spring: |
5.3.2. 注解控制路由 (@DS)
在 Service 层,我们可以通过注解精确控制方法走哪个库。
文件路径: src/main/java/com/example/mpstudy/service/impl/UserServiceImpl.java
1 | // [类级别] 默认走主库,确保写操作安全 |
注意事项:
- 事务失效问题:如果在同一个
@Transactional方法中切换数据源,切换会失效。因为 Spring 事务一旦开启,连接就绑定了。- 对策:尽量不要在事务内切换数据源,或者使用分布式事务(Seata)。
- 主从延迟:刚写入主库的数据,立刻去从库查可能查不到。
- 对策:对于一致性要求高的查询(如支付后的状态查询),强制加上
@DS("master")走主库。
- 对策:对于一致性要求高的查询(如支付后的状态查询),强制加上
5.4. 本章总结与架构速查
5.4.1. 场景化速查
场景一:SaaS 多租户隔离
- 方案:
TenantLineInnerInterceptor - 代码:实现
getTenantId()返回当前用户租户,实现ignoreTable()排除字典表。 - 效果:所有 SQL 自动拼接
WHERE tenant_id = ?。
场景二:导出 500 万行订单数据
- 方案:MyBatis 流式查询 (
ResultHandler) - 代码:
@Options(fetchSize = 1000, resultSetType = FORWARD_ONLY) - 效果:内存占用恒定,不会 OOM。
场景三:数据库 CPU 飙高,读多写少
- 方案:读写分离 (
dynamic-datasource) - 代码:配置
master/slave,写操作@DS("master"),读操作@DS("slave")。 - 效果:将 80% 的查询流量引流到从库,保护主库。
5.4.2. 核心避坑指南
多租户下的“漏网之鱼”
- 现象:手写 XML SQL 时,发现没有自动拼接
tenant_id。 - 原因:MP 的插件主要拦截 MP 自动生成的 SQL。对于手写 SQL,如果 SQL 结构极其复杂,JSqlParser 可能解析失败导致无法添加条件。
- 对策:手写 XML 时,务必手动加上
AND tenant_id = #{tenantId},不要完全依赖插件。
- 现象:手写 XML SQL 时,发现没有自动拼接
流式查询连接未关闭
- 现象:流式查询执行一半报错
Connection is closed。 - 原因:流式查询必须在事务中执行,或者保持连接打开。
- 对策:给 Service 方法加上
@Transactional(readOnly = true)。
- 现象:流式查询执行一半报错







