SpringBoot + MyBatis-Plus + Dynamic-Datasource 读写分离完整指南

在SpringBoot项目中,使用MyBatis-Plus和Dynamic-Datasource实现读写分离是一种常见且高效的架构选择。下面我将为你详细讲解完整的实现方案,包括环境准备、配置、代码实现以及注意事项。

1. 读写分离概述与核心概念

读写分离是一种常见的数据库优化方案,其核心思想是将数据库的写操作(INSERT、UPDATE、DELETE)和读操作(SELECT)分发到不同的数据库节点上。主数据库(Master)负责处理所有写操作,而从数据库(Slave)负责处理读操作,通过数据库的主从复制机制保持数据同步。

1.1 核心价值

  • 提升并发性能:将读请求分散到多个从库,减轻主库压力
  • 提高系统可用性:单个从库故障不影响读服务
  • 优化响应速度:专库专用,避免读写操作相互阻塞

1.2 技术架构

markdown 复制代码
应用程序 → Dynamic-Datasource → 主库(写操作)
                     ↓
                   从库(读操作)

图:读写分离架构示意图

2. 环境准备与依赖配置

2.1 添加Maven依赖

首先在项目的pom.xml中添加必要的依赖:

xml 复制代码
<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- MyBatis-Plus 启动器 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3.2</version>
    </dependency>
    
    <!-- Dynamic-Datasource 启动器(核心依赖) -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
        <version>3.6.1</version>
    </dependency>
    
    <!-- MySQL 驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    
    <!-- 连接池(可选,HikariCP已内置) -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.16</version>
    </dependency>
</dependencies>

