MyBatis-Plus 多数据源配置实战:从读写分离到分库分表​

0 阅读7分钟

MyBatis-Plus 多数据源配置实战:从读写分离到分库分表

作为在分布式系统摸爬滚打七年的老兵,我深知多数据源场景是后端开发的必经之路。无论是读写分离优化性能,还是分库分表应对海量数据,亦或是整合第三方异构数据源,MyBatis-Plus 的多数据源配置都是核心解决方案。本文将通过银行核心系统的真实业务场景,带你从 0 到 1 掌握多数据源配置全流程,包含实战代码、坑点解析和最佳实践。

一、为什么需要多数据源?先搞懂业务场景

典型应用场景(附银行案例)

  1. 读写分离(90% 的场景)
    • 主库(master)写:账户开户、转账等写操作
    • 从库(slave)读:账户余额查询、交易流水统计场景:每日千万级查询流量,通过读写分离减轻主库压力
  1. 分库分表(数据量超 5000 万)
    • 按用户 ID 分库:user_0、user_1 库(尾号 0-4/5-9)
    • 按时间分表:trade_2023、trade_2024 表(按年份拆分)场景:银行交易表日均百万条,分库分表提升查询效率
  1. 多业务系统集成
    • 核心业务库(账户、交易)
    • 报表统计库(OLAP 分析)
    • 第三方对接库(支付渠道、征信系统)场景:统一服务同时访问多个独立数据库

技术方案对比(新手必看)

方案优点缺点适用场景
原生 JDBC 切换可控性强代码侵入性高,重复劳动多简单双数据源
AbstractRoutingDataSourceSpring 原生支持配置繁琐,功能有限基础读写分离
DynamicDataSource零代码入侵,支持动态切换依赖第三方库(需选靠谱的)复杂多数据源场景

推荐方案:使用 Dynamic Data Source(MyBatis-Plus 官方推荐),10 行配置实现动态数据源切换,兼容所有 Spring Boot 版本。

二、实战准备:搭建多数据源项目骨架

1. 添加核心依赖(避开版本陷阱)

<dependencies>
    <!-- 原有依赖:Spring Web、MyBatis-Plus、MySQL驱动、Lombok -->
    <!-- 新增多数据源依赖(注意版本匹配) -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
        <version>3.5.0</version> <!-- 2025年稳定版,与MyBatis-Plus 4.5+兼容 -->
    </dependency>
    <!-- 若需支持XA分布式事务,添加以下依赖(本案例暂不涉及) -->
    <!-- <dependency>groupId>com.atomikos</groupId><artifactId>transactions-jdbc</artifactId></dependency> -->
</dependencies>

2. 目录结构规划(清晰的工程规范)

src/main/java/com/example
├── annotation       # 自定义数据源注解
│   └── DS.java
├── config           # 多数据源配置类
│   ├── DataSourceConfig.java
│   └── MyBatisPlusConfig.java
├── entity           # 实体类(不同数据源可分模块,如core/stat)
├── mapper           # Mapper接口(分库时按库名分组,如master/slave)
├── service          # 业务层(包含数据源切换逻辑)
└── controller       # 控制层

三、核心配置:3 步搞定读写分离(以主从库为例)

1. application.yml 配置(关键细节解析)

spring:
  datasource:
    dynamic:
      primary: master       # 默认数据源(写操作)
      strict: false         # 严格模式,未定义数据源时抛异常(生产环境建议true)
      datasource:
        master:             # 主库(写)
          url: jdbc:mysql://localhost:3306/core_db?serverTimezone=Asia/Shanghai
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: root
          password: 123456
          type: com.zaxxer.hikari.HikariDataSource  # 连接池(性能优于Tomcat默认)
          hikari:
            minimum-idle: 5                       # 最小空闲连接数
            maximum-pool-size: 20                 # 最大连接数
        slave:              # 从库(读)
          url: jdbc:mysql://localhost:3306/core_db_slave?serverTimezone=Asia/Shanghai
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: root
          password: 123456
          type: com.zaxxer.hikari.HikariDataSource
mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml  # 通用Mapper路径
  global-config:
    db-config:
      table-prefix: tb_                          # 表名前缀(所有数据源统一)
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 打印SQL日志(区分数据源)

2. 自定义数据源注解(优雅的切换方式)

package com.example.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE})  // 可用于方法或类
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {
    String value() default "";  // 数据源名称(如"master"、"slave")
}

3. Service 层实现数据源切换(零侵入代码)

package com.example.service;
import com.example.annotation.DS;
import com.example.entity.Account;
import com.example.mapper.AccountMapper;
import org.springframework.stereotype.Service;
@Service
public class AccountService {
    private final AccountMapper accountMapper;
    public AccountService(AccountMapper accountMapper) {
        this.accountMapper = accountMapper;
    }
    // 写操作:使用默认主库(无需注解,或显式@DS("master"))
    public boolean createAccount(Account account) {
        return accountMapper.insert(account) > 0;
    }
    // 读操作:指定从库(关键业务加注解)
    @DS("slave")
    public Account getAccountById(Long id) {
        return accountMapper.selectById(id);
    }
    // 复杂场景:方法内动态切换(非注解方式)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        // 1. 查询转出账户(从库)
        DynamicDataSourceContextHolder.push("slave");
        Account fromAccount = accountMapper.selectById(fromId);
        // 2. 更新账户余额(主库,自动恢复默认数据源)
        fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
        accountMapper.updateById(fromAccount); // 使用主库
    }
}

四、分库分表进阶:按用户 ID 动态路由(银行核心场景)

业务需求:用户 ID 尾号 0-4 存 user_0 库,5-9 存 user_1 库

1. 扩展数据源配置(新增分库)

spring:
  datasource:
    dynamic:
      datasource:
        user_0:
          url: jdbc:mysql://localhost:3306/user_0_db?serverTimezone=Asia/Shanghai
          # 其他配置同master
        user_1:
          url: jdbc:mysql://localhost:3306/user_1_db?serverTimezone=Asia/Shanghai
          # 其他配置同master

2. 实现自定义路由策略(关键代码)

package com.example.config;
import com.baomidou.dynamic.datasource.router.AbstractDataSourceRouter;
import com.baomidou.dynamic.datasource.toolkit.StringUtils;
import org.springframework.stereotype.Component;
@Component
public class UserDataSourceRouter extends AbstractDataSourceRouter {
    @Override
    public String determineDatasource() {
        // 从线程变量获取用户ID(实际场景从请求或上下文获取)
        Long userId = UserContextHolder.getUserId();
        if (userId == null) {
            return getPrimaryDatasource(); // 使用默认主库
        }
        int mod = userId.intValue() % 10;
        return mod < 5 ? "user_0" : "user_1"; // 根据尾号选择分库
    }
}

3. 线程上下文工具类(存储路由参数)

package com.example.util;
public class UserContextHolder {
    private static final ThreadLocal<Long> CONTEXT_HOLDER = new ThreadLocal<>();
    public static void setUserId(Long userId) {
        CONTEXT_HOLDER.set(userId);
    }
    public static Long getUserId() {
        return CONTEXT_HOLDER.get();
    }
    public static void clear() {
        CONTEXT_HOLDER.remove();
    }
}

五、事务处理:多数据源下的事务一致性(核心难点)

1. 单数据源事务(简单场景)

@Service
public class AccountService {
    @Transactional(rollbackFor = Exception.class) // 自动使用当前数据源
    public void updateBalance(Long id, BigDecimal amount) {
        Account account = getAccountById(id);
        account.setBalance(amount);
        updateById(account);
    }
}

2. 跨数据源事务(分布式事务)

方案一:XA 强一致性(适合金融场景)
<!-- 添加Atomikos依赖 -->
<dependency>
    <groupId>com.atomikos</groupId>
    <artifactId>transactions-jdbc</artifactId>
    <version>5.2.1</version>
