Spring Boot读写分离案例

wen java案例 3

Spring Boot读写分离案例:从零搭建高可用MySQL架构(附完整代码)

目录导读

  1. 为什么需要读写分离? —— 理解主从复制的核心价值
  2. 环境准备 —— 主从数据库搭建与配置(Docker一键实现)
  3. Spring Boot整合动态数据源 —— 核心源码剖析(AOP+AbstractRoutingDataSource)
  4. 读写分离路由策略 —— 注解驱动 + 事务内强制主库
  5. 实战测试 —— 5个关键场景验证(含主从延迟问题)
  6. 常见问题FAQ —— 面试必问的5个问题解答
  7. 生产级优化建议 —— 从“能用”到“好用”

为什么需要读写分离?

当业务量增长到一定阶段,单库单表会成为性能瓶颈,MySQL主从复制架构将写操作(INSERT/UPDATE/DELETE) 路由到主库,读操作(SELECT) 分发到从库,实现:

Spring Boot读写分离案例

  • 性能提升:从库分担读压力,主库专注写入
  • 高可用:主库宕机时,从库可提升为备用主库
  • 数据安全:从库可作为备份节点

据实际业务数据:当读写比达到 8:2 以上,读写分离的收益最明显。


环境准备:Docker快速搭建MySQL主从

1 主库配置(master)

docker run -d --name mysql-master \
  -p 3306:3306 \
  -e MYSQL_ROOT_PASSWORD=root123 \
  -e MYSQL_DATABASE=testdb \
  mysql:8.0 --server-id=1 --log-bin=mysql-bin

2 从库配置(slave)

docker run -d --name mysql-slave \
  -p 3307:3306 \
  -e MYSQL_ROOT_PASSWORD=root123 \
  -e MYSQL_DATABASE=testdb \
  mysql:8.0 --server-id=2 --log-bin=mysql-bin

3 配置主从复制(关键SQL)

-- 主库执行
CREATE USER 'repl'@'%' IDENTIFIED BY 'replPwd123';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
SHOW MASTER STATUS;
-- 从库执行
CHANGE MASTER TO
  MASTER_HOST='localhost',
  MASTER_PORT=3306,
  MASTER_USER='repl',
  MASTER_PASSWORD='replPwd123',
  MASTER_LOG_FILE='mysql-bin.000003',
  MASTER_LOG_POS=157;
START SLAVE;

Spring Boot整合动态数据源(核心原理)

1 Maven依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

2 动态数据源路由类

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceType();
    }
}

3 数据源上下文持有类(ThreadLocal)

public class DataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
    public static void setWrite() { CONTEXT.set("write"); }
    public static void setRead()  { CONTEXT.set("read"); }
    public static String get()    { return CONTEXT.get(); }
    public static void clear()    { CONTEXT.remove(); }
}

读写分离路由策略(注解+AOP)

1 自定义注解 @ReadOnly

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {}

2 核心AOP拦截器

@Aspect
@Component
public class DataSourceAspect {
    @Before("@annotation(readOnly) || within(@com.demo.annotation.ReadOnly *)")
    public void setReadDataSource(ReadOnly readOnly) {
        if (TransactionSynchronizationManager.isActualTransactionActive()) {
            // 事务内强制使用主库,保证一致性
            DataSourceContextHolder.setWrite();
        } else {
            DataSourceContextHolder.setRead();
        }
    }
    @AfterReturning("/@annotation(...)")
    public void clearDataSource() {
        DataSourceContextHolder.clear();
    }
}

3 配置类完整定义

@Configuration
public class DataSourceConfig {
    @Bean
    @Primary
    public DataSource dataSource() {
        DynamicDataSource ds = new DynamicDataSource();
        Map<Object, Object> targetMap = new HashMap<>();
        targetMap.put("write", buildDataSource("jdbc:mysql://localhost:3306/testdb"));
        targetMap.put("read", buildDataSource("jdbc:mysql://localhost:3307/testdb"));
        ds.setTargetDataSources(targetMap);
        ds.setDefaultTargetDataSource(targetMap.get("write"));
        return ds;
    }
}

实战测试:5个关键场景验证

场景1:普通查询(走从库)

@Service
public class UserService {
    @ReadOnly
    public User getUserById(Long id) {
        // 实际SQL执行连接的是3307从库
    }
}

场景2:写操作(强制走主库)

@Transactional
public void updateUser(User user) {
    userMapper.update(user); // 即使方法内有@ReadOnly也不会生效
}

场景3:事务内读写一致

@Transactional
public void processOrder(Order order) {
    // 事务开始后,内部所有查询都会走主库,防止拿到旧数据
}

场景4:主从延迟测试

// 刚插入的数据立即查询,在主从复制延迟时会出现null
public User insertAndQuery(User user) {
    save(user);
    Thread.sleep(2000); // 等复制完成
    return mapper.selectById(user.getId());
}

场景5:手动切换数据源

public void cleanAllData() {
    DataSourceContextHolder.setWrite();
    try {
        userMapper.deleteAll(); // 确保走主库
    } finally {
        DataSourceContextHolder.clear();
    }
}

常见问题FAQ(面试必备)

Q1:读写分离后,事务内如何保证一致性? A:通过AOP检查TransactionSynchronizationManager.isActualTransactionActive(),事务活动时强制使用主库。

Q2:主从延迟如何处理? A:① 关键数据用@ForceMaster注解;② 设置最大容忍延迟,路由到从库时检查seconds_behind_master

Q3:从库多个时如何负载均衡? A:在DynamicDataSource中添加轮询/随机算法,从targetDataSources中动态选取。

Q4:事务隔离级别需要特殊配置吗? A:建议主库使用READ COMMITTED,从库可使用REPEATABLE READ提高一致性。

Q5:动态数据源与MyBatis/MyBatis-Plus兼容性? A:完全兼容,因为AbstractRoutingDataSource是JDBC层实现,SQL执行前自动路由。


生产级优化建议

  • 连接池隔离:为读写配置不同的HikariCP连接池容量(写池10,读池50)
  • 监控报警:通过Prometheus监控主从复制状态,延迟超过5s触发告警
  • 灰度策略:新业务可在从库先查询,验证数据一致性后再全量放开
  • 读写分离升级:引入ShardingSphere或MyCat,支持分库分表+读写分离
  • 从库扩展:从库可增加loadBalance策略,分摊查询压力

通过Spring Boot动态数据源方案,我们仅用 200余行代码 就实现了完整的读写分离架构,核心在于:

  1. AbstractRoutingDataSource实现多数据源路由
  2. AOP注解驱动业务无感知切换
  3. 事务内强制主库保证数据一致性

建议先在小规模业务验证,然后逐步扩展到生产环境,如果你的系统已经出现单库性能瓶颈,读写分离是最快见效的优化手段。

抱歉,评论功能暂时关闭!