🏗️ Spring Boot 3实现MySQL读写分离完整指南

✨ 读写分离的核心价值

在高并发场景下,数据库往往成为系统瓶颈。读写分离通过将写操作定向到主库、读操作分发到从库,显著提升系统读性能和数据可用性。当主库出现故障时,从库可以继续提供读服务,提高系统的稳定性。

⚙️ 项目依赖配置

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

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
    </dependency>
</dependencies>

📁 核心实现代码详解

1. 配置文件设置(application.yml)

yaml 复制代码
spring:
  datasource:
    # 主库配置(写操作)
    master:
      jdbc-url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: master_password
      driver-class-name: com.mysql.cj.jdbc.Driver
      # Hikari连接池配置
      hikari:
        maximum-pool-size: 10
        minimum-idle: 5
        idle-timeout: 30000
        max-lifetime: 1800000
        connection-timeout: 30000
        connection-test-query: SELECT 1
    # 从库配置(读操作)
    slave:
      jdbc-url: jdbc:mysql://localhost:3306/slave_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: slave_password
      driver-class-name: com.mysql.cj.jdbc.Driver
      hikari:
        maximum-pool-size: 15  # 从库可以配置更多连接,因为读操作通常更频繁
        minimum-idle: 8
        idle-timeout: 30000
        max-lifetime: 1800000
        connection-timeout: 30000
        connection-test-query: SELECT 1

2. 数据源枚举定义

arduino 复制代码
/**
 * 数据源类型枚举
 * 用于标识当前操作应使用主库还是从库
 */
public enum DataSourceType {
    MASTER,  // 主库:用于写操作(INSERT、UPDATE、DELETE)
    SLAVE    // 从库:用于读操作(SELECT)
}

3. 数据源上下文管理器

csharp 复制代码
/**
 * 数据源上下文管理器(基于ThreadLocal实现线程隔离)
 * 功能:保存当前线程使用的数据源类型,确保多线程环境下数据源切换不会相互干扰
 */
public class DataSourceContextHolder {
    
    // 使用ThreadLocal保证线程安全,每个线程有独立的数据源上下文
    private static final ThreadLocal<DataSourceType> CONTEXT_HOLDER = new ThreadLocal<>();
    
    /**
     * 设置当前线程的数据源类型
     * @param dataSourceType 数据源类型(MASTER或SLAVE)
     */
    public static void setDataSourceType(DataSourceType dataSourceType) {
        CONTEXT_HOLDER.set(dataSourceType);
    }
    
    /**
     * 获取当前线程的数据源类型
     * @return 当前数据源类型,默认为MASTER(保证写操作可靠性)
     */
    public static DataSourceType getDataSourceType() {
        return CONTEXT_HOLDER.get() == null ? DataSourceType.MASTER : CONTEXT_HOLDER.get();
    }
    
    /**
     * 清除当前线程的数据源类型
     * 防止内存泄漏,特别是在线程池场景下
     */
    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

4. 动态路由数据源

scala 复制代码
/**
 * 动态路由数据源(继承Spring的AbstractRoutingDataSource)
 * 核心功能:根据当前上下文动态选择主库或从库
 */
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    
    /**
     * 决定当前数据源查找键(Spring在每次数据库操作前调用此方法)
     * @return 数据源查找键(MASTER或SLAVE)
     */
    @Override
    protected Object determineCurrentLookupKey() {
        DataSourceType dataSourceType = DataSourceContextHolder.getDataSourceType();
        System.out.println("当前使用的数据源: " + dataSourceType);
        return dataSourceType;
    }
}

5. 数据源配置类

less 复制代码
/**
 * 数据源配置类(核心配置)
 * 配置主从数据源并初始化路由数据源
 */
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
    basePackages = "com.example.repository",
    entityManagerFactoryRef = "entityManagerFactory",
    transactionManagerRef = "transactionManager"
)
public class DataSourceConfig {
    