</dependency>
@Service
public class TransferService {
    @Resource
    private PlatformTransactionManager transactionManager; // Atomikos事务管理器
    public void crossDbTransfer(Long fromId, Long toId, BigDecimal amount) {
        UserTransaction userTransaction = AtomikosContext.getThreadLocalUserTransaction();
        try {
            userTransaction.begin();
            // 转出(user_0库)
            UserContextHolder.setUserId(fromId);
            accountService.updateBalance(fromId, fromAccount.getBalance().subtract(amount));
            // 转入(user_1库)
            UserContextHolder.setUserId(toId);
            accountService.updateBalance(toId, toAccount.getBalance().add(amount));
            userTransaction.commit();
        } catch (Exception e) {
            userTransaction.rollback();
            throw new RuntimeException("跨库转账失败");
        }
    }
}
方案二:最终一致性(适合非核心场景)

通过消息队列(如 Kafka)异步补偿,降低事务复杂度,牺牲强一致性换取性能。

六、实战验证:3 个必测场景确保配置正确

1. 读写分离验证(日志查看)

  • 执行写操作(无 @DS 注解),日志输出:Using data source 'master'
  • 执行读操作(@DS ("slave")),日志输出:Using data source 'slave'

2. 分库路由验证(数据库查看)

  • 插入用户 ID=1(尾号 1,user_0 库):SELECT * FROM user_0_db.tb_account WHERE id=1
  • 插入用户 ID=6(尾号 6,user_1 库):SELECT * FROM user_1_db.tb_account WHERE id=6

3. 事务回滚验证(模拟异常)

@DS("master")
@Transactional
public void testTransaction() {
    Account account = new Account();
    account.setAccountNo("A004");
    save(account); // 插入成功
    int i = 1 / 0; // 主动抛异常
    account.setId(1L);
    updateById(account); // 不会执行,数据回滚
}

七、老司机排坑指南:这些陷阱必须避开

1. 数据源优先级问题

  • 注解 > 路由策略 > 默认数据源:方法上的 @DS 优先级最高,其次是自定义路由,最后用 primary 配置

2. MyBatis-Plus 扫描路径冲突

分库场景需按数据源分组扫描 Mapper:

@Configuration
public class MyBatisPlusConfig {
    @Bean
    @DS("user_0") // 指定数据源
    public SqlSessionFactory user0SqlSessionFactory(DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setMapperLocations(new PathMatchingResourcePatternResolver()
            .getResources("classpath:mapper/user0/**/*.xml")); // 独立Mapper路径
        return factory.getObject();
    }
}

3. 连接池配置不当

  • 从库连接池可配置更大的 maximum-pool-size(读多场景)
  • 主库配置更小的 connection-timeout(减少写阻塞)

4. 分页插件冲突

多数据源下需为每个数据源单独配置分页插件:

@Bean
@ConfigurationProperties(prefix = "mybatis-plus")
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
}

八、最佳实践:生产环境必备配置

1. 数据源监控(Prometheus+Grafana)

spring:
  datasource:
    dynamic:
      datasource:
        master:
          hikari:
            metrics-track-creation: true  # 开启连接池监控指标

2. 数据源切换日志

// 在自定义路由策略中添加日志
public class UserDataSourceRouter {
    private static final Logger log = LoggerFactory.getLogger(UserDataSourceRouter.class);
    @Override
    public String determineDatasource() {
        String ds = super.determineDatasource();
        log.info("Using datasource: {}", ds);
        return ds;
    }
}

3. 动态数据源热加载(高级特性)

// 运行时新增数据源(如对接第三方临时库)
DynamicDataSourceContextHolder.registry("temp_db", dataSourceProperties);

九、总结:多数据源配置的核心思维

  1. 分层设计
    • 基础层:通过 Dynamic Data Source 实现数据源快速切换
    • 策略层:自定义路由算法(尾号分库、租户隔离等)
    • 事务层:根据业务特性选择强一致 / 最终一致方案
  1. 最小侵入原则优先使用注解(@DS)和线程上下文(ThreadLocal)切换,避免在 Mapper 层硬编码数据源
  1. 可观测性每个数据源配置独立的日志、监控指标,出现问题时快速定位是主库写入失败还是从库同步延迟