citation:2][citation:3

2.2 配置文件设置

application.yml中配置主从数据源:

yaml 复制代码
spring:
  datasource:
    dynamic:
      primary: master  # 设置默认数据源为主库
      strict: false   # 是否严格匹配数据源,false时未匹配到指定数据源则使用默认数据源
      datasource:
        # 主库配置(写操作)
        master:
          url: jdbc:mysql://master-host:3306/core?useSSL=false&serverTimezone=Asia/Shanghai
          username: admin
          password: master@123
          driver-class-name: com.mysql.cj.jdbc.Driver
        # 从库配置(读操作)- 支持多个从库
        slave1:
          url: jdbc:mysql://slave1-host:3306/core?useSSL=false&serverTimezone=Asia/Shanghai
          username: readonly
          password: slave@123
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave2:
          url: jdbc:mysql://slave2-host:3306/core?useSSL=false&serverTimezone=Asia/Shanghai
          username: readonly
          password: slave@123
          driver-class-name: com.mysql.cj.jdbc.Driver
      
      # 负载均衡策略配置(多个从库时生效)
      strategy: 
        slave: round_robin  # 从库负载均衡策略:random(随机)/round_robin(轮询)
      
      # 连接池配置(HikariCP)
      hikari:
        max-pool-size: 20      # 最大连接数
        min-idle: 5           # 最小空闲连接
        connection-timeout: 30000  # 连接超时时间(ms)
        idle-timeout: 600000  # 空闲连接超时时间(ms)
        max-lifetime: 1800000 # 连接最大生命周期(ms)

# MyBatis-Plus 配置
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true  # 开启驼峰命名转换
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 打印SQL日志(调试用)
  global-config:
    db-config:
      id-type: auto  # 主键策略自增

citation:2][citation:6

3. 数据源与MyBatis-Plus配置类

3.1 动态数据源自动配置

Dynamic-Datasource starter已经提供了自动配置,大多数情况下无需额外配置。但如果需要自定义,可以创建配置类:

less 复制代码
@Configuration
@MapperScan("com.example.mapper")  // 指定MyBatis mapper接口的扫描路径
public class DataSourceConfig {
    
    /**
     * 配置动态数据源
     * Dynamic-Datasource会自动根据application.yml的配置创建数据源
     * 此处可以添加自定义配置
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.dynamic")
    public DynamicDataSourceProperties dynamicDataSourceProperties() {
        return new DynamicDataSourceProperties();
    }
    
    /**
     * 配置MyBatis-Plus拦截器(分页插件等)
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件
        PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        paginationInterceptor.setMaxLimit(1000L);  // 设置最大分页限制
        interceptor.addInnerInterceptor(paginationInterceptor);
        
        // 可以添加其他插件,如乐观锁插件等
        return interceptor;
    }
}

citation:1][citation:5

4. 业务层实现读写分离

4.1 使用@DS注解手动切换数据源

最直接的方式是在Service层使用@DS注解指定数据源:

java 复制代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    
    /**
     * 写操作:使用@DS("master")指定主库
     * 注意:写操作建议都使用主库,确保数据一致性
     */
    @DS("master")  // 指定使用主库
    @Transactional(rollbackFor = Exception.class)  // 添加事务管理
    @Override
    public boolean createUser(User user) {
        // 参数校验
        if (user == null || StringUtils.isBlank(user.getUsername())) {
            throw new IllegalArgumentException("用户信息不完整");
        }
        
        // 设置创建时间
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        
        // 执行插入操作,这里会使用主库
        boolean result = save(user);
        
        // 这里可以添加其他业务逻辑
        log.info("创建用户成功,用户ID: {}", user.getId());
        
        return result;
    }
    
    /**
     * 读操作:使用@DS("slave")指定从库
     * 框架会根据负载均衡策略选择slave1或slave2
     */
    @DS("slave")  // 指定使用从库(负载均衡)
    @Override
    public User getUserById(Long id) {
        // 参数校验
        if (id == null || id <= 0) {
            throw new IllegalArgumentException("用户ID不合法");
        }
        
        // 查询用户信息,这里会使用从库
        User user = getById(id);
        
        if (user == null) {
            log.warn("未找到对应用户,用户ID: {}", id);
            throw new RuntimeException("用户不存在");
        }
        
        return user;
    }
    
    /**
     * 批量查询:同样使用从库
     */
    @DS("slave")
    @Override
    public List<User> listUsersByCondition(UserQuery query) {
        // 构建查询条件
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        
        if (StringUtils.isNotBlank(query.getUsername())) {
            wrapper.like(User::getUsername, query.getUsername());
        }
        
        if (query.getStatus() != null) {
            wrapper.eq(User::getStatus, query.getStatus());
        }
        
        // 添加排序
        wrapper.orderByDesc(User::getCreateTime);
        
        // 执行查询,使用从库
        return list(wrapper);
    }
    
    /**
     * 更新操作:必须使用主库
     */
    @DS("master")
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean updateUser(User user) {
        if (user == null || user.getId() == null) {
            throw new IllegalArgumentException("用户信息不完整");
        }
        
        // 设置更新时间
        user.setUpdateTime(LocalDateTime.now());
        
        // 执行更新,使用主库
        boolean result = updateById(user);
        
        if (result) {
            log.info("更新用户成功,用户ID: {}", user.getId());
        } else {
            log.error("更新用户失败,用户ID: {}", user.getId());
        }
        
        return result;
    }
    
    /**
     * 删除操作:必须使用主库
     */
    @DS("master")
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean deleteUser(Long id) {
        if (id == null || id <= 0) {
            throw new IllegalArgumentException("用户ID不合法");
        }
        
        // 执行删除,使用主库
        boolean result = removeById(id);
        
        if (result) {
            log.info("删除用户成功,用户ID: {}", id);
        } else {
            log.warn("删除用户失败,用户ID: {}", id);
        }
        
        return result;
    }
}

citation:2][citation:8

4.2 基于AOP的自动数据源切换(推荐)

对于大型项目,手动添加@DS注解比较繁琐,可以通过AOP自动根据方法类型切换数据源:

less 复制代码
@Aspect
@Component
@Slf4j
public class DataSourceAspect {
    
    /**
     * 定义切点:拦截Service层的所有方法
     */
    @Pointcut("execution(* com.example.service..*.*(..))")
    public void servicePointcut() {}
    
    /**
     * 前置通知:在方法执行前选择数据源
     */
    @Before("servicePointcut()")
    public void before(JoinPoint joinPoint) {
        // 获取方法名
        String methodName = joinPoint.getSignature().getName();
        
        // 根据方法名前缀判断是读操作还是写操作
        if (isReadOperation(methodName)) {
            // 读操作使用从库
            DynamicDataSourceContextHolder.push("slave");
            log.debug("切换数据源到从库,方法名: {}", methodName);
        } else {
            // 写操作使用主库
            DynamicDataSourceContextHolder.push("master");
            log.debug("切换数据源到主库,方法名: {}", methodName);
        }
    }
    
    /**
     * 后置通知:清理数据源上下文
     */
    @After("servicePointcut()")
    public void after() {
        DynamicDataSourceContextHolder.clear();
        log.debug("清除数据源上下文");
    }
    
    /**
     * 判断是否为读操作
     * 可以根据方法名前缀进行判断
     */
    private boolean isReadOperation(String methodName) {
        // 常见的读操作方法前缀
        String[] readPrefixes = {"get", "select", "list", "query", "find", "search", "count", "check"};
        
        for (String prefix : readPrefixes) {
            if (methodName.startsWith(prefix)) {
                return true;
            }
        }
        return false;
    }
}

citation:3][citation:7

使用AOP后,Service层的代码可以简化为:

scala 复制代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean createUser(User user) {
        // 自动使用主库(AOP根据方法名判断)
        user.setCreateTime(LocalDateTime.now());
        return save(user);
    }
    
    @Override
    public User getUserById(Long id) {
        // 自动使用从库(AOP根据方法名前缀"get"判断)
        return getById(id);
    }
    
    // 其他方法无需添加@DS注解,由AOP自动处理
}

