多商户多租户系统架构设计文档(Java版)
版本 :v1.0
日期 :2026-07-31
技术栈 :Java 17 + Spring Boot 3.x + Spring Cloud + MySQL + Redis + RocketMQ
文档目标:提供一套可落地、可实施的多商户多租户系统架构方案,包含架构设计、数据库设计、核心代码、部署方案与实施路线图。
目录
- 概述
- 多租户架构模式对比与选型
- 系统总体架构设计
- 技术栈选型
- 数据库设计
- 租户隔离方案详解
- 权限体系设计
- 核心业务模块设计
- API设计规范
- 缓存策略设计
- 消息队列与异步处理
- 部署架构
- 高可用与扩展性设计
- 安全设计
- 监控与运维
- 实施路线图
- 关键代码示例
1. 概述
1.1 什么是多商户多租户系统
多商户(Multi-Merchant):系统中存在多个独立经营的商户主体,每个商户拥有自己的商品、订单、用户、资金等业务数据。
多租户(Multi-Tenant):一套软件系统同时为多个租户(Tenant)提供服务,每个租户的数据相互隔离,租户之间互不感知。
在本方案中,租户 = 商户,每个入驻平台的商户即为一个租户,拥有独立的业务空间和数据隔离。
1.2 适用场景
- SaaS 电商平台(多商家入驻)
- 企业服务平台(多公司/多部门)
- 连锁门店管理系统
- 教育培训平台(多机构入驻)
- 医疗健康平台(多医院/诊所)
1.3 核心设计目标
| 目标 | 说明 |
|---|---|
| 数据隔离 | 租户间数据严格隔离,防止越权访问 |
| 弹性扩展 | 支持租户数量从几十到十万级平滑扩展 |
| 独立配置 | 每个租户可独立配置业务规则、界面风格等 |
| 统一运维 | 平台方统一部署、监控、升级,降低运维成本 |
| 高性能 | 单租户请求响应时间 < 200ms,系统支持 10万+ QPS |
| 高可用 | 系统可用性 99.99%,单租户故障不影响其他租户 |
2. 多租户架构模式对比与选型
2.1 三种主流模式对比
| 维度 | 独立数据库(Database per Tenant) | 共享数据库独立Schema(Schema per Tenant) | 共享数据库共享表(Shared Database Shared Table) |
|---|---|---|---|
| 隔离级别 | ⭐⭐⭐⭐⭐ 最高 | ⭐⭐⭐⭐ 高 | ⭐⭐ 低 |
| 数据安全 | 物理隔离,最安全 | Schema隔离,较安全 | 逻辑隔离,依赖应用层 |
| 租户数量 | 几十~几百 | 几百~几千 | 几千~百万级 |
| 运维复杂度 | 高(每个库独立运维) | 中 | 低 |
| 成本 | 高 | 中 | 低 |
| 定制化能力 | 最强(可独立改表结构) | 较强 | 弱(统一Schema) |
| 迁移难度 | 简单(整库迁移) | 中等 | 困难(数据筛选) |
| 备份恢复 | 独立备份,粒度细 | Schema级备份 | 全量备份,恢复困难 |
2.2 本方案选型:混合模式
推荐采用「共享数据库共享表 + 重要租户独立Schema」的混合模式:
┌─────────────────────────────────────────────┐
│ 平台统一接入层 │
├──────────┬──────────┬───────────────────────┤
│ 租户A │ 租户B │ 普通租户池 │
│ (VIP) │ (VIP) │ (共享表 + tenant_id) │
│ 独立Schema│ 独立Schema│ C / D / E / F / ... │
└──────────┴──────────┴───────────────────────┘
选型理由:
- 90% 中小租户使用共享表模式,成本低、扩展快
- 10% 大客户/VIP租户使用独立Schema,满足数据安全与定制化需求
- 平台可根据租户等级动态切换隔离策略
- 兼顾成本与安全,适合大多数 SaaS 场景
3. 系统总体架构设计
3.1 分层架构图
┌─────────────────────────────────────────────────────────────┐
│ 接入层 (Gateway) │
│ Nginx + Spring Cloud Gateway + 限流 + 鉴权 + 路由 │
├─────────────────────────────────────────────────────────────┤
│ 业务服务层 (Services) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌────────┐ │
│ │ 租户服务 │ │ 用户服务 │ │ 商品服务 │ │ 订单服务 │ │支付服务│ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └────────┘ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌────────┐ │
│ │ 营销服务 │ │ 库存服务 │ │ 消息服务 │ │ 文件服务 │ │搜索服务│ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 公共服务层 (Common) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 认证中心 │ │ 权限中心 │ │ 租户上下文│ │ 配置中心 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 数据存储层 (Storage) │
│ MySQL集群 │ Redis集群 │ RocketMQ │ Elasticsearch │ MinIO │
├─────────────────────────────────────────────────────────────┤
│ 基础设施层 (Infrastructure) │
│ Kubernetes │ Prometheus │ SkyWalking │ ELK │ Nacos │
└─────────────────────────────────────────────────────────────┘
3.2 架构设计原则
- 租户上下文贯穿全链路:每次请求必须携带租户标识,从网关到数据库全程传递
- 服务无状态化:业务服务不保存租户状态,水平扩展不受限
- 数据隔离优先:隔离方案在架构层面强制保证,不依赖业务代码自觉
- 故障隔离:单租户故障(如慢查询、流量突增)不影响其他租户
- 配置外置:租户级配置统一管理,支持热更新
4. 技术栈选型
4.1 后端技术栈
| 分类 | 技术选型 | 版本 | 说明 |
|---|---|---|---|
| 语言 | Java | 17 (LTS) | 长期支持版本,性能稳定 |
| 框架 | Spring Boot | 3.2.x | 微服务基础框架 |
| 微服务 | Spring Cloud | 2023.0.x | 微服务全家桶 |
| 网关 | Spring Cloud Gateway | 4.1.x | 响应式网关 |
| 注册中心 | Nacos | 2.3.x | 服务注册与配置中心 |
| ORM | MyBatis-Plus | 3.5.x | 增强版 MyBatis,支持多租户插件 |
| 数据库 | MySQL | 8.0 | 主从复制 + 分库分表 |
| 缓存 | Redis | 7.x | 集群模式,缓存 + 分布式锁 |
| 消息队列 | RocketMQ | 5.x | 事务消息 + 延时消息 |
| 搜索引擎 | Elasticsearch | 8.x | 全文检索 + 日志存储 |
| 对象存储 | MinIO | - | 私有云对象存储 |
| 认证授权 | Spring Security + JWT | - | OAuth2 + JWT |
| 分布式事务 | Seata | 1.7.x | AT 模式 + TCC 模式 |
| 链路追踪 | SkyWalking | 9.x | 全链路追踪 |
| 监控 | Prometheus + Grafana | - | 指标监控 |
| 日志 | ELK (Elasticsearch + Logstash + Kibana) | - | 日志收集分析 |
| 容器编排 | Kubernetes | 1.28+ | 容器化部署 |
| API文档 | Knife4j | 4.x | Swagger增强版 |
4.2 前端技术栈(参考)
| 分类 | 技术选型 | 说明 |
|---|---|---|
| 框架 | Vue 3 + TypeScript | 渐进式框架 |
| UI组件库 | Element Plus / Ant Design Vue | 企业级组件库 |
| 状态管理 | Pinia | Vue官方推荐 |
| 构建工具 | Vite | 极速构建 |
| 微前端 | qiankun | 多应用集成 |
5. 数据库设计
5.1 数据库分库策略
┌─────────────────────────────────────────┐
│ platform_db (平台库) │
│ 租户信息 / 平台配置 / 字典 / 运营数据 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ tenant_shared_db (共享租户库) │
│ 普通租户业务数据,通过 tenant_id 隔离 │
│ 按 tenant_id 哈希分表(如 16 张分表) │
└─────────────────────────────────────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐
│ tenant_ │ │ tenant_ │ │ tenant_ │ ...
│ 1001_db │ │ 1002_db │ │ 1003_db │ (VIP租户独立库)
└──────────┘ └──────────┘ └──────────┘
5.2 平台库核心表结构
5.2.1 租户表(sys_tenant)
sql
CREATE TABLE `sys_tenant` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '租户ID',
`tenant_code` varchar(32) NOT NULL COMMENT '租户编码(唯一标识,如企业简称拼音)',
`tenant_name` varchar(128) NOT NULL COMMENT '租户名称',
`tenant_type` tinyint NOT NULL DEFAULT '1' COMMENT '租户类型:1-普通租户 2-VIP租户 3-平台自营',
`isolation_level` tinyint NOT NULL DEFAULT '1' COMMENT '隔离级别:1-共享表 2-独立Schema 3-独立数据库',
`schema_name` varchar(64) DEFAULT NULL COMMENT '独立Schema名称(isolation_level=2/3时使用)',
`db_url` varchar(512) DEFAULT NULL COMMENT '独立数据库连接URL',
`db_username` varchar(64) DEFAULT NULL COMMENT '数据库用户名(加密存储)',
`db_password` varchar(256) DEFAULT NULL COMMENT '数据库密码(加密存储)',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用 1-正常 2-过期 3-欠费',
`expire_time` datetime DEFAULT NULL COMMENT '到期时间',
`max_user_count` int DEFAULT '100' COMMENT '最大用户数',
`max_storage_mb` bigint DEFAULT '1024' COMMENT '最大存储空间(MB)',
`contact_name` varchar(64) DEFAULT NULL COMMENT '联系人',
`contact_phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`contact_email` varchar(128) DEFAULT NULL COMMENT '联系邮箱',
`domain` varchar(128) DEFAULT NULL COMMENT '绑定域名',
`logo_url` varchar(512) DEFAULT NULL COMMENT '租户Logo',
`theme_config` json DEFAULT NULL COMMENT '主题配置JSON',
`extra_config` json DEFAULT NULL COMMENT '扩展配置JSON',
`creator` bigint DEFAULT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` bigint DEFAULT NULL COMMENT '更新人',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除:0-未删除 1-已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_code` (`tenant_code`),
KEY `idx_status` (`status`),
KEY `idx_tenant_type` (`tenant_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户信息表';
5.2.2 租户套餐表(sys_tenant_package)
sql
CREATE TABLE `sys_tenant_package` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '套餐ID',
`package_code` varchar(32) NOT NULL COMMENT '套餐编码',
`package_name` varchar(64) NOT NULL COMMENT '套餐名称',
`package_type` tinyint NOT NULL DEFAULT '1' COMMENT '套餐类型:1-基础版 2-专业版 3-企业版 4-定制版',
`price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '价格(元/月)',
`year_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '年付价格(元/年)',
`max_user_count` int NOT NULL DEFAULT '10' COMMENT '最大用户数',
`max_storage_mb` bigint NOT NULL DEFAULT '1024' COMMENT '最大存储空间(MB)',
`menu_ids` json DEFAULT NULL COMMENT '包含菜单ID列表',
`permissions` json DEFAULT NULL COMMENT '包含权限标识列表',
`features` json DEFAULT NULL COMMENT '功能特性列表',
`description` varchar(512) DEFAULT NULL COMMENT '套餐描述',
`sort` int DEFAULT '0' COMMENT '排序',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-下架 1-上架',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_package_code` (`package_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户套餐表';
5.2.3 数据源配置表(sys_datasource_config)
sql
CREATE TABLE `sys_datasource_config` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` bigint NOT NULL COMMENT '租户ID',
`ds_name` varchar(64) NOT NULL COMMENT '数据源名称',
`ds_type` varchar(32) NOT NULL DEFAULT 'mysql' COMMENT '数据源类型:mysql/postgresql/oracle',
`driver_class_name` varchar(128) NOT NULL COMMENT '驱动类名',
`url` varchar(512) NOT NULL COMMENT '连接URL',
`username` varchar(64) NOT NULL COMMENT '用户名',
`password` varchar(256) NOT NULL COMMENT '密码(AES加密)',
`initial_size` int DEFAULT '5' COMMENT '初始连接数',
`max_active` int DEFAULT '20' COMMENT '最大活跃连接数',
`min_idle` int DEFAULT '5' COMMENT '最小空闲连接数',
`max_wait` bigint DEFAULT '60000' COMMENT '获取连接最大等待时间(ms)',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用 1-启用',
`remark` varchar(256) DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
PRIMARY KEY (`id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据源配置表';
5.3 租户业务库核心表结构
以下表结构适用于「共享表模式」,每张业务表都包含
tenant_id字段用于租户隔离。
5.3.1 用户表(sys_user)
sql
CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`tenant_id` bigint NOT NULL COMMENT '租户ID',
`username` varchar(64) NOT NULL COMMENT '用户名',
`password` varchar(128) NOT NULL COMMENT '密码(BCrypt加密)',
`nickname` varchar(64) DEFAULT NULL COMMENT '昵称',
`real_name` varchar(64) DEFAULT NULL COMMENT '真实姓名',
`avatar` varchar(512) DEFAULT NULL COMMENT '头像URL',
`email` varchar(128) DEFAULT NULL COMMENT '邮箱',
`mobile` varchar(20) DEFAULT NULL COMMENT '手机号',
`gender` tinyint DEFAULT '0' COMMENT '性别:0-未知 1-男 2-女',
`user_type` tinyint NOT NULL DEFAULT '2' COMMENT '用户类型:1-租户管理员 2-普通用户 3-子账号',
`dept_id` bigint DEFAULT NULL COMMENT '部门ID',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用 1-正常',
`last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间',
`last_login_ip` varchar(64) DEFAULT NULL COMMENT '最后登录IP',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`creator` bigint DEFAULT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` bigint DEFAULT NULL COMMENT '更新人',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_username` (`tenant_id`, `username`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_mobile` (`mobile`),
KEY `idx_dept_id` (`dept_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
5.3.2 角色表(sys_role)
sql
CREATE TABLE `sys_role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`tenant_id` bigint NOT NULL COMMENT '租户ID',
`role_name` varchar(64) NOT NULL COMMENT '角色名称',
`role_code` varchar(64) NOT NULL COMMENT '角色编码',
`role_type` tinyint NOT NULL DEFAULT '2' COMMENT '角色类型:1-系统内置 2-自定义',
`data_scope` tinyint NOT NULL DEFAULT '1' COMMENT '数据权限范围:1-全部数据 2-本部门 3-本部门及以下 4-仅本人 5-自定义',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用 1-正常',
`sort` int DEFAULT '0' COMMENT '排序',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`creator` bigint DEFAULT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` bigint DEFAULT NULL COMMENT '更新人',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_role_code` (`tenant_id`, `role_code`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';
5.3.3 用户角色关联表(sys_user_role)
sql
CREATE TABLE `sys_user_role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` bigint NOT NULL COMMENT '租户ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`role_id` bigint NOT NULL COMMENT '角色ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_user_role` (`tenant_id`, `user_id`, `role_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色关联表';
5.3.4 部门表(sys_dept)
sql
CREATE TABLE `sys_dept` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '部门ID',
`tenant_id` bigint NOT NULL COMMENT '租户ID',
`parent_id` bigint NOT NULL DEFAULT '0' COMMENT '父部门ID',
`ancestors` varchar(512) DEFAULT '' COMMENT '祖级列表(逗号分隔)',
`dept_name` varchar(64) NOT NULL COMMENT '部门名称',
`dept_code` varchar(64) DEFAULT NULL COMMENT '部门编码',
`sort` int DEFAULT '0' COMMENT '排序',
`leader` varchar(64) DEFAULT NULL COMMENT '负责人',
`phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
`email` varchar(128) DEFAULT NULL COMMENT '邮箱',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-禁用 1-正常',
`creator` bigint DEFAULT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` bigint DEFAULT NULL COMMENT '更新人',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
PRIMARY KEY (`id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部门表';
5.3.5 商品表(biz_product)
sql
CREATE TABLE `biz_product` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`tenant_id` bigint NOT NULL COMMENT '租户ID',
`product_name` varchar(256) NOT NULL COMMENT '商品名称',
`product_no` varchar(64) NOT NULL COMMENT '商品编号',
`category_id` bigint DEFAULT NULL COMMENT '分类ID',
`brand_id` bigint DEFAULT NULL COMMENT '品牌ID',
`price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '销售价',
`market_price` decimal(10,2) DEFAULT '0.00' COMMENT '市场价',
`cost_price` decimal(10,2) DEFAULT '0.00' COMMENT '成本价',
`stock` int NOT NULL DEFAULT '0' COMMENT '库存数量',
`stock_warning` int DEFAULT '0' COMMENT '库存预警值',
`main_image` varchar(512) DEFAULT NULL COMMENT '主图URL',
`images` json DEFAULT NULL COMMENT '商品图片列表JSON',
`description` text COMMENT '商品描述',
`specs` json DEFAULT NULL COMMENT '规格JSON',
`attributes` json DEFAULT NULL COMMENT '属性JSON',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:0-下架 1-上架 2-售罄',
`sales_count` int DEFAULT '0' COMMENT '销量',
`sort` int DEFAULT '0' COMMENT '排序',
`creator` bigint DEFAULT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` bigint DEFAULT NULL COMMENT '更新人',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_product_no` (`tenant_id`, `product_no`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_category_id` (`category_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品表';
5.3.6 订单表(biz_order)
sql
CREATE TABLE `biz_order` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`tenant_id` bigint NOT NULL COMMENT '租户ID',
`order_no` varchar(32) NOT NULL COMMENT '订单编号',
`user_id` bigint NOT NULL COMMENT '下单用户ID',
`total_amount` decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '订单总金额',
`pay_amount` decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '实付金额',
`freight_amount` decimal(10,2) DEFAULT '0.00' COMMENT '运费',
`discount_amount` decimal(10,2) DEFAULT '0.00' COMMENT '优惠金额',
`order_status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态:0-待付款 1-待发货 2-已发货 3-已完成 4-已取消 5-退款中',
`pay_status` tinyint NOT NULL DEFAULT '0' COMMENT '支付状态:0-未支付 1-已支付 2-部分退款 3-全额退款',
`pay_time` datetime DEFAULT NULL COMMENT '支付时间',
`pay_type` tinyint DEFAULT NULL COMMENT '支付方式:1-微信 2-支付宝 3-银行卡',
`consignee_name` varchar(64) DEFAULT NULL COMMENT '收货人姓名',
`consignee_phone` varchar(20) DEFAULT NULL COMMENT '收货人电话',
`consignee_address` varchar(512) DEFAULT NULL COMMENT '收货地址',
`remark` varchar(512) DEFAULT NULL COMMENT '订单备注',
`creator` bigint DEFAULT NULL COMMENT '创建人',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` bigint DEFAULT NULL COMMENT '更新人',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_order_no` (`tenant_id`, `order_no`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_order_status` (`order_status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='订单表';
5.4 分库分表策略
大表分表策略(以订单表为例):
按 tenant_id 哈希分库(8个库)+ 按订单创建时间分表(每月一张)
db_order_0: biz_order_202607, biz_order_202608, ...
db_order_1: biz_order_202607, biz_order_202608, ...
...
db_order_7: biz_order_202607, biz_order_202608, ...
分库分表中间件:使用 ShardingSphere-JDBC
6. 租户隔离方案详解
6.1 租户标识传递机制
6.1.1 租户标识来源优先级
1. 请求域名(tenant.example.com)→ 最高优先级
2. 请求头(X-Tenant-Id / X-Tenant-Code)→ 次优先级
3. JWT Token 中的 tenantId 字段 → 默认
4. 请求参数(tenantId)→ 最低优先级(仅开发环境)
6.1.2 全链路传递流程
客户端请求
│
▼
[网关层] 解析租户标识 → 写入请求头 X-Tenant-Id
│
▼
[服务层] TenantInterceptor 拦截 → 写入 ThreadLocal
│
▼
[DAO层] MyBatis-Plus 多租户插件 → 自动拼接 tenant_id 条件
│
▼
[数据库] 按 tenant_id 隔离数据
6.2 MyBatis-Plus 多租户插件配置
java
/**
* MyBatis-Plus 多租户配置
*/
@Configuration
@MapperScan("com.example.**.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 多租户插件(必须放在最前面)
interceptor.addInnerInterceptor(tenantLineInnerInterceptor());
// 2. 分页插件
interceptor.addInnerInterceptor(paginationInnerInterceptor());
// 3. 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
@Bean
public TenantLineInnerInterceptor tenantLineInnerInterceptor() {
TenantLineInnerInterceptor interceptor = new TenantLineInnerInterceptor();
interceptor.setTenantLineHandler(new TenantLineHandler() {
@Override
public Expression getTenantId() {
// 从 ThreadLocal 获取当前租户ID
Long tenantId = TenantContextHolder.getTenantId();
if (tenantId == null) {
throw new BizException("租户信息不存在,请重新登录");
}
return new LongValue(tenantId);
}
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public boolean ignoreTable(String tableName) {
// 忽略不需要租户隔离的表(平台级表)
return TenantIgnoreTableHolder.isIgnored(tableName);
}
});
return interceptor;
}
@Bean
public PaginationInnerInterceptor paginationInnerInterceptor() {
PaginationInnerInterceptor interceptor = new PaginationInnerInterceptor(DbType.MYSQL);
interceptor.setMaxLimit(500L); // 单页最大条数
return interceptor;
}
}
6.3 租户上下文持有器
java
/**
* 租户上下文持有器
* 使用 TransmittableThreadLocal 支持线程池场景下的上下文传递
*/
public class TenantContextHolder {
private static final TransmittableThreadLocal<Long> TENANT_ID_HOLDER =
new TransmittableThreadLocal<>();
private static final TransmittableThreadLocal<String> TENANT_CODE_HOLDER =
new TransmittableThreadLocal<>();
private static final TransmittableThreadLocal<TenantInfo> TENANT_INFO_HOLDER =
new TransmittableThreadLocal<>();
public static void setTenantId(Long tenantId) {
TENANT_ID_HOLDER.set(tenantId);
}
public static Long getTenantId() {
return TENANT_ID_HOLDER.get();
}
public static void setTenantCode(String tenantCode) {
TENANT_CODE_HOLDER.set(tenantCode);
}
public static String getTenantCode() {
return TENANT_CODE_HOLDER.get();
}
public static void setTenantInfo(TenantInfo tenantInfo) {
TENANT_INFO_HOLDER.set(tenantInfo);
}
public static TenantInfo getTenantInfo() {
return TENANT_INFO_HOLDER.get();
}
public static void clear() {
TENANT_ID_HOLDER.remove();
TENANT_CODE_HOLDER.remove();
TENANT_INFO_HOLDER.remove();
}
}
6.4 租户拦截器
java
/**
* 租户上下文拦截器
* 在请求处理前设置租户上下文,请求结束后清理
*/
@Component
@Slf4j
public class TenantInterceptor implements HandlerInterceptor {
@Autowired
private TenantCacheService tenantCacheService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 1. 从请求头获取租户ID
String tenantIdStr = request.getHeader("X-Tenant-Id");
String tenantCode = request.getHeader("X-Tenant-Code");
Long tenantId = null;
// 2. 优先使用租户ID
if (StringUtils.hasText(tenantIdStr)) {
tenantId = Long.parseLong(tenantIdStr);
}
// 3. 其次使用租户编码查询
else if (StringUtils.hasText(tenantCode)) {
TenantInfo tenantInfo = tenantCacheService.getByCode(tenantCode);
if (tenantInfo != null) {
tenantId = tenantInfo.getId();
}
}
// 4. 从JWT Token中解析(兜底)
else {
tenantId = SecurityUtils.getTenantIdFromToken();
}
// 5. 设置租户上下文
if (tenantId != null) {
TenantContextHolder.setTenantId(tenantId);
TenantInfo tenantInfo = tenantCacheService.getById(tenantId);
if (tenantInfo != null) {
TenantContextHolder.setTenantInfo(tenantInfo);
TenantContextHolder.setTenantCode(tenantInfo.getTenantCode());
// 6. 校验租户状态
if (tenantInfo.getStatus() != 1) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(
Result.fail("租户状态异常,请联系管理员")));
return false;
}
}
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 请求结束后清理上下文,防止内存泄漏
TenantContextHolder.clear();
}
}
6.5 动态数据源切换(独立Schema租户)
java
/**
* 动态数据源路由
* 支持VIP租户切换到独立数据源
*/
@Component
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
@Autowired
private TenantCacheService tenantCacheService;
// 数据源缓存:tenantId -> DataSource
private final Map<Long, DataSource> tenantDataSourceMap = new ConcurrentHashMap<>();
@Override
protected Object determineCurrentLookupKey() {
Long tenantId = TenantContextHolder.getTenantId();
if (tenantId == null) {
return "default"; // 默认数据源(共享表模式)
}
TenantInfo tenantInfo = tenantCacheService.getById(tenantId);
if (tenantInfo != null && tenantInfo.getIsolationLevel() >= 2) {
// VIP租户使用独立数据源
return "tenant_" + tenantId;
}
return "default";
}
/**
* 动态添加租户数据源
*/
public void addTenantDataSource(Long tenantId, DataSourceProperties properties) {
if (tenantDataSourceMap.containsKey(tenantId)) {
return;
}
// 创建新的数据源
HikariDataSource dataSource = createDataSource(properties);
// 添加到动态数据源
Map<Object, Object> targetDataSources = new HashMap<>(
this.getResolvedDataSources());
targetDataSources.put("tenant_" + tenantId, dataSource);
this.setTargetDataSources(targetDataSources);
this.afterPropertiesSet();
tenantDataSourceMap.put(tenantId, dataSource);
log.info("已添加租户[{}]的独立数据源", tenantId);
}
private HikariDataSource createDataSource(DataSourceProperties properties) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(properties.getUrl());
dataSource.setUsername(properties.getUsername());
dataSource.setPassword(properties.getPassword());
dataSource.setDriverClassName(properties.getDriverClassName());
dataSource.setMaximumPoolSize(properties.getMaxActive());
dataSource.setMinimumIdle(properties.getMinIdle());
dataSource.setConnectionTimeout(properties.getMaxWait());
return dataSource;
}
}
7. 权限体系设计
7.1 权限模型:RBAC + 数据权限
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 用户 │────>│ 角色 │────>│ 权限 │
│ (User) │ N:M │ (Role) │ N:M │ (Perm) │
└─────────┘ └─────────┘ └─────────┘
│ │
│ ▼
│ ┌─────────┐
│ │ 数据权限 │
│ │(DataScope)
│ └─────────┘
▼
┌─────────┐
│ 部门 │
│ (Dept) │
└─────────┘
7.2 数据权限范围
| 范围 | 说明 | SQL条件示例 |
|---|---|---|
| 全部数据 | 可查看所有租户内数据 | 无额外条件 |
| 本部门 | 只能看本部门数据 | dept_id = 当前部门ID |
| 本部门及以下 | 本部门及子部门数据 | dept_id IN (当前部门及所有子部门ID) |
| 仅本人 | 只能看自己创建的数据 | creator = 当前用户ID |
| 自定义 | 可指定部门范围 | dept_id IN (指定的部门ID列表) |
7.3 数据权限拦截器实现
java
/**
* 数据权限拦截器
* 基于MyBatis-Plus插件实现数据行级权限控制
*/
@Component
@Slf4j
public class DataScopeInterceptor implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql) throws SQLException {
// 获取方法上的 @DataScope 注解
DataScope dataScope = getDataScopeAnnotation(ms);
if (dataScope == null) {
return;
}
// 获取当前用户的数据权限
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null) {
return;
}
// 管理员跳过数据权限
if (loginUser.isAdmin()) {
return;
}
// 构建数据权限SQL
String sql = boundSql.getSql();
String dataScopeSql = buildDataScopeSql(loginUser, dataScope);
if (StringUtils.hasText(dataScopeSql)) {
// 拼接数据权限条件
String newSql = "SELECT * FROM (" + sql + ") temp_data_scope WHERE " + dataScopeSql;
ReflectionUtils.setFieldValue(boundSql, "sql", newSql);
}
}
private String buildDataScopeSql(LoginUser loginUser, DataScope dataScope) {
Integer dataScopeType = loginUser.getDataScope();
String deptAlias = dataScope.deptAlias();
String userAlias = dataScope.userAlias();
StringBuilder sql = new StringBuilder();
switch (dataScopeType) {
case 1: // 全部数据
return "";
case 2: // 本部门
sql.append(deptAlias).append(".dept_id = ").append(loginUser.getDeptId());
break;
case 3: // 本部门及以下
List<Long> deptIds = getDeptAndChildIds(loginUser.getDeptId());
sql.append(deptAlias).append(".dept_id IN (")
.append(StringUtils.join(deptIds, ",")).append(")");
break;
case 4: // 仅本人
sql.append(userAlias).append(".user_id = ").append(loginUser.getUserId());
break;
case 5: // 自定义
List<Long> customDeptIds = loginUser.getCustomDeptIds();
if (CollectionUtils.isNotEmpty(customDeptIds)) {
sql.append(deptAlias).append(".dept_id IN (")
.append(StringUtils.join(customDeptIds, ",")).append(")");
} else {
sql.append("1=0"); // 无权限
}
break;
default:
sql.append("1=0"); // 默认无权限
}
return sql.toString();
}
}
7.4 菜单权限表设计
sql
-- 菜单表
CREATE TABLE `sys_menu` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
`tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户ID(0表示平台通用菜单)',
`parent_id` bigint NOT NULL DEFAULT '0' COMMENT '父菜单ID',
`menu_name` varchar(64) NOT NULL COMMENT '菜单名称',
`menu_type` tinyint NOT NULL COMMENT '菜单类型:1-目录 2-菜单 3-按钮',
`path` varchar(256) DEFAULT NULL COMMENT '路由路径',
`component` varchar(256) DEFAULT NULL COMMENT '组件路径',
`permission` varchar(128) DEFAULT NULL COMMENT '权限标识',
`icon` varchar(64) DEFAULT NULL COMMENT '菜单图标',
`sort` int DEFAULT '0' COMMENT '排序',
`visible` tinyint DEFAULT '1' COMMENT '是否可见:0-隐藏 1-显示',
`status` tinyint DEFAULT '1' COMMENT '状态:0-禁用 1-正常',
`is_platform` tinyint DEFAULT '0' COMMENT '是否平台菜单:0-否 1-是',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除',
PRIMARY KEY (`id`),
KEY `idx_tenant_id` (`tenant_id`),
KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单权限表';
-- 角色菜单关联表
CREATE TABLE `sys_role_menu` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` bigint NOT NULL COMMENT '租户ID',
`role_id` bigint NOT NULL COMMENT '角色ID',
`menu_id` bigint NOT NULL COMMENT '菜单ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_role_menu` (`tenant_id`, `role_id`, `menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色菜单关联表';
8. 核心业务模块设计
8.1 模块划分
multi-tenant-platform/
├── platform-gateway/ # 网关服务
├── platform-auth/ # 认证授权服务
├── platform-tenant/ # 租户管理服务
├── platform-system/ # 系统管理服务(用户/角色/菜单/部门)
├── platform-product/ # 商品服务
├── platform-order/ # 订单服务
├── platform-user/ # C端用户服务
├── platform-payment/ # 支付服务
├── platform-marketing/ # 营销服务
├── platform-file/ # 文件服务
├── platform-message/ # 消息服务
├── platform-search/ # 搜索服务
└── platform-common/ # 公共模块
├── common-core/ # 核心工具类
├── common-redis/ # Redis封装
├── common-mybatis/ # MyBatis封装(多租户插件)
├── common-security/ # 安全模块
├── common-feign/ # Feign封装
└── common-log/ # 日志模块
8.2 租户管理模块
8.2.1 租户创建流程
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 提交申请 │───>│ 平台审核 │───>│ 初始化租户│───>│ 开通成功 │
│ │ │ │ │ 数据 │ │ 发送通知 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│
▼
┌──────────────────────────────┐
│ 1. 创建租户记录 │
│ 2. 创建租户管理员账号 │
│ 3. 初始化角色/菜单/权限 │
│ 4. 初始化业务配置 │
│ 5. 创建独立Schema(VIP租户) │
│ 6. 分配资源配额 │
└──────────────────────────────┘
8.2.2 租户初始化服务
java
@Service
@Slf4j
public class TenantInitService {
@Autowired
private SysTenantMapper tenantMapper;
@Autowired
private SysUserMapper userMapper;
@Autowired
private SysRoleMapper roleMapper;
@Autowired
private DynamicDataSource dynamicDataSource;
/**
* 初始化新租户
*/
@Transactional(rollbackFor = Exception.class)
public Long initTenant(TenantInitDTO dto) {
// 1. 创建租户记录
SysTenant tenant = new SysTenant();
tenant.setTenantCode(dto.getTenantCode());
tenant.setTenantName(dto.getTenantName());
tenant.setTenantType(dto.getTenantType());
tenant.setIsolationLevel(dto.getIsolationLevel());
tenant.setStatus(1);
tenant.setExpireTime(calculateExpireTime(dto.getPackageId()));
tenantMapper.insert(tenant);
Long tenantId = tenant.getId();
try {
// 2. 如果是独立Schema模式,创建Schema
if (dto.getIsolationLevel() >= 2) {
createTenantSchema(tenantId, dto);
}
// 3. 初始化管理员账号
initAdminUser(tenantId, dto);
// 4. 初始化角色
initRoles(tenantId);
// 5. 初始化菜单权限
initMenus(tenantId, dto.getPackageId());
// 6. 初始化业务配置
initBusinessConfig(tenantId);
// 7. 发送开通通知
sendTenantCreatedNotification(tenant);
log.info("租户[{}]初始化成功", tenantId);
return tenantId;
} catch (Exception e) {
log.error("租户[{}]初始化失败", tenantId, e);
// 回滚:删除已创建的资源
rollbackTenantInit(tenantId, dto.getIsolationLevel());
throw new BizException("租户初始化失败: " + e.getMessage());
}
}
private void createTenantSchema(Long tenantId, TenantInitDTO dto) {
String schemaName = "tenant_" + tenantId;
// 1. 创建数据库
try (Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306?useSSL=false", "root", "password");
Statement stmt = conn.createStatement()) {
stmt.executeUpdate("CREATE DATABASE `" + schemaName +
"` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
// 2. 执行建表SQL脚本
runSchemaScript(schemaName, "classpath:db/tenant_schema.sql");
// 3. 注册到动态数据源
DataSourceProperties dsProps = new DataSourceProperties();
dsProps.setUrl("jdbc:mysql://localhost:3306/" + schemaName +
"?useUnicode=true&characterEncoding=utf8mb4");
dsProps.setUsername("root");
dsProps.setPassword("password");
dsProps.setDriverClassName("com.mysql.cj.jdbc.Driver");
dynamicDataSource.addTenantDataSource(tenantId, dsProps);
} catch (SQLException e) {
throw new BizException("创建租户Schema失败: " + e.getMessage());
}
}
private void initAdminUser(Long tenantId, TenantInitDTO dto) {
SysUser adminUser = new SysUser();
adminUser.setTenantId(tenantId);
adminUser.setUsername("admin");
adminUser.setPassword(passwordEncoder.encode("Admin@123")); // 初始密码
adminUser.setNickname("管理员");
adminUser.setUserType(1); // 租户管理员
adminUser.setStatus(1);
adminUser.setMobile(dto.getContactPhone());
adminUser.setEmail(dto.getContactEmail());
userMapper.insert(adminUser);
// 分配超级管理员角色
assignAdminRole(tenantId, adminUser.getId());
}
private void initRoles(Long tenantId) {
// 预置角色:超级管理员、运营、财务、客服等
List<SysRole> roles = Arrays.asList(
createRole(tenantId, "超级管理员", "super_admin", 1, 1),
createRole(tenantId, "运营管理员", "operation", 2, 1),
createRole(tenantId, "财务", "finance", 2, 4),
createRole(tenantId, "客服", "customer_service", 2, 4)
);
roles.forEach(roleMapper::insert);
}
}
8.3 认证授权模块
8.3.1 登录流程
用户输入用户名密码 + 租户标识
│
▼
┌─────────────────┐
│ 1. 解析租户标识 │
│ (域名/请求头) │
└────────┬────────┘
▼
┌─────────────────┐
│ 2. 查询用户信息 │
│ (tenant_id + │
│ username) │
└────────┬────────┘
▼
┌─────────────────┐
│ 3. 校验密码 │
│ (BCrypt匹配) │
└────────┬────────┘
▼
┌─────────────────┐
│ 4. 加载权限列表 │
│ (角色+菜单+按钮) │
└────────┬────────┘
▼
┌─────────────────┐
│ 5. 生成JWT Token│
│ (含tenantId) │
└────────┬────────┘
▼
┌─────────────────┐
│ 6. 返回登录结果 │
└─────────────────┘
8.3.2 JWT Token 结构
json
{
"sub": "1234567890",
"userId": 10001,
"tenantId": 1001,
"tenantCode": "acme_corp",
"username": "admin",
"userType": 1,
"authorities": ["system:user:list", "system:role:add", "..."],
"iat": 1722400000,
"exp": 1722486400
}
9. API设计规范
9.1 RESTful API 规范
9.1.1 URL 设计
# 平台管理API(平台管理员使用)
/api/platform/v1/tenants # 租户管理
/api/platform/v1/packages # 套餐管理
/api/platform/v1/statistics # 平台统计
# 租户业务API(租户内使用)
/api/{tenantCode}/v1/users # 用户管理
/api/{tenantCode}/v1/roles # 角色管理
/api/{tenantCode}/v1/products # 商品管理
/api/{tenantCode}/v1/orders # 订单管理
9.1.2 统一响应格式
json
{
"code": 200,
"message": "操作成功",
"data": {
// 具体业务数据
},
"traceId": "abc123def456",
"timestamp": 1722400000000
}
9.1.3 分页响应格式
json
{
"code": 200,
"message": "操作成功",
"data": {
"list": [],
"total": 100,
"pageNum": 1,
"pageSize": 10,
"pages": 10
}
}
9.2 网关层租户路由配置
yaml
spring:
cloud:
gateway:
routes:
# 平台管理路由
- id: platform-admin
uri: lb://platform-tenant
predicates:
- Path=/api/platform/**
filters:
- StripPrefix=2
# 租户业务路由(通过子域名识别租户)
- id: tenant-service
uri: lb://platform-system
predicates:
- Host={tenantCode}.example.com
- Path=/api/v1/**
filters:
- AddRequestHeader=X-Tenant-Code, {tenantCode}
- StripPrefix=1
# 商品服务路由
- id: product-service
uri: lb://platform-product
predicates:
- Host={tenantCode}.example.com
- Path=/api/v1/products/**
filters:
- AddRequestHeader=X-Tenant-Code, {tenantCode}
- StripPrefix=1
10. 缓存策略设计
10.1 缓存架构
┌─────────────────────────────────────────┐
│ 应用层本地缓存 (Caffeine) │
│ 租户配置 / 字典数据 / 菜单权限 │
├─────────────────────────────────────────┤
│ 分布式缓存 (Redis Cluster) │
│ 用户会话 / 热点数据 / 分布式锁 / 计数器 │
├─────────────────────────────────────────┤
│ 数据库 (MySQL) │
│ 持久化存储,最终一致 │
└─────────────────────────────────────────┘
10.2 缓存Key命名规范
# 格式:{业务前缀}:{租户ID}:{数据类型}:{唯一标识}
tenant:info:1001 # 租户基本信息
tenant:config:1001:business # 租户业务配置
user:info:1001:10001 # 用户信息
user:token:1001:abc123 # 用户Token
product:detail:1001:20001 # 商品详情
product:list:1001:category:100 # 商品列表
order:stat:1001:202607 # 订单统计
dict:type:1001:order_status # 字典数据
menu:user:1001:10001 # 用户菜单
10.3 缓存配置
java
@Configuration
public class CacheConfig {
@Bean
public Cache<String, Object> caffeineCache() {
return Caffeine.newBuilder()
.initialCapacity(1000)
.maximumSize(10000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.expireAfterAccess(10, TimeUnit.MINUTES)
.recordStats()
.build();
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key序列化:String
StringRedisSerializer keySerializer = new StringRedisSerializer();
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);
// Value序列化:JSON
Jackson2JsonRedisSerializer<Object> valueSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL);
valueSerializer.setObjectMapper(mapper);
template.setValueSerializer(valueSerializer);
template.setHashValueSerializer(valueSerializer);
template.afterPropertiesSet();
return template;
}
}
10.4 缓存与数据库一致性策略
| 场景 | 策略 | 说明 |
|---|---|---|
| 读操作 | Cache Aside | 先读缓存,未命中读DB,回写缓存 |
| 写操作 | 先更新DB,再删缓存 | 最终一致性,延迟双删 |
| 强一致性 | 分布式锁 + 缓存更新 | 适用于库存等强一致场景 |
| 统计数据 | 定时刷新 + 主动失效 | 适用于报表统计 |
11. 消息队列与异步处理
11.1 消息队列选型:RocketMQ
选型理由:
- 支持事务消息,保证分布式事务一致性
- 支持延时消息,适用于订单超时取消等场景
- 支持消息轨迹,便于排查问题
- 高吞吐、低延迟,适合电商场景
11.2 消息Topic设计
# 租户相关
TENANT_EVENTS # 租户生命周期事件(创建/停用/续费)
# 订单相关
ORDER_CREATED # 订单创建
ORDER_PAID # 订单支付
ORDER_CANCELLED # 订单取消
ORDER_DELAY_CHECK # 订单超时检查(延时消息)
# 用户相关
USER_REGISTERED # 用户注册
USER_LOGIN # 用户登录
# 支付相关
PAYMENT_CALLBACK # 支付回调
REFUND_REQUEST # 退款申请
# 通知相关
NOTIFICATION_EMAIL # 邮件通知
NOTIFICATION_SMS # 短信通知
NOTIFICATION_PUSH # 推送通知
# 数据同步
DATA_SYNC_ES # ES数据同步
DATA_SYNC_CACHE # 缓存数据同步
11.3 消息体规范
json
{
"msgId": "msg_20260731_abc123",
"tenantId": 1001,
"eventType": "ORDER_CREATED",
"timestamp": 1722400000000,
"source": "platform-order",
"data": {
"orderId": 20001,
"orderNo": "ORD202607310001",
"userId": 10001,
"amount": 299.00
}
}
12. 部署架构
12.1 生产环境部署拓扑
┌──────────────┐
│ CDN/WAF │
└──────┬───────┘
│
┌──────▼───────┐
│ Nginx集群 │ (负载均衡 + 静态资源)
└──────┬───────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Gateway │ │ Gateway │ │ Gateway │ (网关集群)
│ Pod x3 │ │ Pod x3 │ │ Pod x3 │
└─────┬────┘ └─────┬────┘ └─────┬────┘
└──────────────┼──────────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 认证服务 │ │ 用户服务 │ │ 订单服务 │ (业务服务集群)
│ Pod x2 │ │ Pod x3 │ │ Pod x4 │
└──────────┘ └──────────┘ └──────────┘
│ │ │
└────────────────┼────────────────┘
│
┌──────────┬───────────┼───────────┬──────────┐
▼ ▼ ▼ ▼ ▼
┌─────────┐┌─────────┐┌──────────┐┌─────────┐┌─────────┐
│ MySQL ││ Redis ││ RocketMQ ││ ES ││ MinIO │ (中间件集群)
│ 主从 ││ Cluster ││ Cluster ││ Cluster ││ Cluster │
└─────────┘└─────────┘└──────────┘└─────────┘└─────────┘
12.2 Kubernetes 部署配置示例
yaml
# 网关服务 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: platform-gateway
namespace: multi-tenant
spec:
replicas: 3
selector:
matchLabels:
app: platform-gateway
template:
metadata:
labels:
app: platform-gateway
spec:
containers:
- name: gateway
image: registry.example.com/multi-tenant/platform-gateway:v1.0.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "1Gi"
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: NACOS_SERVER_ADDR
value: "nacos.default.svc.cluster.local:8848"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 15
---
# Service
apiVersion: v1
kind: Service
metadata:
name: platform-gateway
namespace: multi-tenant
spec:
selector:
app: platform-gateway
ports:
- port: 80
targetPort: 8080
type: ClusterIP
---
# HPA 自动扩缩容
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: platform-gateway-hpa
namespace: multi-tenant
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: platform-gateway
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
12.3 数据库部署方案
MySQL 集群架构:
┌─────────────────────────────────────────┐
│ ProxySQL (读写分离) │
├──────────────┬──────────────────────────┤
│ 主库 (Master)│ 从库1 (Slave1) │
│ 写操作 │ 读操作 │
│ │ 半同步复制 │
├──────────────┼──────────────────────────┤
│ │ 从库2 (Slave2) │
│ │ 读操作 / 备份 │
│ │ 异步复制 │
└──────────────┴──────────────────────────┘
Redis 集群架构:
3主3从 Cluster 模式,每个主节点负责一部分slot
Sentinel 哨兵模式监控主从切换
13. 高可用与扩展性设计
13.1 高可用设计
13.1.1 服务层高可用
| 措施 | 说明 |
|---|---|
| 多实例部署 | 每个服务至少 2 个实例,避免单点故障 |
| 健康检查 | K8s readiness/liveness 探针,自动摘除异常实例 |
| 自动扩缩容 | HPA 根据 CPU/内存/QPS 自动扩缩容 |
| 熔断降级 | Sentinel 熔断保护,防止级联故障 |
| 限流 | 网关层 + 服务层双重限流,保护系统稳定 |
| 超时控制 | 全链路超时设置,避免资源耗尽 |
13.1.2 数据层高可用
| 组件 | 高可用方案 |
|---|---|
| MySQL | 主从复制 + MHA 自动故障转移 |
| Redis | Cluster 模式 + 哨兵监控 |
| RocketMQ | 多副本 + Dledger 自动选主 |
| Elasticsearch | 多节点 + 副本分片 |
| Nacos | 集群部署(至少3节点) |
13.2 扩展性设计
13.2.1 水平扩展
- 服务层:无状态设计,可无限水平扩展
- 数据层:分库分表,按 tenant_id 哈希分片
- 缓存层:Redis Cluster 模式,在线扩容
13.2.2 租户级资源隔离
# 资源池隔离策略
1. 连接池隔离
- 每个VIP租户独立数据库连接池
- 普通租户共享连接池,但设置租户级连接数上限
2. 线程池隔离
- 关键服务使用租户级线程池
- 防止单租户慢请求耗尽线程
3. 缓存隔离
- 按租户划分缓存namespace
- 设置租户级缓存容量上限
4. MQ隔离
- VIP租户使用独立Topic
- 普通租户共享Topic,但按租户ID消费
13.2.3 租户级限流配置
java
/**
* 租户级限流过滤器
*/
@Component
public class TenantRateLimitFilter implements GlobalFilter, Ordered {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String tenantCode = exchange.getRequest().getHeaders().getFirst("X-Tenant-Code");
if (StringUtils.hasText(tenantCode)) {
// 获取租户限流配置
TenantRateLimitConfig config = getTenantRateLimitConfig(tenantCode);
// 令牌桶限流
String key = "gateway:ratelimit:" + tenantCode;
Long count = redisTemplate.opsForValue().increment(key, 1);
if (count == 1) {
redisTemplate.expire(key, 1, TimeUnit.SECONDS);
}
if (count > config.getQpsLimit()) {
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return exchange.getResponse().setComplete();
}
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -100;
}
}
14. 安全设计
14.1 认证安全
| 措施 | 说明 |
|---|---|
| JWT Token | 无状态认证,含租户ID、用户ID、权限信息 |
| Token刷新 | Access Token(2小时) + Refresh Token(7天) |
| 密码加密 | BCrypt 加密,强度因子 12 |
| 登录保护 | 密码错误5次锁定15分钟,IP限流 |
| 双因素认证 | 支持短信/邮箱验证码二次验证 |
| HTTPS | 全站 HTTPS,TLS 1.3 |
14.2 数据安全
| 措施 | 说明 |
|---|---|
| 数据隔离 | 租户间数据严格隔离,MyBatis插件强制 |
| 敏感数据加密 | 手机号、身份证、银行卡等AES加密存储 |
| 数据脱敏 | 日志、接口返回中敏感数据脱敏 |
| 数据备份 | 每日全量备份 + 实时binlog备份 |
| 数据删除 | 逻辑删除 + 定期物理清理 |
| SQL注入防护 | MyBatis参数化查询 + 输入校验 |
14.3 接口安全
| 措施 | 说明 |
|---|---|
| 鉴权 | 基于RBAC的权限控制,接口级权限校验 |
| 签名 | 开放API使用签名验证,防止篡改 |
| 防重放 | 时间戳 + nonce 防止重放攻击 |
| XSS防护 | 输入过滤 + 输出转义 |
| CSRF防护 | Token验证 + SameSite Cookie |
| 接口限流 | 全局限流 + 租户级限流 + 用户级限流 |
14.4 敏感数据加密工具类
java
/**
* 敏感数据加密工具类
* 支持AES加密/解密,用于手机号、身份证等敏感字段
*/
@Component
public class SensitiveDataEncryptor {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 128;
private static final int IV_LENGTH = 12;
@Value("${security.encrypt.key}")
private String encryptKey;
/**
* 加密
*/
public String encrypt(String plainText) {
if (StringUtils.isEmpty(plainText)) {
return plainText;
}
try {
byte[] key = getKeyBytes();
SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
// 生成随机IV
byte[] iv = new byte[IV_LENGTH];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec);
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
// IV + 密文 一起存储(Base64编码)
byte[] result = new byte[IV_LENGTH + encrypted.length];
System.arraycopy(iv, 0, result, 0, IV_LENGTH);
System.arraycopy(encrypted, 0, result, IV_LENGTH, encrypted.length);
return Base64.getEncoder().encodeToString(result);
} catch (Exception e) {
throw new BizException("数据加密失败");
}
}
/**
* 解密
*/
public String decrypt(String cipherText) {
if (StringUtils.isEmpty(cipherText)) {
return cipherText;
}
try {
byte[] key = getKeyBytes();
SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
byte[] decoded = Base64.getDecoder().decode(cipherText);
// 提取IV
byte[] iv = new byte[IV_LENGTH];
System.arraycopy(decoded, 0, iv, 0, IV_LENGTH);
// 提取密文
byte[] encrypted = new byte[decoded.length - IV_LENGTH];
System.arraycopy(decoded, IV_LENGTH, encrypted, 0, encrypted.length);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec);
byte[] decrypted = cipher.doFinal(encrypted);
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new BizException("数据解密失败");
}
}
}
15. 监控与运维
15.1 监控体系
┌─────────────────────────────────────────────────────┐
│ Grafana 可视化 │
├──────────┬──────────┬──────────┬──────────┬─────────┤
│ 指标监控 │ 链路追踪 │ 日志监控 │ 业务监控 │ 告警通知 │
│Prometheus│SkyWalking│ ELK │ 自定义 │ AlertMgr│
└──────────┴──────────┴──────────┴──────────┴─────────┘
15.2 关键监控指标
15.2.1 系统指标
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| CPU使用率 | 服务CPU使用率 | > 80% 持续5分钟 |
| 内存使用率 | JVM堆内存使用率 | > 85% |
| GC次数/耗时 | Full GC频率和耗时 | Full GC > 5次/小时 |
| 线程数 | 活跃线程数 | > 500 |
| 连接池 | 数据库连接池使用率 | > 80% |
15.2.2 业务指标
| 指标 | 说明 |
|---|---|
| 租户数量 | 总租户数、新增租户数、活跃租户数 |
| 接口QPS | 总QPS、按租户QPS、按接口QPS |
| 接口响应时间 | P50/P95/P99 响应时间 |
| 错误率 | 接口错误率、按租户错误率 |
| 订单量 | 每日订单数、GMV |
15.2.3 租户级监控面板
# 每个租户独立监控视图
- 租户基本信息(套餐、到期时间、用户数)
- 接口调用量(日/周/月趋势)
- 接口响应时间(P50/P95/P99)
- 错误请求统计
- 资源使用情况(存储、API调用次数)
- 最近异常日志
15.3 日志规范
15.3.1 日志格式
json
{
"timestamp": "2026-07-31T10:00:00.000+08:00",
"level": "INFO",
"traceId": "abc123def456",
"spanId": "span789",
"tenantId": 1001,
"tenantCode": "acme_corp",
"userId": 10001,
"service": "platform-order",
"class": "com.example.order.service.OrderService",
"method": "createOrder",
"message": "订单创建成功",
"duration": 150,
"exception": null
}
15.3.2 MDC 配置
java
/**
* 日志MDC过滤器,注入租户信息和traceId
*/
@Component
public class MdcFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
try {
// 注入traceId
String traceId = httpRequest.getHeader("X-Trace-Id");
if (StringUtils.isEmpty(traceId)) {
traceId = IdUtil.fastSimpleUUID();
}
MDC.put("traceId", traceId);
// 注入租户信息
String tenantId = httpRequest.getHeader("X-Tenant-Id");
if (StringUtils.hasText(tenantId)) {
MDC.put("tenantId", tenantId);
}
// 注入用户ID
String userId = httpRequest.getHeader("X-User-Id");
if (StringUtils.hasText(userId)) {
MDC.put("userId", userId);
}
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
16. 实施路线图
16.1 分阶段实施计划
第一阶段:基础框架搭建(4周)
| 周次 | 任务 | 交付物 |
|---|---|---|
| 第1周 | 项目脚手架搭建、技术选型确认、代码规范制定 | 项目骨架、规范文档 |
| 第2周 | 网关服务、认证服务、租户管理服务开发 | 基础服务可用 |
| 第3周 | 多租户插件开发、权限体系开发 | 租户隔离验证通过 |
| 第4周 | 系统管理模块(用户/角色/菜单/部门)开发 | 后台管理基础功能 |
里程碑:完成基础框架,支持租户注册、登录、权限管理
第二阶段:核心业务开发(6周)
| 周次 | 任务 | 交付物 |
|---|---|---|
| 第5-6周 | 商品服务、分类管理、库存管理 | 商品模块完整功能 |
| 第7-8周 | 订单服务、购物车、支付对接 | 订单全流程打通 |
| 第9-10周 | 用户服务、营销服务、消息通知 | 核心业务闭环 |
里程碑:核心业务功能完整,可下单、支付、发货
第三阶段:高级特性开发(4周)
| 周次 | 任务 | 交付物 |
|---|---|---|
| 第11周 | 独立Schema模式支持、动态数据源 | VIP租户功能 |
| 第12周 | 数据权限、字段权限、审计日志 | 安全增强 |
| 第13周 | 搜索服务、报表统计、数据大屏 | 数据增值功能 |
| 第14周 | 租户套餐、计费系统、续费管理 | 商业化能力 |
里程碑:高级特性完成,支持多租户模式、商业化运营
第四阶段:优化与上线(4周)
| 周次 | 任务 | 交付物 |
|---|---|---|
| 第15周 | 性能压测、慢查询优化、缓存优化 | 性能达标 |
| 第16周 | 监控体系搭建、告警配置、压测调优 | 监控完善 |
| 第17周 | 安全测试、渗透测试、漏洞修复 | 安全合规 |
| 第18周 | 生产环境部署、灰度发布、上线验证 | 正式上线 |
里程碑:系统正式上线,稳定运行
16.2 团队配置建议
| 角色 | 人数 | 职责 |
|---|---|---|
| 架构师 | 1 | 技术架构设计、技术选型、核心代码 |
| 后端开发 | 4-6 | 业务服务开发 |
| 前端开发 | 2-3 | 管理后台 + 租户端页面 |
| 测试工程师 | 2 | 功能测试、性能测试、安全测试 |
| 运维工程师 | 1 | 部署、监控、运维 |
| 产品经理 | 1 | 需求分析、产品设计 |
17. 关键代码示例
17.1 统一响应结果
java
/**
* 统一响应结果
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> implements Serializable {
private Integer code;
private String message;
private T data;
private String traceId;
private Long timestamp;
public static <T> Result<T> success() {
return Result.<T>builder()
.code(ResultCode.SUCCESS.getCode())
.message(ResultCode.SUCCESS.getMessage())
.traceId(TraceIdUtil.get())
.timestamp(System.currentTimeMillis())
.build();
}
public static <T> Result<T> success(T data) {
Result<T> result = success();
result.setData(data);
return result;
}
public static <T> Result<T> fail(String message) {
return Result.<T>builder()
.code(ResultCode.FAIL.getCode())
.message(message)
.traceId(TraceIdUtil.get())
.timestamp(System.currentTimeMillis())
.build();
}
public static <T> Result<T> fail(Integer code, String message) {
return Result.<T>builder()
.code(code)
.message(message)
.traceId(TraceIdUtil.get())
.timestamp(System.currentTimeMillis())
.build();
}
}
17.2 全局异常处理
java
/**
* 全局异常处理器
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 业务异常
*/
@ExceptionHandler(BizException.class)
public Result<Void> handleBizException(BizException e) {
log.warn("业务异常: {}", e.getMessage());
return Result.fail(e.getCode(), e.getMessage());
}
/**
* 参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
log.warn("参数校验异常: {}", message);
return Result.fail(ResultCode.PARAM_ERROR.getCode(), message);
}
/**
* 权限异常
*/
@ExceptionHandler(AccessDeniedException.class)
public Result<Void> handleAccessDeniedException(AccessDeniedException e) {
log.warn("权限不足: {}", e.getMessage());
return Result.fail(ResultCode.FORBIDDEN.getCode(), "权限不足");
}
/**
* 未认证异常
*/
@ExceptionHandler(AuthenticationException.class)
public Result<Void> handleAuthenticationException(AuthenticationException e) {
log.warn("认证失败: {}", e.getMessage());
return Result.fail(ResultCode.UNAUTHORIZED.getCode(), "未登录或登录已过期");
}
/**
* 其他未知异常
*/
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e);
return Result.fail(ResultCode.SYSTEM_ERROR.getCode(), "系统繁忙,请稍后重试");
}
}
17.3 基础实体类
java
/**
* 租户基础实体类
* 所有租户业务表实体都继承此类
*/
@Data
public abstract class TenantBaseEntity implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 租户ID(由多租户插件自动填充,业务代码不需要手动设置)
*/
@TableField(fill = FieldFill.INSERT)
private Long tenantId;
@TableField(fill = FieldFill.INSERT)
private Long creator;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updater;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
@TableField(fill = FieldFill.INSERT)
private Integer deleted;
}
/**
* 自动填充处理器
*/
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 填充租户ID
if (metaObject.hasGetter("tenantId")) {
Long tenantId = TenantContextHolder.getTenantId();
if (tenantId != null) {
this.strictInsertFill(metaObject, "tenantId", Long.class, tenantId);
}
}
// 填充创建人
Long userId = SecurityUtils.getCurrentUserId();
this.strictInsertFill(metaObject, "creator", Long.class, userId);
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
// 填充更新人
this.strictInsertFill(metaObject, "updater", Long.class, userId);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
// 填充逻辑删除
this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
}
@Override
public void updateFill(MetaObject metaObject) {
Long userId = SecurityUtils.getCurrentUserId();
this.strictUpdateFill(metaObject, "updater", Long.class, userId);
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
17.4 Feign 租户信息传递
java
/**
* Feign 拦截器,传递租户信息
*/
@Component
public class FeignTenantInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 传递租户ID
Long tenantId = TenantContextHolder.getTenantId();
if (tenantId != null) {
template.header("X-Tenant-Id", String.valueOf(tenantId));
}
// 传递租户编码
String tenantCode = TenantContextHolder.getTenantCode();
if (StringUtils.hasText(tenantCode)) {
template.header("X-Tenant-Code", tenantCode);
}
// 传递TraceId
String traceId = MDC.get("traceId");
if (StringUtils.hasText(traceId)) {
template.header("X-Trace-Id", traceId);
}
// 传递用户Token
String token = SecurityUtils.getToken();
if (StringUtils.hasText(token)) {
template.header("Authorization", "Bearer " + token);
}
}
}
17.5 租户信息缓存服务
java
/**
* 租户信息缓存服务
*/
@Service
@Slf4j
public class TenantCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private SysTenantMapper tenantMapper;
private static final String TENANT_ID_KEY = "tenant:info:id:";
private static final String TENANT_CODE_KEY = "tenant:info:code:";
private static final long EXPIRE_HOURS = 24;
/**
* 根据ID获取租户信息
*/
public TenantInfo getById(Long tenantId) {
if (tenantId == null) {
return null;
}
String key = TENANT_ID_KEY + tenantId;
TenantInfo tenantInfo = (TenantInfo) redisTemplate.opsForValue().get(key);
if (tenantInfo == null) {
// 缓存未命中,从数据库加载
SysTenant tenant = tenantMapper.selectById(tenantId);
if (tenant != null) {
tenantInfo = convertToTenantInfo(tenant);
redisTemplate.opsForValue().set(key, tenantInfo, EXPIRE_HOURS, TimeUnit.HOURS);
}
}
return tenantInfo;
}
/**
* 根据编码获取租户信息
*/
public TenantInfo getByCode(String tenantCode) {
if (StringUtils.isEmpty(tenantCode)) {
return null;
}
String key = TENANT_CODE_KEY + tenantCode;
TenantInfo tenantInfo = (TenantInfo) redisTemplate.opsForValue().get(key);
if (tenantInfo == null) {
LambdaQueryWrapper<SysTenant> wrapper = Wrappers.lambdaQuery();
wrapper.eq(SysTenant::getTenantCode, tenantCode);
SysTenant tenant = tenantMapper.selectOne(wrapper);
if (tenant != null) {
tenantInfo = convertToTenantInfo(tenant);
redisTemplate.opsForValue().set(key, tenantInfo, EXPIRE_HOURS, TimeUnit.HOURS);
// 同时写入ID缓存
redisTemplate.opsForValue().set(TENANT_ID_KEY + tenant.getId(),
tenantInfo, EXPIRE_HOURS, TimeUnit.HOURS);
}
}
return tenantInfo;
}
/**
* 刷新租户缓存
*/
public void refreshCache(Long tenantId) {
SysTenant tenant = tenantMapper.selectById(tenantId);
if (tenant != null) {
TenantInfo tenantInfo = convertToTenantInfo(tenant);
// 删除旧缓存
redisTemplate.delete(TENANT_ID_KEY + tenantId);
redisTemplate.delete(TENANT_CODE_KEY + tenant.getTenantCode());
// 写入新缓存
redisTemplate.opsForValue().set(TENANT_ID_KEY + tenantId,
tenantInfo, EXPIRE_HOURS, TimeUnit.HOURS);
redisTemplate.opsForValue().set(TENANT_CODE_KEY + tenant.getTenantCode(),
tenantInfo, EXPIRE_HOURS, TimeUnit.HOURS);
}
}
private TenantInfo convertToTenantInfo(SysTenant tenant) {
TenantInfo info = new TenantInfo();
info.setId(tenant.getId());
info.setTenantCode(tenant.getTenantCode());
info.setTenantName(tenant.getTenantName());
info.setTenantType(tenant.getTenantType());
info.setIsolationLevel(tenant.getIsolationLevel());
info.setStatus(tenant.getStatus());
info.setExpireTime(tenant.getExpireTime());
info.setSchemaName(tenant.getSchemaName());
info.setMaxUserCount(tenant.getMaxUserCount());
info.setMaxStorageMb(tenant.getMaxStorageMb());
info.setThemeConfig(tenant.getThemeConfig());
info.setExtraConfig(tenant.getExtraConfig());
return info;
}
}
17.6 租户状态检查切面
java
/**
* 租户状态检查切面
* 在执行业务方法前检查租户状态是否正常
*/
@Aspect
@Component
@Slf4j
public class TenantStatusAspect {
@Autowired
private TenantCacheService tenantCacheService;
/**
* 切入点:所有带 @TenantCheck 注解的方法
*/
@Pointcut("@annotation(com.example.common.annotation.TenantCheck)")
public void tenantCheckPointcut() {}
@Around("tenantCheckPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Long tenantId = TenantContextHolder.getTenantId();
if (tenantId == null) {
throw new BizException("租户信息不存在");
}
TenantInfo tenantInfo = tenantCacheService.getById(tenantId);
if (tenantInfo == null) {
throw new BizException("租户不存在");
}
// 检查状态
if (tenantInfo.getStatus() == 0) {
throw new BizException("租户已被禁用,请联系管理员");
}
if (tenantInfo.getStatus() == 2) {
throw new BizException("租户服务已过期,请续费");
}
if (tenantInfo.getStatus() == 3) {
throw new BizException("租户已欠费,请充值");
}
// 检查到期时间
if (tenantInfo.getExpireTime() != null &&
tenantInfo.getExpireTime().isBefore(LocalDateTime.now())) {
throw new BizException("租户服务已到期,请续费");
}
return joinPoint.proceed();
}
}
附录
A. 项目依赖 pom.xml 关键依赖
xml
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- RocketMQ -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<!-- Security + JWT -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.25</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
B. 关键配置文件 application.yml
yaml
spring:
application:
name: platform-system
cloud:
nacos:
discovery:
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
namespace: multi-tenant
config:
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
namespace: multi-tenant
file-extension: yaml
datasource:
dynamic:
primary: master
datasource:
master:
url: jdbc:mysql://localhost:3306/tenant_shared?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
host: localhost
port: 6379
password: password
database: 0
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
type-aliases-package: com.example.**.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# 多租户配置
tenant:
# 忽略租户隔离的表名
ignore-tables:
- sys_tenant
- sys_tenant_package
- sys_datasource_config
- sys_dict_type
- sys_dict_data
# 默认租户ID(平台管理使用)
default-tenant-id: 0
# 是否开启租户校验
enable-check: true
# JWT配置
jwt:
secret: your-256-bit-secret-key-here-must-be-long-enough
expiration: 7200 # 2小时
refresh-expiration: 604800 # 7天
header: Authorization
prefix: "Bearer "
# 安全配置
security:
encrypt:
key: your-encryption-key-32bytes-long-here
# 放行的URL
ignore-urls:
- /api/auth/login
- /api/auth/register
- /api/auth/refresh-token
- /actuator/**
- /doc.html
- /webjars/**
- /v3/api-docs/**
logging:
level:
com.example: debug
org.springframework: info
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId}] [%X{tenantId}] %logger{50} - %msg%n"
文档说明:本架构设计文档基于 Java 17 + Spring Boot 3.x 技术栈,提供了从架构设计到代码实现的完整方案。实际项目中可根据业务需求进行调整和扩展。