LikeShop 单商户 SaaS 版租户隔离:数据库分库分表与配置级权限控制

简介: SaaS核心挑战在于租户隔离:数据不串、功能不越、资源受限。LikeShop采用分层隔离策略——共享库+tenant_id分表为主,支持平滑演进至分库;框架层强制注入tenant_id,配置化功能权限与用量管控,实现从接入到运维的全链路安全隔离。(239字)

SaaS 模式的核心挑战,从来都不是功能多少,而是隔离。

一套系统承载几十上百个商家租户,共用服务器、共用数据库、共享同一套代码。但数据绝对不能串——A 租户看不到 B 租户的商品、订单、用户;功能绝对不能越——基础版租户用不了专业版的模块;资源必须受限——超套餐的商品数、订单数要能拦住。

任何一层做不到位,都是线上事故。

LikeShop 单商户 SaaS 版(Java 技术栈)在租户隔离上采用的是分层隔离思路:数据层分库 / 分表可选、框架层强制 tenant_id 注入、应用层配置级功能权限。在轻量部署和安全隔离之间找平衡,同时支持从小规模到大规模的平滑演进。

一、SaaS 租户隔离的两种主流方案

多租户数据隔离,行业内主流就两种:独立数据库(分库)和共享数据库 + tenant_id(分表)。没有绝对的好坏,只看租户规模、行业合规要求和运维成本。

1.1 两种方案对比

维度 共享库·分表方案 独立库·分库方案
隔离级别 逻辑隔离,靠代码保证 物理隔离,数据库层面天然隔离
租户数量上限 高,单库可承载上千租户 低,几十到几百租户
数据库成本 低,一个实例搞定 高,每个租户一个库或按组分配
性能开销 小,只是多一个查询条件 小,但连接池管理复杂
运维难度 低,统一备份、统一升级 高,每个库单独维护、升级、备份
合规性 一般,满足普通电商场景 高,适合强监管行业
适合场景 中小租户、标准 SaaS 服务 大客户定制、强合规要求、数据量大

1.2 LikeShop 的选型策略

LikeShop 单商户 SaaS 版默认采用共享库 + tenant_id 分表方案,这是综合了绝大多数中小 SaaS 场景的选择:

  • 租户规模从几十到几百都能承载;
  • 运维成本低,一套数据库统一维护;
  • 系统升级简单,一次发布全量生效;
  • 性能足够,只要索引设计合理,tenant_id 过滤后的数据量对 MySQL 完全不是压力。

同时架构上也支持分库模式:当平台出现超大租户、或者合规要求物理隔离时,可以按租户哈希分库、按套餐分库,平滑演进,不用推翻重构。

二、分表方案:全表 tenant_id + 框架层强制注入

分表方案最核心的原则只有一条:绝对不能靠开发者自觉加 tenant_id。

只要靠人,就一定会漏。漏一次,就是跨租户数据泄露。正确的做法是在框架层强制注入,业务开发者根本感知不到 tenant_id 的存在。

2.1 数据库表设计

所有业务表必须带 tenant_id:

-- 商品表
CREATE TABLE ls_goods (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL COMMENT '租户ID',
  name VARCHAR(255) NOT NULL,
  -- ... 其他字段
  INDEX idx_tenant_id(tenant_id)
);

-- 订单表
CREATE TABLE ls_order (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  order_sn VARCHAR(32) NOT NULL,
  -- ...
  INDEX idx_tenant_id(tenant_id)
);

-- 用户表
CREATE TABLE ls_user (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  tenant_id BIGINT NOT NULL,
  nickname VARCHAR(64),
  -- ...
);

系统公用表不带 tenant_id:

  • 套餐表 ls_plan
  • 租户表 ls_tenant
  • 字典表、配置表、行政区域表
  • 定时任务调度表

2.2 框架层自动注入:MyBatis-Plus 租户插件

