1. 主从复制架构的必要性与挑战
MySQL主从复制架构已经成为现代Web应用的标准配置方案。我在过去三年参与的12个电商和物联网项目中,有11个采用了这种架构设计。主库负责所有写操作和核心读请求,而从库则分担了80%以上的报表查询和数据分析负载。
这种架构最直接的优势体现在性能提升上。去年我们处理的一个跨境电商项目,在单库架构下QPS峰值只能达到1200左右,引入一主三从配置后,整体吞吐量提升到5800QPS。更重要的是,当主库出现计划内维护(比如版本升级)时,从库可以维持整个系统的读服务不中断。
但在实际配置过程中,开发者常会遇到几个典型问题:
- 从库同步延迟导致业务逻辑异常(比如刚提交的订单查询不到)
- 连接池未正确区分读写数据源造成写入从库
- 事务跨主从库导致数据不一致
- 故障转移时的数据源切换不及时
关键提示:主从架构不是简单的数据库集群,需要应用层、中间件层和数据库层的协同设计。我们接下来要实现的Spring Boot集成方案,就是要解决这些痛点问题。
2. 环境准备与依赖配置
2.1 MySQL主从库搭建
在生产环境中,我们通常使用Docker或Kubernetes部署数据库集群。这里给出一个docker-compose的配置示例:
version: '3' services: mysql-master: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: masterpass MYSQL_DATABASE: order_db MYSQL_REPLICATION_USER: repl_user MYSQL_REPLICATION_PASSWORD: repl_pass ports: - "3306:3306" volumes: - ./master.cnf:/etc/mysql/conf.d/replication.cnf mysql-slave: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: slavepass MYSQL_DATABASE: order_db ports: - "3307:3306" volumes: - ./slave.cnf:/etc/mysql/conf.d/replication.cnf depends_on: - mysql-master主库配置文件master.cnf需要包含:
[mysqld] server-id=1 log-bin=mysql-bin binlog-format=ROW binlog-do-db=order_db从库配置文件slave.cnf对应配置:
[mysqld] server-id=2 relay-log=mysql-relay-bin read_only=12.2 Spring Boot项目初始化
创建项目时需特别注意依赖版本兼容性。当前推荐组合:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.16</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.2</version> </dependency> </dependencies>避坑指南:MySQL 8.x驱动需要显式指定时区,在application.yml中添加:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai3. Druid多数据源配置实战
3.1 基础数据源定义
首先创建主从数据源的配置类:
@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.master") public DataSource masterDataSource() { return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.slave") @ConditionalOnProperty(prefix = "spring.datasource", name = "slave.enabled", havingValue = "true") public DataSource slaveDataSource() { return DruidDataSourceBuilder.create().build(); } }对应的application.yml配置:
spring: datasource: master: url: jdbc:mysql://localhost:3306/order_db username: root password: masterpass initial-size: 5 max-active: 20 min-idle: 5 slave: enabled: true url: jdbc:mysql://localhost:3307/order_db username: root password: slavepass initial-size: 5 max-active: 15 min-idle: 53.2 动态路由数据源实现
核心是继承AbstractRoutingDataSource创建路由逻辑:
public class DynamicDataSource extends AbstractRoutingDataSource { private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); public static void setDataSourceKey(String key) { CONTEXT_HOLDER.set(key); } @Override protected Object determineCurrentLookupKey() { return CONTEXT_HOLDER.get(); } }配置主从数据源路由:
@Bean @Primary public DataSource dynamicDataSource( @Qualifier("masterDataSource") DataSource master, @Qualifier("slaveDataSource") DataSource slave) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("master", master); targetDataSources.put("slave", slave); DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSources); dataSource.setDefaultTargetDataSource(master); return dataSource; }3.3 读写分离切面配置
通过AOP自动切换数据源:
@Aspect @Component @Order(1) public class DataSourceAspect { @Pointcut("execution(* com.example.repository..*.select*(..)) || " + "execution(* com.example.repository..*.get*(..)) || " + "execution(* com.example.repository..*.find*(..))") public void readPointcut() {} @Pointcut("execution(* com.example.repository..*.insert*(..)) || " + "execution(* com.example.repository..*.update*(..)) || " + "execution(* com.example.repository..*.delete*(..))") public void writePointcut() {} @Before("readPointcut()") public void beforeRead() { DynamicDataSource.setDataSourceKey("slave"); } @Before("writePointcut()") public void beforeWrite() { DynamicDataSource.setDataSourceKey("master"); } @After("readPointcut() || writePointcut()") public void afterExecution() { DynamicDataSource.clearDataSourceKey(); } }性能优化点:在高并发场景下,ThreadLocal可能成为性能瓶颈。我们实测在QPS>3000时,可以考虑改用TransmittableThreadLocal或Alibaba的TTL实现。
4. MyBatis集成与事务管理
4.1 SqlSessionFactory配置
需要为MyBatis指定我们创建的路由数据源:
@Bean public SqlSessionFactory sqlSessionFactory(DynamicDataSource dataSource) throws Exception { SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(dataSource); sessionFactory.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/*.xml")); return sessionFactory.getObject(); }4.2 事务管理的特殊处理
在读写分离架构中,事务需要特别注意:
@Configuration @EnableTransactionManagement public class TransactionConfig { @Bean public TransactionInterceptor transactionInterceptor(PlatformTransactionManager transactionManager) { TransactionInterceptor interceptor = new TransactionInterceptor(); interceptor.setTransactionManager(transactionManager); Properties attributes = new Properties(); attributes.setProperty("get*", "PROPAGATION_REQUIRED,readOnly"); attributes.setProperty("select*", "PROPAGATION_REQUIRED,readOnly"); attributes.setProperty("insert*", "PROPAGATION_REQUIRED,-Exception"); attributes.setProperty("update*", "PROPAGATION_REQUIRED,-Exception"); attributes.setProperty("delete*", "PROPAGATION_REQUIRED,-Exception"); interceptor.setTransactionAttributes(attributes); return interceptor; } }4.3 强制走主库的场景处理
某些查询必须走主库以保证数据一致性:
@Service public class OrderService { @Transactional public Order createOrder(Order order) { // 写入主库 orderMapper.insert(order); // 强制从主库读取 DynamicDataSource.setDataSourceKey("master"); Order newOrder = orderMapper.selectById(order.getId()); DynamicDataSource.clearDataSourceKey(); return newOrder; } }5. 生产环境优化方案
5.1 连接池监控配置
Druid提供了强大的监控功能:
@Bean public ServletRegistrationBean<StatViewServlet> druidServlet() { ServletRegistrationBean<StatViewServlet> reg = new ServletRegistrationBean<>(); reg.setServlet(new StatViewServlet()); reg.addUrlMappings("/druid/*"); reg.addInitParameter("loginUsername", "admin"); reg.addInitParameter("loginPassword", "admin123"); return reg; } @Bean public FilterRegistrationBean<WebStatFilter> filterRegistrationBean() { FilterRegistrationBean<WebStatFilter> reg = new FilterRegistrationBean<>(); reg.setFilter(new WebStatFilter()); reg.addUrlPatterns("/*"); reg.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.css,/druid/*"); return reg; }5.2 从库负载均衡策略
当有多个从库时,可以扩展路由策略:
public class RandomSlaveDataSource extends AbstractRoutingDataSource { private List<Object> slaveKeys = new ArrayList<>(); private Random random = new Random(); public void setSlaveKeys(List<Object> slaveKeys) { this.slaveKeys = slaveKeys; } @Override protected Object determineCurrentLookupKey() { if(DynamicDataSource.isChoiceSlave()) { return slaveKeys.get(random.nextInt(slaveKeys.size())); } return "master"; } }5.3 故障转移与健康检查
实现简单的从库健康检查:
@Scheduled(fixedRate = 30000) public void checkSlaveHealth() { DataSource slave = applicationContext.getBean("slaveDataSource", DataSource.class); try (Connection conn = slave.getConnection(); Statement stmt = conn.createStatement()) { stmt.executeQuery("SELECT 1"); } catch (SQLException e) { DynamicDataSource.removeDataSource("slave"); // 发送告警通知 } }6. 常见问题排查指南
6.1 从库延迟导致的数据不一致
现象:刚插入的数据查询不到 解决方案:
- 在关键业务逻辑中强制走主库
- 使用ShardingSphere的hint强制路由
- 监控从库的Seconds_Behind_Master参数
6.2 事务中的跨库查询
现象:事务内先写后查出现数据不一致 解决方案:
- 使用@Transactional(propagation = Propagation.NOT_SUPPORTED)暂停事务
- 在事务方法开始前明确设置数据源
- 考虑使用分布式事务框架如Seata
6.3 MyBatis缓存导致的问题
现象:从库数据已更新但查询结果未变 解决方案:
- 在select语句上添加flushCache="true"
- 配置局部缓存scope="statement"
- 禁用二级缓存:mybatis.configuration.cache-enabled=false
在实际项目中,我们还需要考虑分库分表与读写分离的协同问题。去年我们在处理一个日订单量50万+的系统时,就遇到了分片键与读路由的冲突问题。最终的解决方案是在应用层维护一个分片-实例的路由表,确保同一个分片的数据始终路由到正确的物理库。