    /**
     * 主库数据源(写操作)
     */
    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    /**
     * 从库数据源(读操作)
     */
    @Bean(name = "slaveDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    /**
     * 动态路由数据源(优先级最高,作为主数据源)
     */
    @Primary
    @Bean(name = "routingDataSource")
    public DataSource routingDataSource(
            @Qualifier("masterDataSource") DataSource masterDataSource,
            @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        
        DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
        
        // 配置目标数据源映射
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER, masterDataSource);
        targetDataSources.put(DataSourceType.SLAVE, slaveDataSource);
        
        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.setDefaultTargetDataSource(masterDataSource); // 默认使用主库
        
        return routingDataSource;
    }
    
    /**
     * 实体管理器工厂
     */
    @Bean(name = "entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder builder, 
            @Qualifier("routingDataSource") DataSource dataSource) {
        return builder
            .dataSource(dataSource)
            .packages("com.example.entity")
            .persistenceUnit("mysqlUnit")
            .build();
    }
    
    /**
     * 事务管理器
     */
    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager(
            @Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}

6. AOP切面实现自动路由

java 复制代码
/**
 * 数据源切面配置(基于AOP自动切换数据源)
 * 通过方法名自动识别读写操作,实现数据源动态路由
 */
@Aspect
@Component
@Order(1) // 确保在事务切面之前执行
public class DataSourceAspect {
    
    /**
     * 写操作切点(insert、update、delete、save开头的方法)
     */
    @Before("execution(* com.example.service..*.create*(..)) || " +
            "execution(* com.example.service..*.update*(..)) || " +
            "execution(* com.example.service..*.delete*(..)) || " +
            "execution(* com.example.service..*.save*(..))")
    public void setWriteDataSourceType() {
        DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
        System.out.println("切换到主库(写操作)");
    }
    
    /**
     * 读操作切点(select、get、find、query开头的方法)
     */
    @Before("execution(* com.example.service..*.select*(..)) || " +
            "execution(* com.example.service..*.get*(..)) || " +
            "execution(* com.example.service..*.find*(..)) || " +
            "execution(* com.example.service..*.query*(..))")
    public void setReadDataSourceType() {
        DataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);
        System.out.println("切换到从库(读操作)");
    }
    
    /**
     * 后置处理:清理数据源上下文
     */
    @After("execution(* com.example.service..*.*(..))")
    public void clearDataSourceType() {
        DataSourceContextHolder.clearDataSourceType();
        System.out.println("清理数据源上下文");
    }
}

7. 业务层使用示例

typescript 复制代码
/**
 * 用户服务实现类
 * 演示读写分离的实际应用
 */
@Service
@Transactional
public class UserServiceImpl implements UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    /**
     * 新增用户(写操作自动路由到主库)
     */
    @Override
    public User createUser(User user) {
        // 方法名以"create"开头,AOP会自动切换到MASTER数据源
        return userRepository.save(user);
    }
    
    /**
     * 根据ID查询用户(读操作自动路由到从库)
     */
    @Override
    @Transactional(readOnly = true) // 只读事务优化性能
    public User getUserById(Long id) {
        // 方法名以"get"开头,AOP会自动切换到SLAVE数据源
        return userRepository.findById(id).orElse(null);
    }
    
    /**
     * 查询所有用户(读操作)
     */
    @Override
    @Transactional(readOnly = true)
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
    
    /**
     * 更新用户信息(写操作)
     */
    @Override
    public User updateUser(User user) {
        return userRepository.save(user);
    }
}

🔍 测试与验证

单元测试类

scss 复制代码
/**
 * 读写分离测试类
 */
@SpringBootTest
class ReadWriteSeparationTest {
    
    @Autowired
    private UserService userService;
    
