✨ 读写分离的核心价值
在高并发场景下,数据库往往成为系统瓶颈。读写分离通过将写操作定向到主库、读操作分发到从库,显著提升系统读性能和数据可用性。当主库出现故障时,从库可以继续提供读服务,提高系统的稳定性。
⚙️ 项目依赖配置
首先在pom.xml中添加必要依赖:
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
</dependencies>
📁 核心实现代码详解
1. 配置文件设置(application.yml)
yaml
spring:
datasource:
# 主库配置(写操作)
master:
jdbc-url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: master_password
driver-class-name: com.mysql.cj.jdbc.Driver
# Hikari连接池配置
hikari:
maximum-pool-size: 10
minimum-idle: 5
idle-timeout: 30000
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
# 从库配置(读操作)
slave:
jdbc-url: jdbc:mysql://localhost:3306/slave_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: slave_password
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 15 # 从库可以配置更多连接,因为读操作通常更频繁
minimum-idle: 8
idle-timeout: 30000
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
2. 数据源枚举定义
arduino
/**
* 数据源类型枚举
* 用于标识当前操作应使用主库还是从库
*/
public enum DataSourceType {
MASTER, // 主库:用于写操作(INSERT、UPDATE、DELETE)
SLAVE // 从库:用于读操作(SELECT)
}
3. 数据源上下文管理器
csharp
/**
* 数据源上下文管理器(基于ThreadLocal实现线程隔离)
* 功能:保存当前线程使用的数据源类型,确保多线程环境下数据源切换不会相互干扰
*/
public class DataSourceContextHolder {
// 使用ThreadLocal保证线程安全,每个线程有独立的数据源上下文
private static final ThreadLocal<DataSourceType> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 设置当前线程的数据源类型
* @param dataSourceType 数据源类型(MASTER或SLAVE)
*/
public static void setDataSourceType(DataSourceType dataSourceType) {
CONTEXT_HOLDER.set(dataSourceType);
}
/**
* 获取当前线程的数据源类型
* @return 当前数据源类型,默认为MASTER(保证写操作可靠性)
*/
public static DataSourceType getDataSourceType() {
return CONTEXT_HOLDER.get() == null ? DataSourceType.MASTER : CONTEXT_HOLDER.get();
}
/**
* 清除当前线程的数据源类型
* 防止内存泄漏,特别是在线程池场景下
*/
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
4. 动态路由数据源
scala
/**
* 动态路由数据源(继承Spring的AbstractRoutingDataSource)
* 核心功能:根据当前上下文动态选择主库或从库
*/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
/**
* 决定当前数据源查找键(Spring在每次数据库操作前调用此方法)
* @return 数据源查找键(MASTER或SLAVE)
*/
@Override
protected Object determineCurrentLookupKey() {
DataSourceType dataSourceType = DataSourceContextHolder.getDataSourceType();
System.out.println("当前使用的数据源: " + dataSourceType);
return dataSourceType;
}
}
5. 数据源配置类
less
/**
* 数据源配置类(核心配置)
* 配置主从数据源并初始化路由数据源
*/
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = "com.example.repository",
entityManagerFactoryRef = "entityManagerFactory",
transactionManagerRef = "transactionManager"
)
public class DataSourceConfig {
/**
* 主库数据源(写操作)
*/
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
/**
* 从库数据源(读操作)
*/
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
/**
* 动态路由数据源(优先级最高,作为主数据源)
*/
@Primary
@Bean(name = "routingDataSource")
public DataSource routingDataSource(
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
// 配置目标数据源映射
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER, masterDataSource);
targetDataSources.put(DataSourceType.SLAVE, slaveDataSource);
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(masterDataSource); // 默认使用主库
return routingDataSource;
}
/**
* 实体管理器工厂
*/
@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("routingDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("com.example.entity")
.persistenceUnit("mysqlUnit")
.build();
}
/**
* 事务管理器
*/
@Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager(
@Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
6. AOP切面实现自动路由
java
/**
* 数据源切面配置(基于AOP自动切换数据源)
* 通过方法名自动识别读写操作,实现数据源动态路由
*/
@Aspect
@Component
@Order(1) // 确保在事务切面之前执行
public class DataSourceAspect {
/**
* 写操作切点(insert、update、delete、save开头的方法)
*/
@Before("execution(* com.example.service..*.create*(..)) || " +
"execution(* com.example.service..*.update*(..)) || " +
"execution(* com.example.service..*.delete*(..)) || " +
"execution(* com.example.service..*.save*(..))")
public void setWriteDataSourceType() {
DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
System.out.println("切换到主库(写操作)");
}
/**
* 读操作切点(select、get、find、query开头的方法)
*/
@Before("execution(* com.example.service..*.select*(..)) || " +
"execution(* com.example.service..*.get*(..)) || " +
"execution(* com.example.service..*.find*(..)) || " +
"execution(* com.example.service..*.query*(..))")
public void setReadDataSourceType() {
DataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);
System.out.println("切换到从库(读操作)");
}
/**
* 后置处理:清理数据源上下文
*/
@After("execution(* com.example.service..*.*(..))")
public void clearDataSourceType() {
DataSourceContextHolder.clearDataSourceType();
System.out.println("清理数据源上下文");
}
}
7. 业务层使用示例
typescript
/**
* 用户服务实现类
* 演示读写分离的实际应用
*/
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
/**
* 新增用户(写操作自动路由到主库)
*/
@Override
public User createUser(User user) {
// 方法名以"create"开头,AOP会自动切换到MASTER数据源
return userRepository.save(user);
}
/**
* 根据ID查询用户(读操作自动路由到从库)
*/
@Override
@Transactional(readOnly = true) // 只读事务优化性能
public User getUserById(Long id) {
// 方法名以"get"开头,AOP会自动切换到SLAVE数据源
return userRepository.findById(id).orElse(null);
}
/**
* 查询所有用户(读操作)
*/
@Override
@Transactional(readOnly = true)
public List<User> getAllUsers() {
return userRepository.findAll();
}
/**
* 更新用户信息(写操作)
*/
@Override
public User updateUser(User user) {
return userRepository.save(user);
}
}
🔍 测试与验证
单元测试类
scss
/**
* 读写分离测试类
*/
@SpringBootTest
class ReadWriteSeparationTest {
@Autowired
private UserService userService;
/**
* 测试写操作(应路由到主库)
*/
@Test
void testWriteOperation() {
User user = new User();
user.setUsername("testUser");
user.setPassword("password");
User savedUser = userService.createUser(user);
Assertions.assertNotNull(savedUser.getId());
System.out.println("写操作测试通过(路由到主库)");
}
/**
* 测试读操作(应路由到从库)
*/
@Test
void testReadOperation() {
List<User> users = userService.getAllUsers();
Assertions.assertNotNull(users);
System.out.println("读操作测试通过(路由到从库)");
}
/**
* 测试读写混合操作
*/
@Test
void testReadWriteMix() {
// 写操作
User user = new User();
user.setUsername("mixUser");
userService.createUser(user);
// 读操作
User foundUser = userService.getUserById(1L);
Assertions.assertNotNull(foundUser);
System.out.println("读写混合操作测试通过");
}
}
⚠️ 关键注意事项
1. 主从同步延迟处理
在读写分离架构中,主从同步存在延迟可能性。刚写入主库的数据可能不会立即在从库中可用。
解决方案:
scss
/**
* 强制读主库的场景
*/
@Service
public class CriticalService {
@Autowired
private UserRepository userRepository;
/**
* 重要业务:写入后立即读取,强制走主库
*/
public User createAndGetUser(User user) {
// 写入主库
User savedUser = userRepository.save(user);
// 强制从主库读取(避免同步延迟)
DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
try {
return userRepository.findById(savedUser.getId()).orElse(null);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
}
}
2. 事务中的数据处理
在事务中,所有操作应使用同一数据源。
解决方案:
typescript
@Service
public class TransactionalService {
/**
* 事务内强制使用主库
*/
@Transactional
public void complexBusinessOperation() {
// 方法开始时显式设置主库,确保事务内一致性
DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
try {
// 一系列数据库操作...
// 所有这些操作都在同一事务中,使用同一数据源
} finally {
// 事务结束后清理
DataSourceContextHolder.clearDataSourceType();
}
}
}
📊 方案优缺点分析
| 优势 | 挑战 | 应对策略 |
|---|---|---|
| 提升读性能:将读请求分发到从库 | 主从同步延迟 | 关键业务强制读主库 |
| 提高可用性:主库故障时从库可读 | 事务内数据源一致性 | 事务中强制使用主库 |
| 减轻主库压力 | 复杂SQL路由 | 明确的读写操作分离 |
💎 总结
通过以上完整的Spring Boot 3实现方案,你可以成功配置MySQL读写分离。关键在于理解动态数据源路由原理,合理处理主从同步延迟和事务一致性等挑战。这种架构能显著提升系统性能,特别适合读多写少的应用场景。