LikeShop Java 版用的是 MyBatis-Plus 内置的 TenantLineInnerInterceptor 租户线插件。所有 SQL 执行前,插件自动拼接 tenant_id = ? 条件。

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 租户线拦截器
        TenantLineInnerInterceptor tenantInterceptor = new TenantLineInnerInterceptor();
        tenantInterceptor.setTenantLineHandler(new TenantLineHandler() {

            @Override
            public Expression getTenantId() {
                // 从线程上下文取当前租户
                Long tenantId = TenantContext.getTenantId();
                return new LongValue(tenantId);
            }

            @Override
            public boolean ignoreTable(String tableName) {
                // 忽略系统表,不加 tenant_id
                return TenantConfig.getIgnoreTables().contains(tableName);
            }
        });

        interceptor.addInnerInterceptor(tenantInterceptor);
        return interceptor;
    }
}

这样一来,业务代码里写:

goodsMapper.selectList(queryWrapper);

实际执行的 SQL 会自动变成:

SELECT * FROM ls_goods WHERE tenant_id = 1001 AND ...

开发者完全不用管 tenant_id,想漏都漏不掉。

2.3 租户上下文:怎么识别当前是谁

租户 ID 不能每次请求都查库,要放在线程上下文里:

public class TenantContext {

    private static final ThreadLocal<Long> TENANT_ID = new ThreadLocal<>();

    public static void setTenantId(Long tenantId) {
        TENANT_ID.set(tenantId);
    }

    public static Long getTenantId() {
        Long id = TENANT_ID.get();
        if (id == null) {
            throw new IllegalStateException("租户上下文未初始化");
        }
        return id;
    }

    public static void clear() {
        TENANT_ID.remove();
    }
}

再用一个 Web 拦截器,在请求最开始识别租户:

@Component
public class TenantInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, 
                         HttpServletResponse response, 
                         Object handler) {
        // 方式1:按域名识别
        String domain = request.getServerName();
        Tenant tenant = tenantService.getByDomain(domain);

        // 方式2:按请求头 tenant-id 识别(开放接口用)
        // String tenantHeader = request.getHeader("X-Tenant-Id");

        TenantContext.setTenantId(tenant.getId());
        return true;
    }

    @Override
    public void afterCompletion(...) {
        TenantContext.clear();
    }
}

从请求入口到数据库底层,全链路自动带上租户,业务代码零感知。

三、分库方案:物理隔离的进阶模式

当平台出现以下情况时,分表就不够了:

  • 单个租户数据量超大(百万级订单);
  • 大客户要求物理隔离;
  • 行业合规要求数据必须独立存储。

这时候就要演进到分库模式。

3.1 实现思路:动态数据源 + 路由策略

// 动态数据源路由
public class TenantRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        Long tenantId = TenantContext.getTenantId();
        // 按租户ID哈希分库:tenant_0 ~ tenant_7
        return "tenant_" + (tenantId % 8);
    }
}

分库策略有几种常见玩法:

  1. 按租户 ID 哈希分库:均匀分布,适合大量中小租户;
  2. 按套餐分库:免费版一个库、专业版一个库、大客户独立库;
  3. 按行业分库:零售一个库、餐饮一个库、服务类一个库。

3.2 平滑演进

LikeShop 的架构支持平滑过渡:

  • 初期:单库分表,快速上线,成本低;
  • 增长期:按套餐分库,把大客户迁出去;
  • 成熟期:全量哈希分库,统一架构。

不需要一开始就上最复杂的方案,跟着业务规模长就行。

四、配置级权限:不只是数据隔离,功能也要隔离

SaaS 的隔离不只是数据。不同套餐的租户,能使用的功能也必须是隔离的——基础版不能用分销、免费版不能开 APP、入门版没有数据导出。

这就是配置级权限控制。

4.1 套餐功能配置

每个套餐的功能权限,存在 features_json 字段里:

{
  "distribution": false,
  "seckill": true,
  "bargain": false,
  "member_level": true,
  "app": false,
  "data_export": true,
  "multi_shop": false
}

不写死在代码里,全部配置化。新增功能只要加一个 key,不用改套餐表结构。

4.2 接口层强制校验

只在前端隐藏菜单没用,后端接口必须再校验一次。

自定义注解 + 拦截器:

// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireFeature {
    String value();
}

// 拦截器
@Component
public class FeatureInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, 
                         HttpServletResponse response, 
                         Object handler) {
        Method method = ((HandlerMethod) handler).getMethod();
        RequireFeature anno = method.getAnnotation(RequireFeature.class);

        if (anno != null) {
            String feature = anno.value();
            if (!PlanService.canUse(TenantContext.getTenantId(), feature)) {
                throw new BizException("当前套餐不支持该功能");
            }
        }
        return true;
    }
}

用的时候直接在接口上加注解:

@RequireFeature("distribution")
@PostMapping("/distributor/apply")
public R applyDistributor() {
    // ...
}

4.3 用量限制

除了功能开关,还有用量限制:商品数上限、月订单数上限、存储空间上限、短信条数上限。

@Service
public class GoodsService {

    public void createGoods(GoodsDTO dto) {
        Tenant tenant = tenantService.getById(TenantContext.getTenantId());
        Plan plan = planService.getById(tenant.getPlanId());

        if (plan.getGoodsLimit() > 0 
                && tenant.getGoodsCount() >= plan.getGoodsLimit()) {
            throw new BizException("商品数量已达套餐上限,请升级");
        }
        // ... 创建商品
    }
}

用量数据每天定时任务刷新,冗余到租户表,不用每次 count。

五、全链路隔离的完整路径

真正可靠的租户隔离,不是只在数据库加个字段。它是一条完整的链路:

请求接入 → 租户识别 → 上下文绑定 → 功能权限校验 → DAO自动加tenant_id → 业务执行 → 结果过滤 → 前端按权限渲染

每一层都有自己的职责,哪一层漏了都可能出问题。

额外还要注意运维侧的隔离:

  • 平台超管可以跨租户看数据,但每一步操作都要写审计日志;
  • 商家客服只能看自己租户的数据,天然隔离;
  • 数据导出接口必须强制校验 tenant_id,不能导出全平台数据。

六、常见坑与最佳实践

坑 1:原生 SQL 绕过插件

最常见的事故源:写了原生 SQL(@Select 写死 SQL 字符串),租户插件不解析,结果没加 tenant_id,全表数据都出来了。

最佳实践:

  • 优先使用 MP 的 QueryWrapper、Lambda 表达式;
  • 必须写原生 SQL 时,手动加 tenant_id = #{tenantId};
  • 代码审查重点查原生 SQL。

坑 2:异步 / 多线程丢失上下文

ThreadLocal 是线程绑定的,异步任务、线程池里的线程拿不到租户 ID。

最佳实践:

  • 自定义线程池包装,提交任务时传递 TenantContext;
  • 或者用 TransmittableThreadLocal 替代 ThreadLocal。

坑 3:新增表忘记加 tenant_id

业务迭代加新表,开发忘了加 tenant_id 字段,插件配置里也没加排除,结果 SQL 报错或者数据串。

最佳实践:

  • 建表规范强制要求业务表必须有 tenant_id;
  • 上线前跑 SQL 审计脚本,检查所有业务表是否都有该字段。

坑 4:管理员越权操作

平台超管账号权限太大,能看所有租户数据,操作没留痕,出了事查不到。

最佳实践:

  • 超管操作全部留审计日志;
  • 敏感操作(改套餐、改数据)二次确认;
  • 定期审计超管操作日志。

坑 5:分库模式下的事务问题

分库后,跨租户事务失效,不能用本地事务。

最佳实践:

  • 尽量避免跨租户操作;
  • 必须跨租户的场景,用最终一致性方案,不要强事务。

结语

SaaS 租户隔离,从来都不是"加个 tenant_id 字段"这么简单的事。它是从接入层、框架层、数据层、业务层到运维层的全链路设计。

LikeShop 单商户 SaaS 版的这套方案,核心思路就是分层隔离、按需演进:

  • 小规模用分表,成本低、上线快;
  • 大规模转分库,物理隔离、满足合规;
  • 功能权限配置化,套餐灵活调整;
  • 框架层强制注入,避免人为疏漏。

