JeecgBoot多租户架构:SaaS应用开发完整解决方案
引言:企业级SaaS应用的核心挑战
在数字化转型浪潮中,越来越多的企业选择SaaS(Software as a Service,软件即服务)模式来构建业务系统。然而,传统的单租户架构在面对多客户、多组织场景时面临着巨大挑战:数据隔离、资源分配、权限控制、定制化需求等痛点亟待解决。
JeecgBoot作为企业级低代码开发平台,提供了完整的**多租户(Multi-Tenancy)**架构解决方案,帮助开发者快速构建安全、稳定、可扩展的SaaS应用。本文将深入解析JeecgBoot的多租户实现机制,并提供完整的开发指南。
多租户架构核心概念
什么是多租户架构?
多租户架构是一种软件架构模式,允许多个租户(客户或组织)共享同一套应用程序实例,同时保持各自数据的隔离性和安全性。每个租户拥有独立的:
- 数据存储空间
- 配置设置
- 用户管理
- 权限体系
JeecgBoot多租户设计原则
JeecgBoot采用共享数据库、共享表结构的设计模式,通过tenant_id字段实现数据逻辑隔离,具有以下优势:
| 特性 | 描述 | 优势 |
|---|---|---|
| 数据隔离 | 基于租户ID的数据过滤 | 安全性高,维护简单 |
| 资源共享 | 共享应用实例和数据库 | 资源利用率高,成本低 |
| 灵活扩展 | 支持动态租户管理 | 易于扩展新租户 |
| 统一维护 | 集中式系统管理 | 运维效率高 |
JeecgBoot多租户核心实现
数据库表结构设计
JeecgBoot通过统一的tenant_id字段实现多租户数据隔离,核心表结构如下:
-- 租户信息表
CREATE TABLE `sys_tenant` (
`id` int(11) NOT NULL COMMENT '租户ID',
`name` varchar(100) DEFAULT NULL COMMENT '租户名称',
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`begin_date` datetime DEFAULT NULL COMMENT '开始时间',
`end_date` datetime DEFAULT NULL COMMENT '结束时间',
`status` int(1) DEFAULT NULL COMMENT '状态',
`trade` varchar(50) DEFAULT NULL COMMENT '所属行业',
`company_size` varchar(50) DEFAULT NULL COMMENT '公司规模',
`del_flag` int(1) DEFAULT '0' COMMENT '删除标志',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 业务表示例(包含tenant_id字段)
CREATE TABLE `sys_user` (
`id` varchar(32) NOT NULL,
`username` varchar(100) DEFAULT NULL COMMENT '登录账号',
`realname` varchar(100) DEFAULT NULL COMMENT '真实姓名',
`tenant_id` varchar(32) DEFAULT NULL COMMENT '租户ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
核心Java实体类
// 租户实体类
@Data
@TableName("sys_tenant")
public class SysTenant implements Serializable {
private Integer id; // 租户ID
private String name; // 租户名称
private String createBy; // 创建人
private Date createTime; // 创建时间
private Date beginDate; // 开始时间
private Date endDate; // 结束时间
private Integer status; // 状态
private String trade; // 所属行业
private String companySize; // 公司规模
private Integer delFlag; // 删除标志
}
// 业务实体基类(包含tenant_id字段)
public class BaseEntity {
private String tenantId; // 租户ID字段
// 其他公共字段...
}
MyBatis多租户拦截器实现
核心配置类
JeecgBoot通过MybatisPlusSaasConfig配置类实现多租户数据过滤:
@Configuration
public class MybatisPlusSaasConfig {
// 需要租户隔离的表配置
public static final List<String> TENANT_TABLE = new ArrayList<>();
static {
// 系统管理表
TENANT_TABLE.add("sys_depart");
TENANT_TABLE.add("sys_category");
TENANT_TABLE.add("sys_data_source");
// 业务表
TENANT_TABLE.add("airag_app");
TENANT_TABLE.add("airag_flow");
// 更多表配置...
}
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 租户拦截器
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
// 获取当前租户ID
String tenantId = TenantContext.getTenant();
if(StringUtils.isEmpty(tenantId)){
tenantId = TokenUtils.getTenantIdByRequest(
SpringContextUtils.getHttpServletRequest());
}
if(StringUtils.isEmpty(tenantId)){
tenantId = "0"; // 默认租户
}
return new LongValue(tenantId);
}
@Override
public String getTenantIdColumn(){
return "tenant_id"; // 租户字段名
}
@Override
public boolean ignoreTable(String tableName) {
// 判断表是否需要租户隔离
return !TENANT_TABLE.contains(tableName);
}
}));
return interceptor;
}
}
租户上下文管理
// 租户上下文工具类
public class TenantContext {
private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
public static void setTenant(String tenant) {
currentTenant.set(tenant);
}
public static String getTenant() {
return currentTenant.get();
}
public static void clear() {
currentTenant.remove();
}
}
// 租户ID解析器
public class TenantIdParser {
public static Integer parseTenantId(HttpServletRequest request) {
// 从请求头、token、参数中解析租户ID
String tenantId = request.getHeader("X-Tenant-Id");
if (StringUtils.isEmpty(tenantId)) {
tenantId = request.getParameter("tenantId");
}
return Integer.parseInt(tenantId);
}
}
多租户服务层实现
租户管理服务接口
public interface ISysTenantService extends IService<SysTenant> {
// 查询有效租户
List<SysTenant> queryEffectiveTenant(Collection<Integer> idList);
// 统计租户用户数量
Long countUserLinkTenant(String id);
// 删除租户(包含引用检查)
boolean removeTenantById(String id);
// 邀请用户加入租户
void invitationUserJoin(String ids, String phone, String username);
// 添加租户并关联用户
Integer saveTenantJoinUser(SysTenant sysTenant, String userId);
// 通过门牌号加入租户
Integer joinTenantByHouseNumber(SysTenant sysTenant, String userId);
// 获取用户所属租户列表
List<SysTenant> getTenantListByUserId(String userId);
}
服务实现示例
@Service
public class SysTenantServiceImpl implements ISysTenantService {
@Autowired
private SysTenantMapper sysTenantMapper;
@Autowired
private SysUserTenantService sysUserTenantService;
@Override
@Transactional
public Integer saveTenantJoinUser(SysTenant sysTenant, String userId) {
// 1. 保存租户信息
this.save(sysTenant);
// 2. 建立用户-租户关联
SysUserTenant userTenant = new SysUserTenant();
userTenant.setUserId(userId);
userTenant.setTenantId(sysTenant.getId());
userTenant.setStatus(1); // 激活状态
sysUserTenantService.save(userTenant);
return sysTenant.getId();
}
@Override
public List<SysTenant> getTenantListByUserId(String userId) {
// 查询用户所属的所有租户
return sysTenantMapper.selectTenantsByUserId(userId);
}
}
多租户数据访问流程
数据访问流程图
SQL自动过滤机制
JeecgBoot的多租户拦截器会自动重写SQL语句:
原始SQL:
SELECT * FROM sys_user WHERE username = 'admin'
拦截后SQL:
SELECT * FROM sys_user
WHERE username = 'admin' AND tenant_id = '1001'
租户权限管理体系
用户-租户关系模型
// 用户租户关联实体
@Data
@TableName("sys_user_tenant")
public class SysUserTenant {
private String id;
private String userId; // 用户ID
private Integer tenantId; // 租户ID
private Integer status; // 状态
private Integer isAdmin; // 是否管理员
private Date createTime;
}
// 租户产品包权限
@Data
@TableName("sys_tenant_pack")
public class SysTenantPack {
private String id;
private Integer tenantId; // 租户ID
private String packCode; // 产品包编码
private String packName; // 产品包名称
private Integer status; // 状态
}
权限控制代码示例
// 租户权限拦截器
@Component
public class TenantAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 获取当前用户ID
String userId = JwtUtil.getUserId(request);
// 获取请求中的租户ID
Integer tenantId = TenantIdParser.parseTenantId(request);
// 验证用户是否有该租户的访问权限
boolean hasPermission = sysUserTenantService
.checkUserTenantPermission(userId, tenantId);
if (!hasPermission) {
throw new BusinessException("无权限访问该租户数据");
}
// 设置当前租户上下文
TenantContext.setTenant(tenantId.toString());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
// 清理租户上下文
TenantContext.clear();
}
}
多租户业务场景实现
场景1:用户登录与租户选择
// 用户登录服务
@Service
public class LoginService {
public LoginResult login(String username, String password) {
// 1. 验证用户 credentials
SysUser user = userService.getUserByUsername(username);
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new AuthenticationException("密码错误");
}
// 2. 获取用户所属租户列表
List<SysTenant> tenants = tenantService.getTenantListByUserId(user.getId());
// 3. 生成JWT token(包含用户ID)
String token = jwtUtil.generateToken(user.getId());
return new LoginResult(token, tenants);
}
}
// 租户选择后设置上下文
@PostMapping("/select-tenant")
public Result selectTenant(@RequestParam Integer tenantId) {
// 验证用户是否有该租户权限
String userId = getCurrentUserId();
if (!userTenantService.hasTenantPermission(userId, tenantId)) {
return Result.error("无权限访问该租户");
}
// 设置当前租户到会话或token中
TenantContext.setTenant(tenantId.toString());
return Result.ok("租户切换成功");
}
场景2:跨租户数据导出
// 多租户数据导出服务
@Service
public class MultiTenantExportService {
@Transactional(readOnly = true)
public void exportData(Integer sourceTenantId, Integer targetTenantId,
String dataType) {
// 保存当前租户上下文
String originalTenant = TenantContext.getTenant();
try {
// 切换到源租户查询数据
TenantContext.setTenant(sourceTenantId.toString());
List<?> sourceData = queryDataByType(dataType);
// 切换到目标租户保存数据
TenantContext.setTenant(targetTenantId.toString());
saveDataToTarget(sourceData, dataType);
} finally {
// 恢复原始租户上下文
TenantContext.setTenant(originalTenant);
}
}
private List<?> queryDataByType(String dataType) {
switch (dataType) {
case "user":
return userService.list();
case "department":
return departmentService.list();
default:
throw new BusinessException("不支持的数据类型");
}
}
}
性能优化与最佳实践
数据库索引优化
-- 为租户字段创建索引
CREATE INDEX idx_tenant_id ON sys_user(tenant_id);
CREATE INDEX idx_tenant_id ON sys_depart(tenant_id);
CREATE INDEX idx_tenant_id ON sys_role(tenant_id);
-- 复合索引优化查询性能
CREATE INDEX idx_tenant_user ON sys_user(tenant_id, username);
CREATE INDEX idx_tenant_status ON sys_user(tenant_id, status);
缓存策略设计
// 多租户缓存管理器
@Component
public class TenantAwareCacheManager {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 生成租户感知的缓存key
private String getTenantKey(String originalKey) {
String tenantId = TenantContext.getTenant();
return String.format("tenant:%s:%s", tenantId, originalKey);
}
public void put(String key, Object value, long timeout) {
String tenantKey = getTenantKey(key);
redisTemplate.opsForValue().set(tenantKey, value, timeout, TimeUnit.SECONDS);
}
public Object get(String key) {
String tenantKey = getTenantKey(key);
return redisTemplate.opsForValue().get(tenantKey);
}
}
连接池优化配置
# application.yml 多租户数据库配置
spring:
datasource:
dynamic:
primary: master
strict: false
datasource:
master:
url: jdbc:mysql://localhost:3306/jeecg_boot?useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 连接池配置
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
安全考虑与数据隔离
数据隔离级别
JeecgBoot支持多种数据隔离策略:
| 隔离级别 | 实现方式 | 适用场景 | 优缺点 |
|---|---|---|---|
| 数据库级 | 不同租户使用不同数据库 | 金融、政府等高安全要求 | 安全性最高,成本高 |
| Schema级 | 同一数据库不同schema | 中型企业应用 | 平衡安全与成本 |
| 表级 | 共享表通过tenant_id隔离 | 通用SaaS应用 | 成本低,需要严格权限控制 |
安全审计日志
// 多租户操作日志记录
@Aspect
@Component
public class TenantAuditAspect {
@Autowired
private SysLogService logService;
@Pointcut("execution(* org.jeecg.modules..service.*.*(..))")
public void servicePointcut() {}
@Around("servicePointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String tenantId = TenantContext.getTenant();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
// 记录操作日志
SysLog auditLog = new SysLog();
auditLog.setTenantId(tenantId);
auditLog.setMethod(methodName);
auditLog.setParams(JSON.toJSONString(args));
auditLog.setCreateTime(new Date());
try {
Object result = joinPoint.proceed();
auditLog.setStatus(1); // 成功
return result;
} catch (Exception e) {
auditLog.setStatus(0); // 失败
auditLog.setErrorMsg(e.getMessage());
throw e;
} finally {
logService.save(auditLog);
}
}
}
部署与运维指南
Docker多租户部署
# docker-compose.yml 多租户部署配置
version: '3.8'
services:
jeecg-boot:
image: jeecg-boot:latest
environment:
- SPRING_PROFILES_ACTIVE=prod
- TENANT_MODE=multi
- DB_HOST=mysql
- DB_PORT=3306
- DB_NAME=jeecg_boot
- REDIS_HOST=redis
ports:
- "8080:8080"
depends_on:
- mysql
- redis
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=123456
- MYSQL_DATABASE=jeecg_boot
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6.2
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
mysql_data:
redis_data:
监控与告警配置
# Prometheus 多租户监控配置
scrape_configs:
- job_name: 'jeecg-boot'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
relabel_configs:
- source_labels: [__address__]
target_label: instance
- source_labels: [__meta_tenant_id]
target_label: tenant_id
# 租户级监控告警规则
groups:
- name: tenant.rules
rules:
- alert: HighTenantCpuUsage
expr: process_cpu_usage{tenant_id!=""} > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "租户 {{ $labels.tenant_id }} CPU使用率过高"
description: "租户 {{ $labels.tenant_id }} 的CPU使用率达到 {{ $value }}%"
总结与展望
JeecgBoot的多租户架构为企业级SaaS应用开发提供了完整的解决方案,具有以下核心优势:
- 完善的数据隔离机制:通过
tenant_id字段和MyBatis拦截器实现自动数据过滤 - 灵活的权限管理体系:支持用户-租户多对多关系,细粒度权限控制
- 高性能的架构设计:数据库索引优化、缓存策略、连接池配置全面提升性能
- 全面的安全审计:操作日志记录、安全监控、异常检测保障系统安全
- 便捷的运维部署:Docker容器化部署,监控告警一体化
随着云计算和SaaS模式的不断发展,JeecgBoot的多租户架构将继续演进,未来可能在以下方向进行增强:
- 云原生支持:更好的Kubernetes集成和弹性伸缩能力
- AI增强:智能租户资源分配和性能优化
- 微服务化:更细粒度的服务拆分和租户隔离
- 全球化部署:多地域数据合规和延迟优化
通过JeecgBoot的多租户架构,开发者可以快速构建安全、稳定、可扩展的企业级SaaS应用,显著降低开发成本和运维复杂度。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