5. 事务处理策略

事务处理是读写分离中的关键问题,需要特别注意:

5.1 单数据源事务

less 复制代码
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
    
    /**
     * 单数据源事务:所有操作都在主库执行
     * 使用@DS("master")确保即使有AOP配置也使用主库
     */
    @DS("master")  // 显式指定主库
    @Transactional(rollbackFor = Exception.class)  // 声明式事务
    @Override
    public void createOrder(Order order) {
        // 1. 保存订单主信息(主库)
        save(order);
        
        // 2. 更新库存(主库)
        updateStock(order);
        
        // 3. 记录操作日志(主库)
        logOrderOperation(order);
        
        // 如果任何一步失败,整个事务回滚
    }
    
    /**
     * 只读事务:可以指定从库
     */
    @DS("slave")
    @Transactional(readOnly = true)  // 只读事务,有优化效果
    @Override
    public Order getOrderDetail(Long orderId) {
        return getById(orderId);
    }
}

5.2 多数据源事务处理

对于涉及多个数据源的复杂事务,需要使用分布式事务解决方案:

java 复制代码
@Service
@Slf4j
public class DistributedTransactionService {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private OrderService orderService;
    
    /**
     * 使用@DSTransactional实现多数据源分布式事务
     * 注意:需要集成Seata等分布式事务框架
     */
    // @DSTransactional  // 分布式事务注解(需要额外配置)
    @Transactional(rollbackFor = Exception.class)
    public void placeOrder(Order order, User user) {
        try {
            // 1. 用户操作(主库)
            userService.updateUser(user);
            
            // 2. 订单操作(主库)
            orderService.createOrder(order);
            
            // 模拟业务异常
            if (order.getAmount() == null) {
                throw new RuntimeException("订单金额不能为空");
            }
            
            log.info("下单成功");
        } catch (Exception e) {
            log.error("下单失败,已回滚", e);
            throw e;  // 抛出异常触发回滚
        }
    }
}

citation:7

6. 高级功能与优化策略

6.1 多从库负载均衡

