Spring Boot读写分离案例:从零搭建高可用MySQL架构(附完整代码)
目录导读
- 为什么需要读写分离? —— 理解主从复制的核心价值
- 环境准备 —— 主从数据库搭建与配置(Docker一键实现)
- Spring Boot整合动态数据源 —— 核心源码剖析(AOP+AbstractRoutingDataSource)
- 读写分离路由策略 —— 注解驱动 + 事务内强制主库
- 实战测试 —— 5个关键场景验证(含主从延迟问题)
- 常见问题FAQ —— 面试必问的5个问题解答
- 生产级优化建议 —— 从“能用”到“好用”
为什么需要读写分离?
当业务量增长到一定阶段,单库单表会成为性能瓶颈,MySQL主从复制架构将写操作(INSERT/UPDATE/DELETE) 路由到主库,读操作(SELECT) 分发到从库,实现:

- 性能提升:从库分担读压力,主库专注写入
- 高可用:主库宕机时,从库可提升为备用主库
- 数据安全:从库可作为备份节点
据实际业务数据:当读写比达到 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余行代码 就实现了完整的读写分离架构,核心在于:
AbstractRoutingDataSource实现多数据源路由- AOP注解驱动业务无感知切换
- 事务内强制主库保证数据一致性
建议先在小规模业务验证,然后逐步扩展到生产环境,如果你的系统已经出现单库性能瓶颈,读写分离是最快见效的优化手段。