读写分离
读写分离将写操作路由到主库(Master),读操作路由到从库(Slave/Replica),以此减轻主库压力、提升查询吞吐量。属于多数据源的特定场景。
整体架构
应用层
│
┌───────┴────────┐
│ 路由逻辑 │ ← 判断当前操作是读还是写
└───────┬────────┘
┌───────┴────────┐
▼ ▼
主库 (Master) 从库 (Replica)
INSERT/UPDATE SELECT
DELETE/DDL 负载均衡(多从库轮询)
│ ▲
└── 主从同步 ─────┘ (MySQL Binlog 复制,异步/半同步)
注意:主从同步存在延迟(通常毫秒级,高负载时可能更长),写后立即读应强制走主库。
方案一:dynamic-datasource 注解路由(推荐)
引入 dynamic-datasource-spring-boot-starter,详见 多数据源。
spring:
datasource:
dynamic:
primary: master
datasource:
master:
url: jdbc:mysql://master-host:3306/db
username: root
password: secret
slave1:
url: jdbc:mysql://slave1-host:3306/db
username: root
password: secret
slave2:
url: jdbc:mysql://slave2-host:3306/db
username: root
password: secret@Service
public class UserService {
@DS("slave1")
public List<User> findAll() { ... } // 读从库
// 写操作不加 @DS,走默认主库
@Transactional
public User create(User user) { ... }
}多从库负载均衡:通过 dynamic-datasource 的分组功能,将多个从库配置为同一分组,框架自动轮询:
spring:
datasource:
dynamic:
datasource:
slave_1: # 分组名 slave,编号 _1
url: ...
slave_2: # 分组名 slave,编号 _2
url: ...@DS("slave") // 指定分组名,自动轮询 slave_1 / slave_2
public List<User> findAll() { ... }方案二:AOP + AbstractRoutingDataSource
适合不引入第三方框架的场景,完全自定义路由逻辑。
1. 数据源上下文
public class DataSourceContext {
private static final ThreadLocal<String> HOLDER = new ThreadLocal<>();
public static final String MASTER = "master";
public static final String SLAVE = "slave";
public static void useMaster() { HOLDER.set(MASTER); }
public static void useSlave() { HOLDER.set(SLAVE); }
public static String get() { return HOLDER.get(); }
public static void clear() { HOLDER.remove(); }
}2. 路由数据源
public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContext.get();
}
}3. 注册 Bean
@Configuration
public class DataSourceConfig {
@Bean("masterDs")
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean("slaveDs")
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource routingDataSource(
@Qualifier("masterDs") DataSource master,
@Qualifier("slaveDs") DataSource slave) {
Map<Object, Object> targets = Map.of(
DataSourceContext.MASTER, master,
DataSourceContext.SLAVE, slave
);
ReadWriteRoutingDataSource ds = new ReadWriteRoutingDataSource();
ds.setDefaultTargetDataSource(master);
ds.setTargetDataSources(targets);
return ds;
}
}4. AOP 切面自动路由
@Aspect
@Component
@Order(1) // 需在事务切面之前执行
public class ReadWriteAspect {
// @Transactional(readOnly = true) 的方法走从库
@Around("@annotation(transactional)")
public Object route(ProceedingJoinPoint pjp,
Transactional transactional) throws Throwable {
try {
if (transactional.readOnly()) {
DataSourceContext.useSlave();
} else {
DataSourceContext.useMaster();
}
return pjp.proceed();
} finally {
DataSourceContext.clear(); // 防止线程复用时污染
}
}
}使用时只需在 Service 上标注 readOnly:
@Service
public class UserService {
@Transactional(readOnly = true) // → 从库
public List<User> findAll() { ... }
@Transactional // → 主库
public User create(User user) { ... }
}主从延迟问题处理
写后立即读需强制走主库:
// 方式一:注解切换
@Transactional
public void createAndReturn(User user) {
userRepo.save(user);
// 同一事务内后续查询已在主库连接上,无需额外处理
}
// 方式二:写操作后临时强制主库(跨方法场景)
public User createUser(User user) {
userRepo.save(user);
DataSourceContext.useMaster(); // 强制后续读取走主库
return userRepo.findById(user.getId()).orElseThrow();
}
// 方式三:业务上接受最终一致(异步场景,不强制)ShardingSphere 方案
Apache ShardingSphere 同时支持分库分表和读写分离,配置驱动,无需 AOP:
spring:
datasource:
driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
url: jdbc:shardingsphere:classpath:shardingsphere.yml# shardingsphere.yml
rules:
- !READWRITE_SPLITTING
dataSources:
rw_ds:
writeDataSourceName: master
readDataSourceNames:
- slave1
- slave2
loadBalancerName: round_robin
loadBalancers:
round_robin:
type: ROUND_ROBIN