news 2026/4/18 17:31:18

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零开始构建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改造的团队有所帮助。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/18 17:30:23

Modbus RTU功能码的隐藏用法:如何用功能码05和06实现高级控制逻辑

Modbus RTU功能码的隐藏用法&#xff1a;如何用功能码05和06实现高级控制逻辑 在工业自动化领域&#xff0c;Modbus RTU协议因其简单可靠而广泛应用。大多数开发者对功能码05&#xff08;写单个线圈&#xff09;和06&#xff08;写单个寄存器&#xff09;的基础用法耳熟能详&a…

作者头像 李华
网站建设 2026/4/18 17:28:39

3个步骤掌握Electerm:跨平台终端与远程连接全能解决方案

3个步骤掌握Electerm&#xff1a;跨平台终端与远程连接全能解决方案 【免费下载链接】electerm &#x1f4fb;Terminal/ssh/sftp/ftp/telnet/serialport/RDP/VNC/Spice client(linux, mac, win) 项目地址: https://gitcode.com/gh_mirrors/el/electerm 还在为管理多个远…

作者头像 李华
网站建设 2026/4/18 17:26:26

开源MIT协议!Ostrakon-VL-8B像素特工终端GPU算力适配部署方案

开源MIT协议&#xff01;Ostrakon-VL-8B像素特工终端GPU算力适配部署方案 1. 项目概述 Pixel Agent: Ostrakon-VL零售扫描终端是一款基于Ostrakon-VL-8B多模态大模型开发的Web交互应用。该终端专为零售与餐饮行业设计&#xff0c;采用独特的8-bit像素艺术风格界面&#xff0c…

作者头像 李华
网站建设 2026/4/18 17:23:57

Outfit字体完全指南:9种字重的开源几何无衬线字体深度解析

Outfit字体完全指南&#xff1a;9种字重的开源几何无衬线字体深度解析 【免费下载链接】Outfit-Fonts The most on-brand typeface 项目地址: https://gitcode.com/gh_mirrors/ou/Outfit-Fonts Outfit字体是一款专业的开源几何无衬线字体&#xff0c;专为品牌自动化设计…

作者头像 李华
网站建设 2026/4/18 17:23:57

SI4735 Arduino库:多平台广播接收解决方案的技术实现与应用

SI4735 Arduino库&#xff1a;多平台广播接收解决方案的技术实现与应用 【免费下载链接】SI4735 SI473X Library for Arduino 项目地址: https://gitcode.com/gh_mirrors/si/SI4735 在嵌入式系统开发领域&#xff0c;构建高性能的广播接收系统一直面临诸多挑战&#xff…

作者头像 李华
网站建设 2026/4/18 17:19:17

macOS视频预览终极指南:用QLVideo解锁Finder隐藏功能

macOS视频预览终极指南&#xff1a;用QLVideo解锁Finder隐藏功能 【免费下载链接】QuickLookVideo This package allows macOS Finder to display thumbnails, static QuickLook previews, cover art and metadata for most types of video files. 项目地址: https://gitcode…

作者头像 李华