当前位置: 首页 > news >正文

从零开始构建SaaS多租户架构:SpringBoot + MyBatis-Plus动态数据源实战

1. 为什么选择SpringBoot + MyBatis-Plus构建SaaS系统

最近公司要求将现有系统升级为SaaS架构,作为Java技术栈的团队,我们评估了多种方案后选择了SpringBoot + MyBatis-Plus组合。这个选择主要基于三个实际考量:首先,SpringBoot的自动配置和快速启动特性特别适合需要频繁部署的SaaS场景;其次,MyBatis-Plus在MyBatis基础上做了大量增强,特别是它的动态数据源功能正好满足多租户隔离的核心需求;最后,这两个框架在国内Java生态中普及度高,遇到问题容易找到解决方案。

我对比过几种常见的多租户实现方案,比如共享数据库独立Schema、独立数据库等。最终选择了独立数据库方案,虽然资源消耗较大,但数据隔离最彻底,安全性最高,特别适合金融、医疗等对数据敏感的场景。MyBatis-Plus Dynamic数据源模块完美支持这种需求,配置简单且性能稳定。

2. 项目初始化与环境搭建

2.1 创建基础项目结构

使用IDEA创建一个标准的SpringBoot项目,这里有个小技巧:通过Spring Initializr生成项目时,我只勾选了最基本的Web和Lombok依赖,其他依赖后续手动添加更灵活。项目结构保持Maven标准布局,特别注意resources目录下需要创建mapper文件夹存放XML文件。

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.7</version> </parent> <dependencies> <!-- 基础依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies> </project>

2.2 关键依赖配置

完整pom.xml需要添加几个核心依赖:首先是MyBatis-Plus全家桶,特别注意dynamic-datasource版本要与mybatis-plus-boot-starter保持一致;其次是Druid连接池,比HikariCP提供了更多监控功能;最后是p6spy用于SQL日志打印,调试时非常有用。

<dependencies> <!-- MyBatis-Plus核心 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <!-- 动态数据源 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.5.2</version> </dependency> <!-- Druid连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.11</version> </dependency> </dependencies>

3. 多租户数据源配置实战

3.1 数据库准备与YML配置

我准备了两个测试数据库saas_tenant1和saas_tenant2,每个库都有相同的sys_user表结构。application.yml配置是核心,dynamic.datasource下配置了master主库和tenant1、tenant2两个租户库。特别注意connection-timeout和validation-query等参数对生产环境很重要。

spring: datasource: dynamic: primary: master strict: true datasource: master: url: jdbc:mysql://localhost:3306/saas_base?useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver tenant1: url: jdbc:mysql://localhost:3306/saas_tenant1?useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver tenant2: url: jdbc:mysql://localhost:3306/saas_tenant2?useSSL=false username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver

3.2 租户上下文与数据源切换

实现租户隔离的关键是建立租户上下文,我通常使用ThreadLocal保存当前租户信息。创建TenantContextHolder工具类,配合自定义注解@DS实现动态切换。这里有个坑要注意:Spring的AOP代理会导致注解失效,解决方法是在Service方法内部调用时使用AopContext.currentProxy()。

public class TenantContext { private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>(); public static void setTenant(String tenant) { CURRENT_TENANT.set(tenant); } public static String getTenant() { return CURRENT_TENANT.get(); } public static void clear() { CURRENT_TENANT.remove(); } } // 使用示例 @Service public class UserServiceImpl implements UserService { @DS("#tenant") public User getUserById(Long id) { return userMapper.selectById(id); } }

4. 业务层实现与测试

4.1 租户识别与路由策略

实际项目中,租户信息通常来自JWT令牌或请求头。我实现了一个TenantInterceptor拦截器,从请求头X-TENANT-ID获取租户标识并存入上下文。对于没有租户标识的请求,可以返回错误或路由到默认租户,这取决于业务需求。

public class TenantInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String tenantId = request.getHeader("X-TENANT-ID"); if (StringUtils.isBlank(tenantId)) { throw new BusinessException("租户标识缺失"); } TenantContext.setTenant(tenantId); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { TenantContext.clear(); } }

4.2 接口测试与验证

使用Postman测试时,需要在Header中添加X-TENANT-ID: tenant1或tenant2。我编写了两个测试接口:/api/users/list返回当前租户下的用户列表,/api/users/add添加用户到当前租户库。测试时发现一个典型问题:事务注解会导致数据源切换失效,解决方法是在事务方法上显式指定@DS注解。

