在SpringBoot项目中,使用MyBatis-Plus和Dynamic-Datasource实现读写分离是一种常见且高效的架构选择。下面我将为你详细讲解完整的实现方案,包括环境准备、配置、代码实现以及注意事项。
1. 读写分离概述与核心概念
读写分离是一种常见的数据库优化方案,其核心思想是将数据库的写操作(INSERT、UPDATE、DELETE)和读操作(SELECT)分发到不同的数据库节点上。主数据库(Master)负责处理所有写操作,而从数据库(Slave)负责处理读操作,通过数据库的主从复制机制保持数据同步。
1.1 核心价值
- 提升并发性能:将读请求分散到多个从库,减轻主库压力
- 提高系统可用性:单个从库故障不影响读服务
- 优化响应速度:专库专用,避免读写操作相互阻塞
1.2 技术架构
markdown
应用程序 → Dynamic-Datasource → 主库(写操作)
↓
从库(读操作)
图:读写分离架构示意图
2. 环境准备与依赖配置
2.1 添加Maven依赖
首先在项目的pom.xml中添加必要的依赖:
xml
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus 启动器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
<!-- Dynamic-Datasource 启动器(核心依赖) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.6.1</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 连接池(可选,HikariCP已内置) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
</dependencies>
citation:2][citation:3
2.2 配置文件设置
在application.yml中配置主从数据源:
yaml
spring:
datasource:
dynamic:
primary: master # 设置默认数据源为主库
strict: false # 是否严格匹配数据源,false时未匹配到指定数据源则使用默认数据源
datasource:
# 主库配置(写操作)
master:
url: jdbc:mysql://master-host:3306/core?useSSL=false&serverTimezone=Asia/Shanghai
username: admin
password: master@123
driver-class-name: com.mysql.cj.jdbc.Driver
# 从库配置(读操作)- 支持多个从库
slave1:
url: jdbc:mysql://slave1-host:3306/core?useSSL=false&serverTimezone=Asia/Shanghai
username: readonly
password: slave@123
driver-class-name: com.mysql.cj.jdbc.Driver
slave2:
url: jdbc:mysql://slave2-host:3306/core?useSSL=false&serverTimezone=Asia/Shanghai
username: readonly
password: slave@123
driver-class-name: com.mysql.cj.jdbc.Driver
# 负载均衡策略配置(多个从库时生效)
strategy:
slave: round_robin # 从库负载均衡策略:random(随机)/round_robin(轮询)
# 连接池配置(HikariCP)
hikari:
max-pool-size: 20 # 最大连接数
min-idle: 5 # 最小空闲连接
connection-timeout: 30000 # 连接超时时间(ms)
idle-timeout: 600000 # 空闲连接超时时间(ms)
max-lifetime: 1800000 # 连接最大生命周期(ms)
# MyBatis-Plus 配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名转换
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志(调试用)
global-config:
db-config:
id-type: auto # 主键策略自增
citation:2][citation:6
3. 数据源与MyBatis-Plus配置类
3.1 动态数据源自动配置
Dynamic-Datasource starter已经提供了自动配置,大多数情况下无需额外配置。但如果需要自定义,可以创建配置类:
less
@Configuration
@MapperScan("com.example.mapper") // 指定MyBatis mapper接口的扫描路径
public class DataSourceConfig {
/**
* 配置动态数据源
* Dynamic-Datasource会自动根据application.yml的配置创建数据源
* 此处可以添加自定义配置
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.dynamic")
public DynamicDataSourceProperties dynamicDataSourceProperties() {
return new DynamicDataSourceProperties();
}
/**
* 配置MyBatis-Plus拦截器(分页插件等)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
paginationInterceptor.setMaxLimit(1000L); // 设置最大分页限制
interceptor.addInnerInterceptor(paginationInterceptor);
// 可以添加其他插件,如乐观锁插件等
return interceptor;
}
}
citation:1][citation:5
4. 业务层实现读写分离
4.1 使用@DS注解手动切换数据源
最直接的方式是在Service层使用@DS注解指定数据源:
java
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
/**
* 写操作:使用@DS("master")指定主库
* 注意:写操作建议都使用主库,确保数据一致性
*/
@DS("master") // 指定使用主库
@Transactional(rollbackFor = Exception.class) // 添加事务管理
@Override
public boolean createUser(User user) {
// 参数校验
if (user == null || StringUtils.isBlank(user.getUsername())) {
throw new IllegalArgumentException("用户信息不完整");
}
// 设置创建时间
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
// 执行插入操作,这里会使用主库
boolean result = save(user);
// 这里可以添加其他业务逻辑
log.info("创建用户成功,用户ID: {}", user.getId());
return result;
}
/**
* 读操作:使用@DS("slave")指定从库
* 框架会根据负载均衡策略选择slave1或slave2
*/
@DS("slave") // 指定使用从库(负载均衡)
@Override
public User getUserById(Long id) {
// 参数校验
if (id == null || id <= 0) {
throw new IllegalArgumentException("用户ID不合法");
}
// 查询用户信息,这里会使用从库
User user = getById(id);
if (user == null) {
log.warn("未找到对应用户,用户ID: {}", id);
throw new RuntimeException("用户不存在");
}
return user;
}
/**
* 批量查询:同样使用从库
*/
@DS("slave")
@Override
public List<User> listUsersByCondition(UserQuery query) {
// 构建查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
if (StringUtils.isNotBlank(query.getUsername())) {
wrapper.like(User::getUsername, query.getUsername());
}
if (query.getStatus() != null) {
wrapper.eq(User::getStatus, query.getStatus());
}
// 添加排序
wrapper.orderByDesc(User::getCreateTime);
// 执行查询,使用从库
return list(wrapper);
}
/**
* 更新操作:必须使用主库
*/
@DS("master")
@Transactional(rollbackFor = Exception.class)
@Override
public boolean updateUser(User user) {
if (user == null || user.getId() == null) {
throw new IllegalArgumentException("用户信息不完整");
}
// 设置更新时间
user.setUpdateTime(LocalDateTime.now());
// 执行更新,使用主库
boolean result = updateById(user);
if (result) {
log.info("更新用户成功,用户ID: {}", user.getId());
} else {
log.error("更新用户失败,用户ID: {}", user.getId());
}
return result;
}
/**
* 删除操作:必须使用主库
*/
@DS("master")
@Transactional(rollbackFor = Exception.class)
@Override
public boolean deleteUser(Long id) {
if (id == null || id <= 0) {
throw new IllegalArgumentException("用户ID不合法");
}
// 执行删除,使用主库
boolean result = removeById(id);
if (result) {
log.info("删除用户成功,用户ID: {}", id);
} else {
log.warn("删除用户失败,用户ID: {}", id);
}
return result;
}
}
citation:2][citation:8
4.2 基于AOP的自动数据源切换(推荐)
对于大型项目,手动添加@DS注解比较繁琐,可以通过AOP自动根据方法类型切换数据源:
less
@Aspect
@Component
@Slf4j
public class DataSourceAspect {
/**
* 定义切点:拦截Service层的所有方法
*/
@Pointcut("execution(* com.example.service..*.*(..))")
public void servicePointcut() {}
/**
* 前置通知:在方法执行前选择数据源
*/
@Before("servicePointcut()")
public void before(JoinPoint joinPoint) {
// 获取方法名
String methodName = joinPoint.getSignature().getName();
// 根据方法名前缀判断是读操作还是写操作
if (isReadOperation(methodName)) {
// 读操作使用从库
DynamicDataSourceContextHolder.push("slave");
log.debug("切换数据源到从库,方法名: {}", methodName);
} else {
// 写操作使用主库
DynamicDataSourceContextHolder.push("master");
log.debug("切换数据源到主库,方法名: {}", methodName);
}
}
/**
* 后置通知:清理数据源上下文
*/
@After("servicePointcut()")
public void after() {
DynamicDataSourceContextHolder.clear();
log.debug("清除数据源上下文");
}
/**
* 判断是否为读操作
* 可以根据方法名前缀进行判断
*/
private boolean isReadOperation(String methodName) {
// 常见的读操作方法前缀
String[] readPrefixes = {"get", "select", "list", "query", "find", "search", "count", "check"};
for (String prefix : readPrefixes) {
if (methodName.startsWith(prefix)) {
return true;
}
}
return false;
}
}
citation:3][citation:7
使用AOP后,Service层的代码可以简化为:
scala
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Transactional(rollbackFor = Exception.class)
@Override
public boolean createUser(User user) {
// 自动使用主库(AOP根据方法名判断)
user.setCreateTime(LocalDateTime.now());
return save(user);
}
@Override
public User getUserById(Long id) {
// 自动使用从库(AOP根据方法名前缀"get"判断)
return getById(id);
}
// 其他方法无需添加@DS注解,由AOP自动处理
}
5. 事务处理策略
事务处理是读写分离中的关键问题,需要特别注意:
5.1 单数据源事务
less
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
/**
* 单数据源事务:所有操作都在主库执行
* 使用@DS("master")确保即使有AOP配置也使用主库
*/
@DS("master") // 显式指定主库
@Transactional(rollbackFor = Exception.class) // 声明式事务
@Override
public void createOrder(Order order) {
// 1. 保存订单主信息(主库)
save(order);
// 2. 更新库存(主库)
updateStock(order);
// 3. 记录操作日志(主库)
logOrderOperation(order);
// 如果任何一步失败,整个事务回滚
}
/**
* 只读事务:可以指定从库
*/
@DS("slave")
@Transactional(readOnly = true) // 只读事务,有优化效果
@Override
public Order getOrderDetail(Long orderId) {
return getById(orderId);
}
}
5.2 多数据源事务处理
对于涉及多个数据源的复杂事务,需要使用分布式事务解决方案:
java
@Service
@Slf4j
public class DistributedTransactionService {
@Autowired
private UserService userService;
@Autowired
private OrderService orderService;
/**
* 使用@DSTransactional实现多数据源分布式事务
* 注意:需要集成Seata等分布式事务框架
*/
// @DSTransactional // 分布式事务注解(需要额外配置)
@Transactional(rollbackFor = Exception.class)
public void placeOrder(Order order, User user) {
try {
// 1. 用户操作(主库)
userService.updateUser(user);
// 2. 订单操作(主库)
orderService.createOrder(order);
// 模拟业务异常
if (order.getAmount() == null) {
throw new RuntimeException("订单金额不能为空");
}
log.info("下单成功");
} catch (Exception e) {
log.error("下单失败,已回滚", e);
throw e; // 抛出异常触发回滚
}
}
}
citation:7
6. 高级功能与优化策略
6.1 多从库负载均衡
当配置多个从库时,Dynamic-Datasource支持多种负载均衡策略:
yaml
spring:
datasource:
dynamic:
datasource:
master:
# 主库配置...
slave1:
# 从库1配置...
slave2:
# 从库2配置...
slave3:
# 从库3配置...
strategy:
slave: round_robin # 负载均衡策略
# 可选值:
# random - 随机选择(默认)
# round_robin - 轮询
# weight_round_robin - 加权轮询(需要额外配置权重)
负载均衡策略对比:
| 策略类型 | 描述 | 适用场景 |
|---|---|---|
| random | 随机选择从库 | 简单的读负载均衡 |
| round_robin | 轮询选择从库 | 从库配置相近,需要均匀分布 |
| weight_round_robin | 根据权重选择 | 从库配置不同,按性能分配负载 |
citation:3][citation:7
6.2 健康检查与故障转移
less
@Component
@Slf4j
public class DataSourceHealthCheck {
@Autowired
private DataSource dataSource;
/**
* 定时检查从库健康状态
*/
@Scheduled(fixedRate = 60000) // 每分钟执行一次
public void checkSlaveHealth() {
// 这里可以实现从库健康检查逻辑
// 如果某个从库不可用,可以动态将其从负载均衡池中移除
log.info("执行从库健康检查...");
// 实际实现中可以使用JDBC测试连接或查询数据库状态
}
/**
* 获取从库同步延迟
*/
public Long getReplicationDelay(String slaveName) {
// 执行SQL查询从库同步状态
// SHOW SLAVE STATUS 可以获取Seconds_Behind_Master等信息
// 如果延迟过大,可以临时将该从库标记为不可用
return 0L; // 返回延迟秒数
}
}
citation:2
7. 测试与验证
7.1 单元测试
scss
@SpringBootTest
@Slf4j
class ReadWriteSeparationTest {
@Autowired
private UserService userService;
/**
* 测试写操作(应该路由到主库)
*/
@Test
void testWriteOperation() {
User user = new User();
user.setUsername("testUser");
user.setEmail("test@example.com");
boolean result = userService.createUser(user);
assertTrue(result);
log.info("写操作测试通过,应路由到主库");
}
/**
* 测试读操作(应该路由到从库)
*/
@Test
void testReadOperation() {
User user = userService.getUserById(1L);
assertNotNull(user);
log.info("读操作测试通过,应路由到从库");
}
/**
* 测试事务内的数据源选择
*/
@Test
void testTransactionalOperation() {
// 测试事务方法,应始终使用主库
User user = userService.getUserById(1L);
assertNotNull(user);
log.info("事务内操作测试完成");
}
}
7.2 验证数据源路由
在application.yml中开启SQL日志,验证读写分离是否生效:
yaml
# 开启MyBatis SQL日志
logging:
level:
com.example.mapper: debug # Mapper接口的包路径
com.baomidou.dynamic.datasource: debug # Dynamic-Datasource日志
观察控制台输出,应该能看到类似日志:
sql
2025-11-11 10:00:00 | Master DataSource | INSERT INTO user ...
2025-11-11 10:00:05 | Slave DataSource | SELECT * FROM user ...
citation:3
8. 生产环境注意事项
8.1 主从同步延迟处理
kotlin
@Service
public class CriticalReadService {
@Autowired
private UserMapper userMapper;
/**
* 对于一致性要求高的读操作,强制走主库
*/
@DS("master") // 强制使用主库,避免读取旧数据
public User getCriticalUserInfo(Long userId) {
// 例如:账户余额、订单状态等关键信息
return userMapper.selectById(userId);
}
/**
* 检查数据是否已同步
*/
public boolean waitForReplication(Long userId, int maxWaitSeconds) {
for (int i = 0; i < maxWaitSeconds; i++) {
User masterUser = getFromMaster(userId);
User slaveUser = getFromSlave(userId);
if (Objects.equals(masterUser, slaveUser)) {
return true; // 数据已同步
}
try {
Thread.sleep(1000); // 等待1秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
return false; // 同步超时
}
}
8.2 监控与告警
yaml
# 连接池监控配置
spring:
datasource:
dynamic:
druid:
# 开启监控统计
filters: stat,wall,log4j
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: false
login-username: admin
login-password: admin
9. 常见问题与解决方案
9.1 事务内数据源切换失效
问题 :在@Transactional方法内,数据源在方法开始时确定,方法内部调用无法切换数据源。
解决方案:
less
@Service
public class TransactionalService {
@Autowired
private ApplicationContext applicationContext;
@Transactional
public void transactionalMethod() {
// 方法内需要切换数据源时,通过代理对象调用
TransactionalService self = applicationContext.getBean(TransactionalService.class);
// 通过代理对象调用,可以正常切换数据源
self.readOperation(); // 使用从库
self.writeOperation(); // 使用主库
}
@DS("slave")
public void readOperation() {
// 读操作
}
@DS("master")
public void writeOperation() {
// 写操作
}
}
9.2 动态增减数据源
typescript
@Service
public class DynamicDataSourceService {
@Autowired
private DynamicDataSourceProvider dataSourceProvider;
/**
* 运行时动态添加数据源
*/
public void addDataSource(String dataSourceName, DataSourceProperty property) {
DynamicDataSourceContextHolder.addDataSource(dataSourceName, property);
log.info("动态添加数据源: {}", dataSourceName);
}
/**
* 运行时移除数据源
*/
public void removeDataSource(String dataSourceName) {
DynamicDataSourceContextHolder.removeDataSource(dataSourceName);
log.info("动态移除数据源: {}", dataSourceName);
}
}
总结
通过SpringBoot + MyBatis-Plus + Dynamic-Datasource实现读写分离,可以显著提升数据库读性能和高可用性。关键点包括:
- 正确配置:确保主从数据源配置正确,负载均衡策略合理
- 合理使用注解 :在适当的方法上使用
@DS注解或通过AOP自动切换 - 事务管理:注意事务内的数据源行为,关键操作强制走主库
- 监控告警:建立完善的监控体系,及时发现主从延迟等问题
- 故障处理:实现从库故障自动转移和恢复机制
这种架构在读写比例高的应用中能带来显著的性能提升,但也需要处理好主从同步延迟等一致性问题。