本文目录导读:

在Java中实现主从数据源(读写分离)有多种方式,这里我给你一个最常用、最实用的完整方案,使用Spring Boot + MyBatis + 动态数据源(基于AbstractRoutingDataSource)。
核心实现方案
Maven依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.18</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
</dependencies>
数据库配置(application.yml)
spring:
datasource:
# 主库(写)
master:
url: jdbc:mysql://localhost:3306/db_master?useUnicode=true&characterEncoding=utf-8
username: root
password: root123
driver-class-name: com.mysql.cj.jdbc.Driver
# 从库(读) - 可以有多个
slave:
- url: jdbc:mysql://localhost:3307/db_slave1?useUnicode=true&characterEncoding=utf-8
username: root
password: root123
driver-class-name: com.mysql.cj.jdbc.Driver
- url: jdbc:mysql://localhost:3308/db_slave2?useUnicode=true&characterEncoding=utf-8
username: root
password: root123
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
定义数据源枚举
public enum DataSourceType {
MASTER("master"),
SLAVE("slave");
private String name;
DataSourceType(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
实现动态数据源核心类
/**
* 动态数据源上下文持有者
*/
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
CONTEXT_HOLDER.set(dataSourceType);
}
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
/**
* 动态数据源 - 继承AbstractRoutingDataSource
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSourceType();
}
}
/**
* 数据源配置类
*/
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public DynamicDataSource dynamicDataSource() {
// 这里简化处理,实际应该从配置读取多个从库
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER.getName(), masterDataSource());
targetDataSources.put(DataSourceType.SLAVE.getName(), slaveDataSource());
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
}
注解定义(用于标记读写操作)
/**
* 读操作注解 - 走从库
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
boolean value() default true;
}
/**
* 写操作注解 - 走主库
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MasterOnly {
boolean value() default true;
}
AOP切面实现数据源自动切换
@Aspect
@Component
@Order(1) // 优先级高,先于事务执行
public class DataSourceAspect {
private static final Logger log = LoggerFactory.getLogger(DataSourceAspect.class);
/**
* 对service层方法进行拦截(这里简化为拦截所有方法)
*/
@Pointcut("execution(* com.example..service.*.*(..))")
public void servicePointcut() {}
@Before("servicePointcut()")
public void before(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 1. 检查方法上的@MasterOnly注解
MasterOnly masterOnly = method.getAnnotation(MasterOnly.class);
if (masterOnly != null) {
DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.getName());
log.debug("Set DataSource to MASTER for method: {}", method.getName());
return;
}
// 2. 检查方法上的@ReadOnly注解
ReadOnly readOnly = method.getAnnotation(ReadOnly.class);
if (readOnly != null) {
// 从从库中随机选择一个(负载均衡)
String slaveKey = selectSlave();
DynamicDataSourceContextHolder.setDataSourceType(slaveKey);
log.debug("Set DataSource to SLAVE for method: {}", method.getName());
return;
}
// 3. 默认走主库
DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.getName());
}
@After("servicePointcut()")
public void after(JoinPoint joinPoint) {
DynamicDataSourceContextHolder.clearDataSourceType();
log.debug("Cleared DataSource context for method: {}",
joinPoint.getSignature().getName());
}
/**
* 简单的从库负载均衡 - 轮询
*/
private String selectSlave() {
// 可以在这里实现随机、轮询、最少连接等算法
// 这里简单返回SLAVE,如果有多个从库可以扩展
return DataSourceType.SLAVE.getName();
}
}
事务支持(重要)
/**
* 事务管理器配置 - 使用动态数据源
*/
@Configuration
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(DynamicDataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
/**
* 支持在事务内切换数据源
*/
@Bean
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
return new TransactionTemplate(transactionManager);
}
}
业务层使用示例
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 写操作 - 自动走主库
*/
@Transactional
@MasterOnly
public void createUser(User user) {
userMapper.insert(user);
}
/**
* 读操作 - 自动走从库
*/
@ReadOnly
public User getUserById(Long id) {
return userMapper.selectById(id);
}
/**
* 读写混合操作
* 注意:事务内读操作需要特别注意,默认走主库
*/
@Transactional
@MasterOnly
public User updateAndGetUser(User user) {
userMapper.updateById(user);
// 这里虽然是读,但在事务内,需要走主库
return userMapper.selectById(user.getId());
}
/**
* 复杂查询 - 走从库
*/
@ReadOnly
public List<User> getUserList(String name) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.like(User::getName, name);
return userMapper.selectList(wrapper);
}
}
高级扩展方案
多从库负载均衡
/**
* 动态数据源负载均衡器
*/
@Component
public class DynamicDataSourceBalancer {
// 模拟多个从库
private static final List<String> SLAVE_KEYS = Arrays.asList(
"slave1", "slave2", "slave3"
);
private AtomicInteger counter = new AtomicInteger(0);
public String getSlaveKey() {
// 轮询
int index = counter.getAndIncrement() % SLAVE_KEYS.size();
if (counter.get() > Integer.MAX_VALUE - 10000) {
counter.set(0);
}
return SLAVE_KEYS.get(index);
}
}
读写分离数据源配置优化
@Configuration
public class MultiDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slaves")
public List<DataSource> slaveDataSources() {
// 从配置读取多个从库
return new ArrayList<>();
}
@Bean
@Primary
public DataSource routingDataSource() {
DynamicDataSource routingDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource());
// 注册多个从库
List<DataSource> slaves = slaveDataSources();
for (int i = 0; i < slaves.size(); i++) {
targetDataSources.put("slave" + (i + 1), slaves.get(i));
}
routingDataSource.setDefaultTargetDataSource(masterDataSource());
routingDataSource.setTargetDataSources(targetDataSources);
return routingDataSource;
}
}
注意事项
- 事务内一致性:在事务内读操作建议走主库,避免读到脏数据
- 数据同步延迟:主从复制有延迟,实时性要求高的读操作走主库
- 连接池配置:主从库使用独立的连接池配置
- 监控告警:监控主从同步状态和延迟时间
- 异常处理:从库不可用自动切换到主库
这个方案是最常用的注解驱动 + AOP拦截方式,易于理解、便于维护,适合大部分业务场景。