1. 为什么需要多数据源在实际开发中我们经常会遇到需要同时操作多个数据库的场景。比如电商系统中用户数据和订单数据可能分别存储在不同的数据库中又或者企业级应用中业务数据和日志数据需要分开存储。这种需求在微服务架构中尤为常见。传统做法是使用Spring的AbstractRoutingDataSource来实现动态数据源切换但这种方式需要手动管理数据源路由代码侵入性强维护成本高。而MyBatis-Flex提供的多数据源方案通过注解驱动的方式让开发者可以零配置实现多数据源管理。我最近在一个供应链管理系统中就遇到了类似需求。系统需要同时连接ERP数据库、WMS数据库和财务数据库MyBatis-Flex的多数据源特性完美解决了这个问题。2. MyBatis-Flex多数据源核心配置2.1 基础环境搭建首先确保你的项目是基于SpringBoot 3.x的。我推荐使用以下技术栈组合dependencies !-- SpringBoot基础依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Flex SpringBoot3 Starter -- dependency groupIdcom.mybatis-flex/groupId artifactIdmybatis-flex-spring-boot3-starter/artifactId version1.10.9/version /dependency !-- MySQL驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- 连接池(推荐使用Druid) -- dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.24/version /dependency /dependencies2.2 多数据源配置在application.yml中配置多个数据源非常简单mybatis-flex: datasource: user_db: # 第一个数据源 url: jdbc:mysql://localhost:3306/user_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver order_db: # 第二个数据源 url: jdbc:mysql://localhost:3306/order_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver log_db: # 可以继续添加更多数据源 url: jdbc:mysql://localhost:3306/log_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver这里有个小技巧数据源名称建议使用有意义的英文命名避免使用特殊字符或中文这样在后续开发中更容易维护。3. 实体类与数据源绑定3.1 基本绑定方式MyBatis-Flex通过Table注解的dataSource属性来实现实体类与数据源的绑定Data Table(value user, dataSource user_db) public class User { Id(keyType KeyType.Auto) private Long id; private String username; private String password; // 其他字段... } Data Table(value order, dataSource order_db) public class Order { Id(keyType KeyType.Auto) private Long id; private Long userId; private BigDecimal amount; // 其他字段... }在实际项目中我建议将不同数据源的实体类放在不同的包下比如com.example.entity.user (用户库相关实体)com.example.entity.order (订单库相关实体)com.example.entity.log (日志库相关实体)这样代码结构更清晰便于团队协作。3.2 动态数据源切换在某些特殊场景下我们可能需要动态切换数据源。MyBatis-Flex提供了DataSourceKey注解来实现Service public class UserService { DataSourceKey(user_db) public User getUserById(Long id) { // 这个方法会自动使用user_db数据源 return userMapper.selectOneById(id); } DataSourceKey(order_db) public Order getOrderById(Long id) { // 这个方法会自动使用order_db数据源 return orderMapper.selectOneById(id); } }这种方式的优点是可以在Service层灵活控制数据源而不需要修改实体类定义。4. 跨库查询实战4.1 简单跨库查询虽然MyBatis-Flex不支持真正的跨库JOIN操作但我们可以通过编程方式实现类似功能public UserOrderDTO getUserWithOrders(Long userId) { // 先从用户库查询用户信息 User user userMapper.selectOneById(userId); // 再从订单库查询该用户的订单 ListOrder orders orderMapper.selectListByQuery( QueryWrapper.create() .where(Order::getUserId).eq(userId) ); // 组装结果 return new UserOrderDTO(user, orders); }这种方式的性能在大多数场景下已经足够特别是当配合缓存使用时。4.2 分页跨库查询对于需要分页的跨库查询我们需要特别注意public PageUserOrderDTO getUserOrdersPage(Long userId, PageUserOrderDTO page) { // 查询用户基本信息 User user userMapper.selectOneById(userId); // 查询订单分页数据 PageOrder orderPage orderMapper.paginate( page.getPageNumber(), page.getPageSize(), QueryWrapper.create() .where(Order::getUserId).eq(userId) ); // 组装结果 ListUserOrderDTO records orderPage.getRecords().stream() .map(order - new UserOrderDTO(user, order)) .collect(Collectors.toList()); return new Page( orderPage.getPageNumber(), orderPage.getPageSize(), orderPage.getTotalPage(), orderPage.getTotalRow(), records ); }这里有个坑我踩过不要试图在内存中做大数据量的分页处理这会导致性能问题。正确的做法是尽量在单个数据源内完成分页查询。5. 多数据源事务管理5.1 单数据源事务对于单个数据源的操作直接使用Spring的Transactional注解即可Transactional public void updateUserBalance(Long userId, BigDecimal amount) { User user userMapper.selectOneById(userId); user.setBalance(user.getBalance().add(amount)); userMapper.update(user); }5.2 分布式事务处理跨数据源的分布式事务是个复杂话题。MyBatis-Flex本身不提供分布式事务解决方案但可以集成Seata等框架首先添加Seata依赖dependency groupIdio.seata/groupId artifactIdseata-spring-boot-starter/artifactId version1.7.1/version /dependency然后配置Seataseata: enabled: true application-id: your-application-name tx-service-group: your-tx-group service: vgroup-mapping: your-tx-group: default config: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP最后在需要分布式事务的方法上添加注解GlobalTransactional public void placeOrder(OrderDTO orderDTO) { // 扣减用户余额 userMapper.deductBalance(orderDTO.getUserId(), orderDTO.getAmount()); // 创建订单 Order order new Order(); // 设置订单属性... orderMapper.insert(order); // 其他业务操作... }在实际项目中分布式事务会带来性能开销所以要谨慎使用。我通常的做法是尽量设计避免跨库事务或者使用最终一致性方案。6. 性能优化建议6.1 连接池配置多数据源环境下连接池配置尤为重要。以Druid为例mybatis-flex: datasource: user_db: url: jdbc:mysql://localhost:3306/user_db username: root password: 123456 druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false不同数据源可以根据业务特点配置不同的连接池参数。比如订单库通常并发较高可以适当增大max-active而日志库查询较多可以设置较小的连接数。6.2 二级缓存MyBatis-Flex支持多种缓存实现对于多数据源场景特别有用Configuration public class MyBatisFlexConfig { Bean public CacheFactory cacheFactory() { return new MybatisRedisCacheFactory(); // 使用Redis作为二级缓存 } }然后在Mapper接口上启用缓存Cache(expire 600) // 缓存10分钟 public interface UserMapper extends BaseMapperUser { // 方法定义... }缓存能显著减少跨库查询的压力特别是对于不常变动的数据。7. 常见问题排查7.1 数据源切换失败如果发现数据源没有按预期切换首先检查Table注解的dataSource值是否与配置中的数据源名称完全一致是否在同一个事务方法中尝试切换数据源事务方法内数据源是固定的是否有自定义的拦截器影响了数据源切换逻辑7.2 事务不生效跨数据源事务问题通常表现为部分操作没有回滚事务超时不生效解决方法确保使用的是支持分布式事务的解决方案如Seata检查事务超时时间设置是否合理确认数据库引擎是否支持事务如MyISAM不支持7.3 性能问题多数据源环境下常见的性能瓶颈连接池配置不合理频繁的跨库查询缺乏适当的缓存我的经验是定期监控各数据源的连接使用情况对慢查询进行分析优化合理使用缓存减少跨库操作。8. 最佳实践总结经过多个项目的实践我总结了以下多数据源使用经验命名规范要统一数据源名称、实体类包名、Mapper接口包名都应有清晰的约定事务设计要谨慎尽量避免跨库事务必要时使用最终一致性方案监控要到位对每个数据源的性能指标进行监控及时发现潜在问题文档要齐全在项目文档中明确记录各数据源的用途、配置参数等重要信息测试要充分特别是跨库操作和分布式事务场景要进行充分的集成测试一个实际项目中的目录结构参考src/main/java ├── com.example │ ├── config │ ├── controller │ ├── service │ ├── mapper │ │ ├── user # 用户库Mapper │ │ ├── order # 订单库Mapper │ │ └── log # 日志库Mapper │ └── entity │ ├── user # 用户库实体 │ ├── order # 订单库实体 │ └── log # 日志库实体这种结构清晰地区分了不同数据源的相关代码便于团队协作和维护。
MyBatis-Flex多数据源实战:SpringBoot3中实现跨库查询与事务管理
1. 为什么需要多数据源在实际开发中我们经常会遇到需要同时操作多个数据库的场景。比如电商系统中用户数据和订单数据可能分别存储在不同的数据库中又或者企业级应用中业务数据和日志数据需要分开存储。这种需求在微服务架构中尤为常见。传统做法是使用Spring的AbstractRoutingDataSource来实现动态数据源切换但这种方式需要手动管理数据源路由代码侵入性强维护成本高。而MyBatis-Flex提供的多数据源方案通过注解驱动的方式让开发者可以零配置实现多数据源管理。我最近在一个供应链管理系统中就遇到了类似需求。系统需要同时连接ERP数据库、WMS数据库和财务数据库MyBatis-Flex的多数据源特性完美解决了这个问题。2. MyBatis-Flex多数据源核心配置2.1 基础环境搭建首先确保你的项目是基于SpringBoot 3.x的。我推荐使用以下技术栈组合dependencies !-- SpringBoot基础依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Flex SpringBoot3 Starter -- dependency groupIdcom.mybatis-flex/groupId artifactIdmybatis-flex-spring-boot3-starter/artifactId version1.10.9/version /dependency !-- MySQL驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- 连接池(推荐使用Druid) -- dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.24/version /dependency /dependencies2.2 多数据源配置在application.yml中配置多个数据源非常简单mybatis-flex: datasource: user_db: # 第一个数据源 url: jdbc:mysql://localhost:3306/user_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver order_db: # 第二个数据源 url: jdbc:mysql://localhost:3306/order_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver log_db: # 可以继续添加更多数据源 url: jdbc:mysql://localhost:3306/log_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver这里有个小技巧数据源名称建议使用有意义的英文命名避免使用特殊字符或中文这样在后续开发中更容易维护。3. 实体类与数据源绑定3.1 基本绑定方式MyBatis-Flex通过Table注解的dataSource属性来实现实体类与数据源的绑定Data Table(value user, dataSource user_db) public class User { Id(keyType KeyType.Auto) private Long id; private String username; private String password; // 其他字段... } Data Table(value order, dataSource order_db) public class Order { Id(keyType KeyType.Auto) private Long id; private Long userId; private BigDecimal amount; // 其他字段... }在实际项目中我建议将不同数据源的实体类放在不同的包下比如com.example.entity.user (用户库相关实体)com.example.entity.order (订单库相关实体)com.example.entity.log (日志库相关实体)这样代码结构更清晰便于团队协作。3.2 动态数据源切换在某些特殊场景下我们可能需要动态切换数据源。MyBatis-Flex提供了DataSourceKey注解来实现Service public class UserService { DataSourceKey(user_db) public User getUserById(Long id) { // 这个方法会自动使用user_db数据源 return userMapper.selectOneById(id); } DataSourceKey(order_db) public Order getOrderById(Long id) { // 这个方法会自动使用order_db数据源 return orderMapper.selectOneById(id); } }这种方式的优点是可以在Service层灵活控制数据源而不需要修改实体类定义。4. 跨库查询实战4.1 简单跨库查询虽然MyBatis-Flex不支持真正的跨库JOIN操作但我们可以通过编程方式实现类似功能public UserOrderDTO getUserWithOrders(Long userId) { // 先从用户库查询用户信息 User user userMapper.selectOneById(userId); // 再从订单库查询该用户的订单 ListOrder orders orderMapper.selectListByQuery( QueryWrapper.create() .where(Order::getUserId).eq(userId) ); // 组装结果 return new UserOrderDTO(user, orders); }这种方式的性能在大多数场景下已经足够特别是当配合缓存使用时。4.2 分页跨库查询对于需要分页的跨库查询我们需要特别注意public PageUserOrderDTO getUserOrdersPage(Long userId, PageUserOrderDTO page) { // 查询用户基本信息 User user userMapper.selectOneById(userId); // 查询订单分页数据 PageOrder orderPage orderMapper.paginate( page.getPageNumber(), page.getPageSize(), QueryWrapper.create() .where(Order::getUserId).eq(userId) ); // 组装结果 ListUserOrderDTO records orderPage.getRecords().stream() .map(order - new UserOrderDTO(user, order)) .collect(Collectors.toList()); return new Page( orderPage.getPageNumber(), orderPage.getPageSize(), orderPage.getTotalPage(), orderPage.getTotalRow(), records ); }这里有个坑我踩过不要试图在内存中做大数据量的分页处理这会导致性能问题。正确的做法是尽量在单个数据源内完成分页查询。5. 多数据源事务管理5.1 单数据源事务对于单个数据源的操作直接使用Spring的Transactional注解即可Transactional public void updateUserBalance(Long userId, BigDecimal amount) { User user userMapper.selectOneById(userId); user.setBalance(user.getBalance().add(amount)); userMapper.update(user); }5.2 分布式事务处理跨数据源的分布式事务是个复杂话题。MyBatis-Flex本身不提供分布式事务解决方案但可以集成Seata等框架首先添加Seata依赖dependency groupIdio.seata/groupId artifactIdseata-spring-boot-starter/artifactId version1.7.1/version /dependency然后配置Seataseata: enabled: true application-id: your-application-name tx-service-group: your-tx-group service: vgroup-mapping: your-tx-group: default config: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: group: SEATA_GROUP最后在需要分布式事务的方法上添加注解GlobalTransactional public void placeOrder(OrderDTO orderDTO) { // 扣减用户余额 userMapper.deductBalance(orderDTO.getUserId(), orderDTO.getAmount()); // 创建订单 Order order new Order(); // 设置订单属性... orderMapper.insert(order); // 其他业务操作... }在实际项目中分布式事务会带来性能开销所以要谨慎使用。我通常的做法是尽量设计避免跨库事务或者使用最终一致性方案。6. 性能优化建议6.1 连接池配置多数据源环境下连接池配置尤为重要。以Druid为例mybatis-flex: datasource: user_db: url: jdbc:mysql://localhost:3306/user_db username: root password: 123456 druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false不同数据源可以根据业务特点配置不同的连接池参数。比如订单库通常并发较高可以适当增大max-active而日志库查询较多可以设置较小的连接数。6.2 二级缓存MyBatis-Flex支持多种缓存实现对于多数据源场景特别有用Configuration public class MyBatisFlexConfig { Bean public CacheFactory cacheFactory() { return new MybatisRedisCacheFactory(); // 使用Redis作为二级缓存 } }然后在Mapper接口上启用缓存Cache(expire 600) // 缓存10分钟 public interface UserMapper extends BaseMapperUser { // 方法定义... }缓存能显著减少跨库查询的压力特别是对于不常变动的数据。7. 常见问题排查7.1 数据源切换失败如果发现数据源没有按预期切换首先检查Table注解的dataSource值是否与配置中的数据源名称完全一致是否在同一个事务方法中尝试切换数据源事务方法内数据源是固定的是否有自定义的拦截器影响了数据源切换逻辑7.2 事务不生效跨数据源事务问题通常表现为部分操作没有回滚事务超时不生效解决方法确保使用的是支持分布式事务的解决方案如Seata检查事务超时时间设置是否合理确认数据库引擎是否支持事务如MyISAM不支持7.3 性能问题多数据源环境下常见的性能瓶颈连接池配置不合理频繁的跨库查询缺乏适当的缓存我的经验是定期监控各数据源的连接使用情况对慢查询进行分析优化合理使用缓存减少跨库操作。8. 最佳实践总结经过多个项目的实践我总结了以下多数据源使用经验命名规范要统一数据源名称、实体类包名、Mapper接口包名都应有清晰的约定事务设计要谨慎尽量避免跨库事务必要时使用最终一致性方案监控要到位对每个数据源的性能指标进行监控及时发现潜在问题文档要齐全在项目文档中明确记录各数据源的用途、配置参数等重要信息测试要充分特别是跨库操作和分布式事务场景要进行充分的集成测试一个实际项目中的目录结构参考src/main/java ├── com.example │ ├── config │ ├── controller │ ├── service │ ├── mapper │ │ ├── user # 用户库Mapper │ │ ├── order # 订单库Mapper │ │ └── log # 日志库Mapper │ └── entity │ ├── user # 用户库实体 │ ├── order # 订单库实体 │ └── log # 日志库实体这种结构清晰地区分了不同数据源的相关代码便于团队协作和维护。