一、读写分离核心概述
1.1 什么是读写分离
MySQL 读写分离是数据库性能优化的核心方案之一,核心思想是将数据库的读操作和写操作拆分到不同数据库节点执行。通常由一台主库(Master)负责处理新增、修改、删除等写操作,多台从库(Slave)负责处理查询读操作,主库通过 binlog 日志将数据实时同步到从库,保证数据一致性。
1.2 为什么需要读写分离
- 分担数据库压力:互联网业务大多是读多写少场景,读请求极易造成数据库瓶颈,读写分离可将读写压力拆分,大幅提升数据库并发能力。
- 提升系统吞吐量:从库可横向扩展,通过多从库分担读请求,有效提升系统查询响应速度和整体吞吐量。
- 提高数据安全性:主库专注写操作,从库仅提供查询服务,降低误操作、数据篡改风险,同时从库可作为数据备份节点。
- 实现高可用:主库故障时可快速切换从库,避免单点故障,保障业务持续可用。
1.3 实现方案选型
读写分离分为代码层实现和中间件实现,本文采用轻量、无侵入、易落地的代码层动态数据源方案:基于 SpringBoot 动态数据源注解 + AOP 切面,结合 MyBatis 实现读写请求自动路由,无需部署 Sharding-JDBC、MyCat 等中间件,适合中小型项目快速落地。
二、环境准备
本次实战采用稳定主流技术版本,适配绝大多数企业开发场景:
- JDK:1.8+
- SpringBoot:2.7.x
- MyBatis:3.5.x(适配 SpringBoot 自动装配)
- MySQL:5.7 / 8.0
- 数据库架构:1主1从(最小集群架构,可扩展多从)
前置要求:已完成 MySQL 主从复制搭建(主库开启 binlog,从库配置同步,保证主从数据实时一致)。
三、项目依赖配置
在 pom.xml 中引入核心依赖,包含 SpringBoot 数据源、MyBatis、AOP、动态数据源核心依赖:
<!-- SpringBoot 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 数据源核心依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis 适配 SpringBoot -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- AOP 切面依赖(实现动态数据源切换) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
四、核心配置实现
4.1 多数据源配置(application.yml)
配置主库、从库数据源信息,主库支持读写,从库仅支持读,通过 Druid 配置连接池参数:
spring:
# 数据源配置
datasource:
# 主库数据源(写库)
master:
url: jdbc:mysql://127.0.0.1:3306/test_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
# 从库数据源(读库)
slave:
url: jdbc:mysql://127.0.0.1:3307/test_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
# Druid 连接池通用配置
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
# MyBatis 配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.readwrite.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
4.2 数据源类型枚举
定义数据源枚举,统一管理主库、从库标识,方便切面切换数据源:
package com.example.readwrite.enums;
/**
* 数据源类型枚举
*/
public enum DataSourceType {
/**
* 主库(写)
*/
MASTER,
/**
* 从库(读)
*/
SLAVE
}
4.3 自定义数据源切换注解
通过注解标记方法,实现动态指定数据源,默认走主库写操作:
package com.example.readwrite.annotation;
import com.example.readwrite.enums.DataSourceType;
import java.lang.annotation.*;
/**
* 自定义数据源切换注解
* 优先级:方法级 > 类级
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
// 默认使用主库
DataSourceType value() default DataSourceType.MASTER;
}
4.4 动态数据源上下文工具类
基于 ThreadLocal 存储当前线程数据源类型,保证多线程环境下数据源切换隔离,避免线程安全问题:
package com.example.readwrite.context;
import com.example.readwrite.enums.DataSourceType;
/**
* 数据源上下文工具类
* 用于切换、获取、清空当前线程数据源
*/
public class DataSourceContext {
// 线程本地变量,存储当前数据源类型
private static final ThreadLocal<DataSourceType> DATA_SOURCE_TYPE = new ThreadLocal<>();
/**
* 设置数据源
*/
public static void setDataSource(DataSourceType type) {
DATA_SOURCE_TYPE.set(type);
}
/**
* 获取当前数据源,默认主库
*/
public static DataSourceType getDataSource() {
return DATA_SOURCE_TYPE.get() == null ? DataSourceType.MASTER : DATA_SOURCE_TYPE.get();
}
/**
* 清空数据源
*/
public static void clearDataSource() {
DATA_SOURCE_TYPE.remove();
}
}
4.5 动态数据源配置类
继承 Spring 抽象动态数据源类,实现数据源动态路由,加载主从数据源并绑定标识:
package com.example.readwrite.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.example.readwrite.context.DataSourceContext;
import com.example.readwrite.enums.DataSourceType;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 动态数据源配置
*/
@Configuration
public class DynamicDataSourceConfig {
/**
* 主库数据源
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return new DruidDataSource();
}
/**
* 从库数据源
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return new DruidDataSource();
}
/**
* 动态路由数据源
* Primary:优先使用该数据源
*/
@Bean
@Primary
public AbstractRoutingDataSource dynamicDataSource() {
DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
// 配置多数据源映射关系
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(DataSourceType.MASTER, masterDataSource());
dataSourceMap.put(DataSourceType.SLAVE, slaveDataSource());
// 设置默认数据源(主库)
dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
// 设置所有数据源
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
/**
* 自定义数据源路由规则
*/
public static class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 从上下文获取当前数据源类型
return DataSourceContext.getDataSource();
}
}
}
4.6 AOP 切面实现数据源自动切换
通过切面拦截带有 @DataSource 注解的方法,自动切换对应数据源,方法执行后清空上下文,避免污染后续请求:
package com.example.readwrite.aspect;
import com.example.readwrite.annotation.DataSource;
import com.example.readwrite.context.DataSourceContext;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 数据源切换切面
*/
@Aspect
@Component
@Order(1) // 优先级高于事务切面
@Slf4j
public class DataSourceAspect {
@Around("@annotation(dataSource)")
public Object around(ProceedingJoinPoint point, DataSource dataSource) throws Throwable {
try {
// 设置当前方法对应的数据源
DataSourceContext.setDataSource(dataSource.value());
log.info("切换数据源成功,当前数据源:{}", dataSource.value().name());
// 执行目标方法
return point.proceed();
} finally {
// 清空数据源上下文,防止线程复用导致数据源错乱
DataSourceContext.clearDataSource();
}
}
}
五、业务层实战使用
核心规则:写操作(增删改)默认走主库,查询操作添加 @DataSource(DataSourceType.SLAVE) 注解走从库
5.1 Service 层代码示例
package com.example.readwrite.service.impl;
import com.example.readwrite.annotation.DataSource;
import com.example.readwrite.enums.DataSourceType;
import com.example.readwrite.entity.User;
import com.example.readwrite.mapper.UserMapper;
import com.example.readwrite.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
/**
* 写操作:默认主库,无需手动指定注解
*/
@Override
public int addUser(User user) {
return userMapper.insert(user);
}
/**
* 读操作:指定从库查询
*/
@Override
@DataSource(DataSourceType.SLAVE)
public List<User> listUser() {
return userMapper.selectList(null);
}
/**
* 读操作:单条查询走从库
*/
@Override
@DataSource(DataSourceType.SLAVE)
public User getUserById(Long id) {
return userMapper.selectById(id);
}
}
5.2 Mapper 层接口
package com.example.readwrite.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.readwrite.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 继承 MyBatis-Plus 通用CRUD方法,无需手写SQL
}
六、关键问题解决与注意事项
6.1 事务数据源优先级问题
Spring 事务切面优先级默认高于自定义切面,会导致事务开启后无法切换数据源。解决方案:手动设置数据源切面优先级高于事务切面(上文代码中 @Order(1) 已实现)。
6.2 主从延迟问题
MySQL 主从复制存在毫秒级延迟,极端场景下会出现写数据后立即查询,从库未同步数据的问题。解决方案:
- 实时性要求高的查询,强制走主库;
- 开启 MySQL 半同步复制,缩短同步延迟;
- 核心业务读写均走主库,非核心查询走从库。
6.3 多从库负载均衡
本文实现1主1从架构,若需多从库,可改造动态数据源逻辑,通过随机、轮询算法分发读请求到不同从库,实现读负载均衡。
6.4 数据源切换失效场景
同一事务内,第一次切换数据源后,后续所有SQL都会复用当前数据源,无法重复切换。因此禁止在同一个事务中混合读写操作。
七、测试验证
- 写操作测试:调用新增用户接口,查看日志,数据源为 MASTER,数据写入主库并同步到从库;
- 读操作测试:调用查询用户接口,日志显示数据源为 SLAVE,请求路由到从库执行;
- 并发测试:高并发查询场景下,读请求全部分摊到从库,主库仅处理写请求,数据库压力有效拆分。
八、总结
本文基于 SpringBoot + MyBatis 实现了无中间件、轻量高效的 MySQL 读写分离方案,通过 AOP 切面 + 自定义注解 实现数据源动态自动切换,完美适配读多写少的业务场景。核心亮点如下:
- 轻量无侵入:无需部署中间件,代码耦合度低,易于维护和扩展;
- 灵活可控:通过注解精准控制读写数据源,适配不同业务场景;
- 线程安全:基于 ThreadLocal 实现数据源隔离,避免并发错乱问题;
- 落地性强:适配绝大多数中小型项目,可快速上线使用。
后续可基于该方案扩展多从库负载均衡、数据源故障自动切换、动态数据源热更新等功能,进一步提升系统稳定性和并发能力。