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.Driver3.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: 10006. 常见问题解决方案
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改造的团队有所帮助。