新闻详情

新闻详情

首页 / 资讯中心 / 详情

qkl-boot 多租户落地:MyBatis-Plus 拦截器 + tenant_id,一套代码隔离所有租户数据

发布时间:2026/10/2 5:56:39来源:尧图网络
qkl-boot 多租户落地:MyBatis-Plus 拦截器 + tenant_id,一套代码隔离所有租户数据
导读接了个 SaaS 单子多家企业客户共用一套系统。第一版我直接在每张表加 tenant_id然后在 Service 里手动拼 where 条件结果漏改一张表两个客户的数据直接串了。后来用 MyBatis-Plus 的租户拦截器统一处理一套代码自动隔离所有租户数据。这篇把落地过程和踩的坑都记下来。qkl-boot 多租户落地MyBatis-Plus 拦截器 tenant_id一套代码隔离所有租户数据先说场景。客户要求一套系统卖给多家企业每个企业只能看到自己的数据。最简单粗暴的方案每张业务表加tenant_id字段查询时手动带上WHERE tenant_id 当前租户。这方案听起来没毛病坑在人总会漏。我第一版就是在 20 多张表里手动拼条件结果漏了简历附件表A 公司的求职者居然在 B 公司的列表里看到了自己的附件客户当场炸了。数据模型tenant_id 加哪索引怎么建业务表统一加tenant_id注意联合索引要把 tenant_id 放第一位否则隔离查询还是要全表扫CREATETABLEjob_(idBIGINTNOTNULLAUTO_INCREMENTCOMMENT主键,tenant_idBIGINTNOTNULLDEFAULT1COMMENT租户ID,titleVARCHAR(100)NOTNULLCOMMENT职位名称,salaryDECIMAL(10,2)DEFAULTNULLCOMMENT薪资,statusTINYINTNOTNULLDEFAULT1COMMENT状态 1招聘中 2关闭,PRIMARYKEY(id),KEYidx_tenant_title(tenant_id,title),KEYidx_tenant_status(tenant_id,status))ENGINEInnoDBCOMMENT职位表;tenant_id 放第一位tenant_id 业务字段的联合索引才能支撑租户内的业务查询。核心MyBatis-Plus 租户拦截器SQL 自动加条件手动拼 where 不可靠MyBatis-Plus 内置了TenantLineInnerInterceptor拦截器会在所有自动生成的 SQL 上自动追加租户条件ConfigurationpublicclassMybatisPlusConfig{BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptornewMybatisPlusInterceptor();interceptor.addInnerInterceptor(newTenantLineInnerInterceptor(newTenantLineHandler(){OverridepublicExpressiongetTenantId(){// 从上下文拿当前租户返回 SQL 条件值returnnewLongValue(TenantContext.getTenantId());}OverridepublicStringgetTenantIdColumn(){returntenant_id;}OverridepublicbooleanignoreTable(StringtableName){// 忽略表系统表、租户表本身不加租户条件returnsys_user.equals(tableName)||sys_tenant.equals(tableName)||sys_dict.equals(tableName);}}));returninterceptor;}}拦截器自动给 SELECT/UPDATE/DELETE 加tenant_id ?给 INSERT 自动填充tenant_id。前提是租户上下文必须在线程里传递publicclassTenantContext{privatestaticfinalThreadLocalHOLDERnewThreadLocal();publicstaticvoidsetTenantId(LongtenantId){HOLDER.set(tenantId);}publicstaticLonggetTenantId(){returnHOLDER.get()null?1L:HOLDER.get();}publicstaticvoidclear(){HOLDER.remove();}}登录时从 Token 里解析租户 ID 塞进 ThreadLocal请求结束记得clear()不然线程池复用会串租户。踩坑一自定义 XML SQL 绕过了拦截器数据又串了现象上线两周后A 公司的后台列表出现了 B 公司的职位。查日志WHERE里居然没有 tenant_id 条件。排查过程加租户拦截器后我以为万事大吉但排查发现问题出在 XML 里手写的 SQL。列表页有个复杂的多表联查职位 公司 收藏状态我是手写 XML 的MyBatis-Plus 拦截器对纯手写 SQL 默认不解析没开ignoreSqlSelect那种处理条件没加上。定位思路拦截器只管 MP 自动生成的方法selectById、selectList、wrapper 查询XML 手写 SQL 是漏网之鱼。最终解决把 XML 里所有查询改成强制带 tenant_id 条件并且写进代码规范自定义 SQL 必须显式传 tenantId。给 Mapper 方法加参数SELECT j.*, c.company_name FROM job_ j LEFT JOIN company_ c ON c.id j.company_id AND c.tenant_id j.tenant_id WHERE j.tenant_id #{tenantId} AND j.status 1踩坑二定时任务没有登录上下文租户 ID 全变成默认值现象每天凌晨 2 点的职位自动下架任务把所有租户的过期职位全下架了。排查过程定时任务在独立线程里跑TenantContext.getTenantId()拿到的是默认值 1第一个租户于是拦截器给 SQL 加的是tenant_id 1其余租户的职位没被处理更糟的是任务里有一段遍历租户的逻辑如果没设置上下文默认值直接污染了所有查询。定位思路ThreadLocal 只在线程内生效异步线程、定时任务线程都要显式设置上下文。最终解决定时任务开头遍历租户列表循环内设置上下文、执行、finally 清理Scheduled(cron0 0 2 * * ?)publicvoidautoCloseExpiredJob(){ListtenantIdstenantMapper.selectAllTenantIds();for(LongtenantId:tenantIds){TenantContext.setTenantId(tenantId);try{jobService.closeExpired(tenantId);}finally{TenantContext.clear();}}}同理MQ 消费、异步线程池都要在入口处显式设置租户上下文这是多租户最容易翻车的地方。可直接复用的要点业务表统一tenant_id联合索引把 tenant_id 放第一位。用 MyBatis-PlusTenantLineInnerInterceptor自动加租户条件别手写 where。XML 手写 SQL 是漏网之鱼必须显式传 tenantId写进代码规范。租户上下文用 ThreadLocal请求结束必须 clear防止线程池复用串租户。定时任务、MQ 消费、异步线程入口处显式设置租户上下文。忽略表配置要收敛sys_user、sys_tenant、sys_dict 等系统表别漏别多。上线前写个单测切租户 A 查询断言 SQL 里带tenant_id A。项目源码https://gitee.com/gzqkl/qkl-boot
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

2026年DeepSeek关键事件复盘:V4多模态与Agent推理的成本革命,TaoToken统一Key实测 2026/10/2 13:56:06

2026年DeepSeek关键事件复盘:V4多模态与Agent推理的成本革命,TaoToken统一Key实测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
【Mongoose学习笔记一】简单使用:从零搭建HTTP服务与TCP/UDP通信的配置验证 2026/10/2 13:56:05

【Mongoose学习笔记一】简单使用:从零搭建HTTP服务与TCP/UDP通信的配置验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
OpenCode AI 编程助手使用教程:把 Base URL 改到 TaoToken 的完整配置 2026/10/2 13:55:58

OpenCode AI 编程助手使用教程:把 Base URL 改到 TaoToken 的完整配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
高远-全球法规管理平台实战:从几百页 PDF 到零部件级合规底数,四步串起来 2026/10/2 13:55:52

高远-全球法规管理平台实战:从几百页 PDF 到零部件级合规底数,四步串起来

说明:本文以「高远-全球法规管理平台」为例,介绍车企出海合规数字化的人机协同思路。该平台由北京高远华信科技自主研发与交付,并非飞书项目、钉钉等协同工具的原生功能,而是通过标准 OpenAPI 与飞书、钉钉、企业微信协同&#xf…

阅读更多 →
中通服-从数据安全审计检查,到看懂企业数据安全技术体系 2026/10/2 13:55:51

中通服-从数据安全审计检查,到看懂企业数据安全技术体系

一、为什么开始整理这篇 Blog从实际数据安全检查项出发,拆解企业数据安全管理要求背后的技术实现。之前参与数据安全检查工作时,手里拿到的通常是一张很长的检查表。表里面可能有几十甚至上百个检查项,例如:是否建立数据全生命周期…

阅读更多 →
嵌入式偶发故障排查实战:串口丢数据、蓝牙断开与烧录失败 2026/10/2 13:55:44

嵌入式偶发故障排查实战:串口丢数据、蓝牙断开与烧录失败

1. 偶发故障为什么比必现故障更难缠做嵌入式开发和硬件调试的人都有一个共识:必现的bug是好bug,偶发的bug才是真正的噩梦。串口通信偶尔丢一帧数据、蓝牙连接用着用着突然断开、烧录工具十次里有一两次报错——这些问题最让人头疼的地方不在于修复难度&a…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