@RestController @RequestMapping("/api/users") public class UserController { @GetMapping("/list") public List<User> listUsers() { return userService.listAll(); } @PostMapping("/add") @DS("#tenant") // 显式指定数据源 @Transactional public void addUser(@RequestBody User user) { userService.save(user); } }

5. 生产环境进阶配置

5.1 多租户缓存隔离

单纯的数据库隔离还不够,Redis缓存也需要租户隔离。我的做法是在所有缓存key前添加租户前缀,如"tenant1:user:1001"。更优雅的方式是自定义RedisTemplate,自动注入租户信息。Spring Cache的CacheManager也需要相应改造。

public class TenantAwareRedisTemplate extends RedisTemplate<String, Object> { @Override public <T> T execute(RedisCallback<T> action, boolean exposeConnection, boolean pipeline) { String tenantPrefix = TenantContext.getTenant() + ":"; return super.execute(new TenantPrefixRedisCallback(tenantPrefix, action), exposeConnection, pipeline); } }

5.2 性能监控与调优

多数据源环境下,监控尤为重要。我配置了Druid的监控页面,可以查看每个数据源的连接池状态。对于高频访问场景,建议配置不同的连接池参数,比如核心业务库可以设置更大的maxActive。另外,p6spy的SQL日志要合理配置过滤条件,避免日志爆炸。

spring: datasource: druid: stat-view-servlet: enabled: true login-username: admin login-password: admin123 filter: stat: log-slow-sql: true slow-sql-millis: 1000

6. 常见问题解决方案

6.1 事务管理问题

在多数据源环境下,分布式事务是个难题。对于不强一致性的场景,我推荐使用最终一致性方案。如果必须使用XA事务,可以集成Seata框架。实际项目中,我们更多是通过业务设计避免跨库事务,比如将关联操作放在同一个租户库内。

@Service public class OrderService { @Transactional @DS("order") public void createOrder(Order order) { orderMapper.insert(order); // 调用库存服务 inventoryService.reduceStock(order.getProductId(), order.getQuantity()); } @Transactional @DS("inventory") public void reduceStock(Long productId, int quantity) { // 扣减库存 } }

6.2 动态添加租户

系统运行后可能需要动态添加新租户。我实现了一个TenantManagerService,通过DynamicDataSourceCreator在运行时注册新数据源。注意要同步更新所有相关缓存和上下文信息。生产环境建议配合配置中心实现动态刷新。

@Service public class TenantManagerServiceImpl implements TenantManagerService { @Autowired private DynamicRoutingDataSource dataSource; @Override public void addTenantDataSource(TenantConfig config) { DataSourceProperty property = new DataSourceProperty(); property.setUrl(config.getJdbcUrl()); property.setUsername(config.getUsername()); property.setPassword(config.getPassword()); DataSource newDataSource = dataSource.createDataSource(property); dataSource.addDataSource(config.getTenantId(), newDataSource); } }

在项目上线后,我们发现租户数量增长到50+时,连接池管理变得复杂。通过引入租户分组和懒加载机制,我们优化了资源占用。同时建立了租户数据源健康检查机制,自动隔离异常数据源。这些经验都是在实际踩坑后总结出来的,希望对准备实施SaaS改造的团队有所帮助。

http://www.jsqmd.com/news/661877/

相关文章:

  • 用Java Stream一行代码搞定彩票随机选号(双色球/大乐透)
  • Mysql--基础知识点--102--redo log内容
  • Kubernetes资源配额实战:LimitRange配置指南
  • PINN实战:从零构建一个偏微分方程求解器
  • 海洋CMS资源接口实战:XML数据格式与API调用详解
  • STM32 FOC电机库PID调参避坑指南:为什么你的定点参数调不好?
  • 邢台脱发白发理疗养发馆哪家好?黑奥秘参与行业标准制定,专业有据可依 - 美业信息观察
  • AMD平台ESXI 7.0实战:避坑部署Win11与TrueNAS虚拟化存储方案
  • Flask-Admin进阶指南:从基础增删改查到自定义视图和权限控制的完整配置流程
  • 从入门到实战:在UniApp中高效集成uCharts图表(组件与原生双模式详解)
  • 大模型应用开发实战(19)——Andrej Karpathy Skills 为什么突然火了?一份 CLAUDE.md,把 Claude Code 从“会写”拉回“会做事”
  • 2026年团鱼脚鱼甲鱼养殖基地推荐:中华鳖老鳖水鱼专业供应与回收服务选型指南 - 品牌推荐官
  • ContextMenuManager:Windows右键菜单终极解决方案,3个核心功能重塑你的操作效率
  • 别再傻傻地直接扫了!手把手教你用wafw00f在Windows和Kali上优雅地“试探”网站防火墙
  • Intel RealSense D435i数据采集避坑指南:Python脚本获取相机内参、外参并同步保存多传感器图像
  • TMSpeech:Windows本地实时语音识别工具终极配置指南
  • 2026年台式净饮机推荐:碧云泉G7S万相凭实力问鼎年度榜首 - 品牌企业推荐师(官方)
  • 设计模式系列目录
  • 如何用Open-Lyrics实现AI字幕生成与语音翻译:3步完成多语言转换
  • Mysql--基础知识点--101--在线扩容
  • 给企业开发者的MFI指南:当你的App需要连接Honeywell扫描枪时,info.plist和PPID该怎么填?
  • Infinity图像合成实战:如何用比特级建模提升你的AI画质(附GitHub代码)
  • 【技术解析】SwAV:用在线聚类与最优运输破解无监督视觉特征学习难题
  • 考不上高中怎么办,上海华科学校铸就别样精彩 - 品牌企业推荐师(官方)
  • 别再手动传数据了!用MatrikonOPC连接Matlab和NX MCD,实现自动化联合仿真
  • 远程生理信号监测终极指南:rPPG框架的完整实践教程
  • MOTR:基于Transformer的端到端多目标跟踪框架深度剖析
  • 仅限首批200家企业的AGI治理合规工具包泄露(源自2026奇点大会技术委员会内部推演)
  • ESP32 UI美化秘籍:手把手教你从阿里图标库(iconfont)扒图标,集成到LVGL界面里
  • ESP32的GPIO不够用?手把手教你用I2C和PCA9557扩展8个IO(附完整代码)