一、先别急着看代码:什么是「租户」?
1️⃣ 什么是租户(Tenant)?
在SaaS 系统中:
租户 = 一套系统的一个“客户单位”
举几个直观例子:
一个 OA 系统
- A 公司是一位租户
- B 公司是另一位租户
一个进销存系统
- 每家使用的商户 = 一个租户
特点只有一个:
👉数据必须隔离,但代码是同一套
2️⃣ 为什么芋道一定要做租户?
芋道源码是一个企业级 / SaaS 友好的后台系统,如果没有租户:
- 所有公司用户混在一张表里
- 一次 SQL 写错,直接全公司数据泄露
- 后期根本没法商业化
所以芋道从一开始就设计了:
系统级多租户支持(不是 Demo 级)
二、芋道的租户方案总览(先给结论)
在芋道源码中,多租户的核心思路是:
在程序层面,自动给每一条 SQL 加上
tenant_id条件
一句话总结就是:
当前请求属于哪个租户? ↓ 把租户 ID 放进上下文 ↓ SQL 执行前自动拼:tenant_id = ?你几乎不需要自己在 SQL 里写 tenant_id。
三、芋道的租户核心设计(架构层面)
1️⃣ 芋道采用的是哪种多租户方案?
多租户一般有 3 种方案:
| 方案 | 说明 | 芋道是否采用 |
|---|---|---|
| 独立数据库 | 每个租户一个 DB | ❌ |
| 独立 Schema | 一个 DB,多 Schema | ❌ |
| 共享表 + tenant_id | 每行数据带 tenant_id | ✅采用 |
芋道采用的是最常见、最灵活的一种:
共享表 + tenant_id 字段隔离数据
2️⃣ 数据库层面是怎么设计的?
几乎所有业务表,都会有一个字段:
tenant_idBIGINTNOTNULL例如:
CREATETABLEsystem_user(idBIGINTPRIMARYKEY,usernameVARCHAR(50),tenant_idBIGINT,...);❗ 注意:
- 不是所有表都有 tenant_id
- 像「租户表、字典表、菜单模板表」等是全局表
四、租户是怎么“进入程序”的?(最关键)
1️⃣ 租户 ID 从哪里来?
在芋道中,99% 的请求租户 ID 来自:
登录用户的 Token
流程是这样的:
浏览器请求 ↓ 携带 token(JWT) ↓ 解析 token ↓ 拿到 tenantId这个 tenantId 会被放入一个线程上下文(ThreadLocal)中。
2️⃣ 租户上下文:TenantContextHolder
芋道内部维护了一个类似这样的类(概念简化):
publicclassTenantContextHolder{privatestaticfinalThreadLocal<Long>TENANT=newThreadLocal<>();publicstaticvoidsetTenantId(LongtenantId){TENANT.set(tenantId);}publicstaticLonggetTenantId(){returnTENANT.get();}publicstaticvoidclear(){TENANT.remove();}}📌关键点:
- 每个请求线程都有自己的 tenantId
- 不同请求互不影响
- 请求结束后会清理
五、SQL 是如何“自动加 tenant_id”的?
这是芋道多租户最精华的部分 👇
1️⃣ 芋道用的是什么技术?
MyBatis Plus + 租户插件(TenantLineInnerInterceptor)
本质是一个SQL 拦截器:
SQL 执行前 ↓ 拦截 SQL ↓ 判断是否需要租户隔离 ↓ 自动拼 tenant_id 条件 ↓ 再执行2️⃣ 举个真实效果的例子
你在代码里写的 Mapper:
@Select("SELECT * FROM system_user")List<UserDO>selectList();实际执行到数据库的 SQL:
SELECT*FROMsystem_userWHEREtenant_id=101👉你没写 tenant_id,但系统自动帮你加了
3️⃣ 插件是怎么知道 tenant_id 的?
拦截器内部会调用:
TenantContextHolder.getTenantId()只要当前线程有 tenantId:
- 自动加条件
- 不需要你干预
六、不是所有表都加 tenant_id(如何控制?)
1️⃣ 芋道如何排除“全局表”?
芋道在租户配置中,维护了一份忽略表名单:
tenant:ignore-tables:-system_tenant-system_menu-system_dict_data这些表:
- 不拼 tenant_id
- 所有租户共享
2️⃣ 某些接口不想要租户隔离怎么办?
芋道提供了显式关闭租户的能力。
例如:
TenantContextHolder.clear();或使用封装好的工具类,在代码块内临时关闭租户过滤。
📌 常见使用场景:
- 超级管理员
- 定时任务
- 跨租户统计
七、写业务代码时,你要关心什么?
1️⃣ 正常 CRUD,你几乎不用管租户
你写业务代码时:
userMapper.selectById(id);userMapper.insert(user);芋道会帮你自动处理:
- tenant_id 注入
- tenant_id 查询条件
👉这是设计最成功的地方
2️⃣ 你必须注意的 4 个点(血的教训)
❌ 1. 不要手写 tenant_id 条件(除非你非常清楚)
容易导致:
- 条件重复
- SQL 失效
❌ 2. 不要用原生 JDBC
会绕过 MyBatis Plus 拦截器
⚠️ 3. 自定义 SQL 要确认是否被拦截
@Select、XML SQL 都会被拦截 ✔JdbcTemplate❌
⚠️ 4. 定时任务里 tenantId 为空
要手动设置租户上下文
八、一个完整请求的租户生命周期(强烈建议看)
HTTP 请求进入 ↓ 解析 Token ↓ 获取 tenantId ↓ TenantContextHolder.setTenantId() ↓ Controller / Service / Mapper ↓ MyBatis 拦截 SQL,加 tenant_id ↓ 请求结束 ↓ TenantContextHolder.clear()💡 你理解了这条链路,就理解了芋道 90% 的多租户设计
九、芋道租户设计适合什么项目?
✅ 非常适合
- SaaS 系统
- 多客户后台
- 中小企业管理系统
- 二次开发商业项目
⚠️ 不太适合
- 超大规模分库分表
- 强物理隔离(金融级)
十、总结(给小白的最终结论)
用一句话概括芋道的租户设计:
芋道通过 ThreadLocal 保存租户上下文,
再通过 MyBatis Plus 拦截器自动拼接 tenant_id,
实现“对业务代码几乎无侵入”的多租户隔离。
你作为新手,只需要记住:
- 租户 ID 来自登录用户
- 你不用手写 tenant_id
- SQL 自动生效
- 小心绕过 MyBatis 的方式