JeecgBoot多租户架构:SaaS应用开发完整解决方案

JeecgBoot多租户架构:SaaS应用开发完整解决方案

【免费下载链接】JeecgBoot 🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~ 【免费下载链接】JeecgBoot 项目地址: https://gitcode.com/jeecgboot/JeecgBoot

引言:企业级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);
    }
}

多租户数据访问流程

数据访问流程图

mermaid

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应用开发提供了完整的解决方案,具有以下核心优势:

  1. 完善的数据隔离机制:通过tenant_id字段和MyBatis拦截器实现自动数据过滤
  2. 灵活的权限管理体系:支持用户-租户多对多关系,细粒度权限控制
  3. 高性能的架构设计:数据库索引优化、缓存策略、连接池配置全面提升性能
  4. 全面的安全审计:操作日志记录、安全监控、异常检测保障系统安全
  5. 便捷的运维部署:Docker容器化部署,监控告警一体化

随着云计算和SaaS模式的不断发展,JeecgBoot的多租户架构将继续演进,未来可能在以下方向进行增强:

  • 云原生支持:更好的Kubernetes集成和弹性伸缩能力
  • AI增强:智能租户资源分配和性能优化
  • 微服务化:更细粒度的服务拆分和租户隔离
  • 全球化部署:多地域数据合规和延迟优化

通过JeecgBoot的多租户架构,开发者可以快速构建安全、稳定、可扩展的企业级SaaS应用,显著降低开发成本和运维复杂度。

【免费下载链接】JeecgBoot 🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~ 【免费下载链接】JeecgBoot 项目地址: https://gitcode.com/jeecgboot/JeecgBoot

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值