对做 SaaS 的团队来说,隔离属于"地基"级别的设计——地基打稳了,上面加功能、加套餐、扩租户,才是顺理成章的事。

目录
相关文章
|
5天前
|
云安全 人工智能 运维
当攻击全面Agent化,你的安全防御能否跟上AI Speed?
多起客户侧AK泄露事件应急的真实观察
1241 4
|
28天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
9785 32
|
8天前
|
存储 人工智能 安全
千问办公官网入口网页版登录:qwenwork.cn 注册送2000积分、登录送100积分
阿里云千问办公(QwenWork)是AI智能工作平台,支持一句话生成PPT、表格、网页、视频及数据分析。个人免费版送2000积分+每日登录赠100分,含1GB存储、5个发布页;企业版198元/席/月起。阿里千问办公QwenWork官网:https://t.aliyun.com/U/0VCTGt 阿里AI工作平台,一句话完成数据分析、PPT 生成、视频剪辑、网页搭建等复杂任务
|
3天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
|
14天前
|
存储 人工智能 并行计算
大模型本地部署终端选型方法论:以 Qwen3.8-27B 为例的四档分层完整流程
本文提出一套大模型本地部署终端选型方法论:定约束、定档位、定框架、定参数四步决策法,配合入门、主力、质量、无损四档分层模型。以 Qwen3.8-27B 实测数据为例,逐环节解读显存、带宽、存储、散热、系统、预算等要素,给出面向不同预算的优选方案、决策自查清单与市场观察框架。文末前瞻 AI 笔记本的 CPU+GPU 与统一内存两条路线,论证四步决策法在新品类上的延续性。
|
5天前
|
Web App开发 人工智能 JavaScript
太猛了,一个月干到15W+,给大家推荐一个最近爆火的开源逆向工具:REA
REA(Reverse Engineer Anything)是近期爆火的开源AI逆向工具,4月发布,1个月内斩获15万+ Star。它让AI Agent无需源码即可分析二进制、APK、JS/Electron、HAR包等17类目标,自动反编译、生成可运行代码,并强制返回带证据与局限的结论。全本地执行,MIT协议,支持Hopper/Ghidra/IDA等引擎,一行命令即可上手。
625 1
太猛了,一个月干到15W+,给大家推荐一个最近爆火的开源逆向工具:REA
|
13天前
|
人工智能 JSON 自然语言处理
2026 年 Jev 决策模型深度拆解:原理解读、实战测评与保姆级落地教程
有一款特殊AI模型在开发者圈子刷屏,它摒弃传统大模型擅长的对话聊天能力,专注做高速结构化决策,它就是TypeSafe AI推出的Jev模型。该模型由ChatGPT共同发明人Diogo Almeida主导研发,定位为**System One Model(系统一模型)**,对标人类大脑快速直觉判断的思维模式,在响应延迟、调用成本、结构化输出稳定性上相比传统生成式大模型有着巨大差异。本文会完整拆解Jev底层原理、三大核心原语能力、适用业务场景,同时提供可直接运行的curl、Python代码示例,并且结合多组实测数据,客观分析模型优势与能力边界,帮助普通开发者和AI应用从业者快速上手落地。
928 1
|
9天前
|
人工智能 JSON 自然语言处理
2026 年 Jev 决策模型全解析:核心原理拆解,性能实战测评与落地部署教程
有一款特殊AI模型在开发者圈子刷屏,它摒弃传统大模型擅长的对话聊天能力,专注做高速结构化决策,它就是TypeSafe AI推出的Jev模型。该模型由ChatGPT共同发明人Diogo Almeida主导研发,定位为System One Model(系统一模型),对标人类大脑快速直觉判断的思维模式,在响应延迟、调用成本、结构化输出稳定性上相比传统生成式大模型有着巨大差异。本文会完整拆解Jev底层原理、三大核心原语能力、适用业务场景,同时提供可直接运行的curl、Python代码示例,并且结合多组实测数据,客观分析模型优势与能力边界,帮助普通开发者和AI应用从业者快速上手落地。
592 0

热门文章

最新文章