当配置多个从库时,Dynamic-Datasource支持多种负载均衡策略:

yaml 复制代码
spring:
  datasource:
    dynamic:
      datasource:
        master:
          # 主库配置...
        slave1:
          # 从库1配置...
        slave2:
          # 从库2配置...
        slave3:
          # 从库3配置...
      strategy:
        slave: round_robin  # 负载均衡策略
        # 可选值:
        # random - 随机选择(默认)
        # round_robin - 轮询
        # weight_round_robin - 加权轮询(需要额外配置权重)

负载均衡策略对比:

策略类型 描述 适用场景
random 随机选择从库 简单的读负载均衡
round_robin 轮询选择从库 从库配置相近,需要均匀分布
weight_round_robin 根据权重选择 从库配置不同,按性能分配负载

citation:3][citation:7

6.2 健康检查与故障转移

less 复制代码
@Component
@Slf4j
public class DataSourceHealthCheck {
    
    @Autowired
    private DataSource dataSource;
    
    /**
     * 定时检查从库健康状态
     */
    @Scheduled(fixedRate = 60000)  // 每分钟执行一次
    public void checkSlaveHealth() {
        // 这里可以实现从库健康检查逻辑
        // 如果某个从库不可用,可以动态将其从负载均衡池中移除
        
        log.info("执行从库健康检查...");
        // 实际实现中可以使用JDBC测试连接或查询数据库状态
    }
    
    /**
     * 获取从库同步延迟
     */
    public Long getReplicationDelay(String slaveName) {
        // 执行SQL查询从库同步状态
        // SHOW SLAVE STATUS 可以获取Seconds_Behind_Master等信息
        // 如果延迟过大,可以临时将该从库标记为不可用
        
        return 0L;  // 返回延迟秒数
    }
}

citation:2

7. 测试与验证

7.1 单元测试

scss 复制代码
@SpringBootTest
@Slf4j
class ReadWriteSeparationTest {
    
    @Autowired
    private UserService userService;
    
    /**
     * 测试写操作(应该路由到主库)
     */
    @Test
    void testWriteOperation() {
        User user = new User();
        user.setUsername("testUser");
        user.setEmail("test@example.com");
        
        boolean result = userService.createUser(user);
        
        assertTrue(result);
        log.info("写操作测试通过,应路由到主库");
    }
    
    /**
     * 测试读操作(应该路由到从库)
     */
    @Test
    void testReadOperation() {
        User user = userService.getUserById(1L);
        
        assertNotNull(user);
        log.info("读操作测试通过,应路由到从库");
    }
    
    /**
     * 测试事务内的数据源选择
     */
    @Test
    void testTransactionalOperation() {
        // 测试事务方法,应始终使用主库
        User user = userService.getUserById(1L);
        assertNotNull(user);
        
        log.info("事务内操作测试完成");
    }
}

7.2 验证数据源路由

application.yml中开启SQL日志,验证读写分离是否生效:

yaml 复制代码
# 开启MyBatis SQL日志
logging:
  level:
    com.example.mapper: debug  # Mapper接口的包路径
    com.baomidou.dynamic.datasource: debug  # Dynamic-Datasource日志

观察控制台输出,应该能看到类似日志:

sql 复制代码
2025-11-11 10:00:00 | Master DataSource  | INSERT INTO user ...
2025-11-11 10:00:05 | Slave DataSource   | SELECT * FROM user ...

citation:3

8. 生产环境注意事项

8.1 主从同步延迟处理

kotlin 复制代码
@Service
public class CriticalReadService {
    
    @Autowired
    private UserMapper userMapper;
    
    /**
     * 对于一致性要求高的读操作,强制走主库
     */
    @DS("master")  // 强制使用主库,避免读取旧数据
    public User getCriticalUserInfo(Long userId) {
        // 例如:账户余额、订单状态等关键信息
        return userMapper.selectById(userId);
    }
    
    /**
     * 检查数据是否已同步
     */
    public boolean waitForReplication(Long userId, int maxWaitSeconds) {
        for (int i = 0; i < maxWaitSeconds; i++) {
            User masterUser = getFromMaster(userId);
            User slaveUser = getFromSlave(userId);
            
            if (Objects.equals(masterUser, slaveUser)) {
                return true;  // 数据已同步
            }
            
            try {
                Thread.sleep(1000);  // 等待1秒
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        return false;  // 同步超时
    }
}

8.2 监控与告警

yaml 复制代码
# 连接池监控配置
spring:
  datasource:
    dynamic:
      druid:
        # 开启监控统计
        filters: stat,wall,log4j
        web-stat-filter:
          enabled: true
        stat-view-servlet:
          enabled: true
          url-pattern: /druid/*
          reset-enable: false
          login-username: admin
          login-password: admin

9. 常见问题与解决方案

9.1 事务内数据源切换失效

问题 :在@Transactional方法内,数据源在方法开始时确定,方法内部调用无法切换数据源。

解决方案

less 复制代码
@Service
public class TransactionalService {
    
    @Autowired
    private ApplicationContext applicationContext;
    
    @Transactional
    public void transactionalMethod() {
        // 方法内需要切换数据源时,通过代理对象调用
        TransactionalService self = applicationContext.getBean(TransactionalService.class);
        
        // 通过代理对象调用,可以正常切换数据源
        self.readOperation();  // 使用从库
        self.writeOperation();  // 使用主库
    }
    
    @DS("slave")
    public void readOperation() {
        // 读操作
    }
    
    @DS("master")
    public void writeOperation() {
        // 写操作
    }
}

9.2 动态增减数据源

typescript 复制代码
@Service
public class DynamicDataSourceService {
    
    @Autowired
    private DynamicDataSourceProvider dataSourceProvider;
    
    /**
     * 运行时动态添加数据源
     */
    public void addDataSource(String dataSourceName, DataSourceProperty property) {
        DynamicDataSourceContextHolder.addDataSource(dataSourceName, property);
        log.info("动态添加数据源: {}", dataSourceName);
    }
    
    /**
     * 运行时移除数据源
     */
    public void removeDataSource(String dataSourceName) {
        DynamicDataSourceContextHolder.removeDataSource(dataSourceName);
        log.info("动态移除数据源: {}", dataSourceName);
    }
}

总结

通过SpringBoot + MyBatis-Plus + Dynamic-Datasource实现读写分离,可以显著提升数据库读性能和高可用性。关键点包括:

  1. 正确配置:确保主从数据源配置正确,负载均衡策略合理
  2. 合理使用注解 :在适当的方法上使用@DS注解或通过AOP自动切换
  3. 事务管理:注意事务内的数据源行为,关键操作强制走主库
  4. 监控告警:建立完善的监控体系,及时发现主从延迟等问题
  5. 故障处理:实现从库故障自动转移和恢复机制

这种架构在读写比例高的应用中能带来显著的性能提升,但也需要处理好主从同步延迟等一致性问题。

相关推荐
间彧2 小时前
数据库读写分离下如何解决主从同步延迟问题
后端
码事漫谈2 小时前
C++中的线程同步机制浅析
后端
间彧2 小时前
在高并发场景下,动态数据源切换与Seata全局事务锁管理如何协同避免性能瓶颈?
后端
码事漫谈2 小时前
CI/CD集成工程师前景分析:与开发岗位的全面对比
后端
间彧2 小时前
在微服务架构下,如何结合Spring Cloud实现动态数据源的路由管理?
后端
间彧2 小时前
动态数据源切换与Seata分布式事务如何协同工作?
后端
静若繁花_jingjing2 小时前
NoSql数据库概念
数据库·nosql
间彧2 小时前
除了AOP切面,还有哪些更灵活的数据源切换策略?比如基于注解或自定义路由规则
数据库·后端
弥生赞歌2 小时前
Mysql作业四
数据库·mysql