从零开始构建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 version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.6.7/version /parent dependencies !-- 基础依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependencies /project2.2 关键依赖配置完整pom.xml需要添加几个核心依赖首先是MyBatis-Plus全家桶特别注意dynamic-datasource版本要与mybatis-plus-boot-starter保持一致其次是Druid连接池比HikariCP提供了更多监控功能最后是p6spy用于SQL日志打印调试时非常有用。dependencies !-- MyBatis-Plus核心 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.2/version /dependency !-- 动态数据源 -- dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.5.2/version /dependency !-- Druid连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.11/version /dependency /dependencies3. 多租户数据源配置实战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?useSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver tenant1: url: jdbc:mysql://localhost:3306/saas_tenant1?useSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver tenant2: url: jdbc:mysql://localhost:3306/saas_tenant2?useSSLfalse 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 ThreadLocalString 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 ListUser 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 RedisTemplateString, Object { Override public T T execute(RedisCallbackT 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改造的团队有所帮助。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2530749.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!