    /**
     * 测试写操作(应路由到主库)
     */
    @Test
    void testWriteOperation() {
        User user = new User();
        user.setUsername("testUser");
        user.setPassword("password");
        
        User savedUser = userService.createUser(user);
        
        Assertions.assertNotNull(savedUser.getId());
        System.out.println("写操作测试通过(路由到主库)");
    }
    
    /**
     * 测试读操作(应路由到从库)
     */
    @Test
    void testReadOperation() {
        List<User> users = userService.getAllUsers();
        
        Assertions.assertNotNull(users);
        System.out.println("读操作测试通过(路由到从库)");
    }
    
    /**
     * 测试读写混合操作
     */
    @Test
    void testReadWriteMix() {
        // 写操作
        User user = new User();
        user.setUsername("mixUser");
        userService.createUser(user);
        
        // 读操作
        User foundUser = userService.getUserById(1L);
        
        Assertions.assertNotNull(foundUser);
        System.out.println("读写混合操作测试通过");
    }
}

⚠️ 关键注意事项

1. 主从同步延迟处理

在读写分离架构中,主从同步存在延迟可能性。刚写入主库的数据可能不会立即在从库中可用。

解决方案:

scss 复制代码
/**
 * 强制读主库的场景
 */
@Service
public class CriticalService {
    
    @Autowired
    private UserRepository userRepository;
    
    /**
     * 重要业务:写入后立即读取,强制走主库
     */
    public User createAndGetUser(User user) {
        // 写入主库
        User savedUser = userRepository.save(user);
        
        // 强制从主库读取(避免同步延迟)
        DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
        try {
            return userRepository.findById(savedUser.getId()).orElse(null);
        } finally {
            DataSourceContextHolder.clearDataSourceType();
        }
    }
}

2. 事务中的数据处理

在事务中,所有操作应使用同一数据源。

解决方案:

typescript 复制代码
@Service
public class TransactionalService {
    
    /**
     * 事务内强制使用主库
     */
    @Transactional
    public void complexBusinessOperation() {
        // 方法开始时显式设置主库,确保事务内一致性
        DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
        
        try {
            // 一系列数据库操作...
            // 所有这些操作都在同一事务中,使用同一数据源
        } finally {
            // 事务结束后清理
            DataSourceContextHolder.clearDataSourceType();
        }
    }
}

📊 方案优缺点分析

优势 挑战 应对策略
提升读性能:将读请求分发到从库 主从同步延迟 关键业务强制读主库
提高可用性:主库故障时从库可读 事务内数据源一致性 事务中强制使用主库
减轻主库压力 复杂SQL路由 明确的读写操作分离

💎 总结

通过以上完整的Spring Boot 3实现方案,你可以成功配置MySQL读写分离。关键在于理解动态数据源路由原理,合理处理主从同步延迟和事务一致性等挑战。这种架构能显著提升系统性能,特别适合读多写少的应用场景。

相关推荐
PawSQL3 小时前
智能SQL优化工具 PawSQL 月度更新 | 2025年10月
数据库·人工智能·sql·sql优化·pawsql
Ace_31750887763 小时前
淘宝店铺全量商品接口实战:分类穿透采集与增量同步的技术方案
大数据·数据库·python
Gavin_9153 小时前
【Ruby】Mixins扩展方式之include,extend和prepend
数据库·ruby
瀚高PG实验室3 小时前
pg_pdr的生成方式
数据库·瀚高数据库
烤麻辣烫3 小时前
黑马程序员苍穹外卖(新手)Day1
java·数据库·spring boot·学习·mybatis
llxxyy卢4 小时前
SQL注入之堆叠及waf绕过注入(安全狗)
数据库·sql·安全
dblens 数据库管理和开发工具5 小时前
PostgreSQL模式:数据库中的命名空间艺术
数据库·postgresql·oracle
数据最前线6 小时前
数据管理技术发展的3个阶段
数据库·考研·数据库系统概论
SelectDB6 小时前
冷查第一,再登榜首!Apache Doris 3.1 全面刷新 JSONBench 性能纪录
数据库·apache