SpringBoot项目实战:用dynamic-datasource搞定MySQL读写分离(附完整配置与避坑点)
SpringBoot实战:基于dynamic-datasource的MySQL读写分离架构深度优化
当你的应用开始面临每秒上千次查询请求时,单台MySQL服务器的性能瓶颈就会像早高峰的地铁闸机一样令人窒息。去年我们电商系统在大促期间就经历过这样的至暗时刻——数据库CPU长时间保持在90%以上,订单查询延迟飙升到5秒以上。正是这次惨痛教训让我们彻底重构了数据层架构,而dynamic-datasource在这个过程中扮演了关键角色。
1. 读写分离架构设计原理
现代数据库架构设计中,读写分离就像给数据库装上了涡轮增压器。其核心思想是将写操作(INSERT/UPDATE/DELETE)定向到主库(Master),而将读操作(SELECT)分散到多个从库(Slave)。这种设计源于一个简单的事实:大多数互联网应用中,读操作通常占数据库总负载的70%-80%。
主从同步机制 是这一切的基础。当主库执行写操作后,会通过binlog将变更事件发送给从库:
# 主库配置示例
server-id = 1
log_bin = mysql-bin
binlog_format = ROW
sync_binlog = 1
从库通过I/O线程获取这些事件并写入自己的relay log,再由SQL线程重放:
# 从库配置示例
server-id = 2
relay_log = mysql-relay-bin
log_slave_updates = ON
read_only = ON
但实现读写分离需要解决几个关键问题:
- 如何透明地路由SQL到不同数据库
- 如何保证主从同步延迟不影响业务
- 如何优雅处理事务中的读写操作
- 如何实现从库的负载均衡
2. dynamic-datasource核心配置实战
在SpringBoot项目中集成dynamic-datasource,就像给应用装上了智能导航系统。以下是经过生产验证的配置方案:
2.1 基础Maven依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.6.1</version>
</dependency>
<!-- 使用HikariCP连接池 -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>
2.2 精调过的YML配置
spring:
datasource:
dynamic:
primary: master
strict: true
datasource:
master:
url: jdbc:mysql://master-db:3306/core?useSSL=false
username: admin
password: $ENC(AES加密字符串)
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
connection-timeout: 30000
slave_1:
url: jdbc:mysql://slave1-db:3306/core?useSSL=false
username: reader
password: $ENC(AES加密字符串)
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 30 # 从库通常需要更多连接
read-only: true
slave_2:
url: jdbc:mysql://slave2-db:3306/core?useSSL=false
username: reader
password: $ENC(AES加密字符串)
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 30
read-only: true
关键配置说明:
strict: true强制开发人员显式指定数据源- 从库连接池通常需要比主库更大的size
- 所有密码都应使用ENC()加密存储
2.3 分组配置的妙用
对于多个从库的场景,可以使用分组配置实现负载均衡:
spring:
datasource:
dynamic:
datasource:
master:
# 主库配置...
slave-1:
# 从库1配置...
slave-2:
# 从库2配置...
groups:
slave_group: slave-1,slave-2 # 定义从库组
这样在代码中只需指定 @DS("slave_group") ,框架会自动轮询选择可用的从库。
3. 高级应用与避坑指南
3.1 智能路由拦截器实现
比起在每个DAO方法上标注 @DS 注解,更优雅的方式是实现MyBatis拦截器:
@Intercepts({
@Signature(type=Executor.class, method="query",
args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type=Executor.class, method="update",
args={MappedStatement.class, Object.class})
})
public class ReadWriteInterceptor implements Interceptor {
private static final String MASTER = "master";
private static final String SLAVE_GROUP = "slave_group";
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
try {
if (ms.getSqlCommandType() == SqlCommandType.SELECT) {
// 特殊方法强制走主库
if (shouldRouteToMaster(ms.getId())) {
DynamicDataSourceContextHolder.push(MASTER);
} else {
DynamicDataSourceContextHolder.push(SLAVE_GROUP);
}
} else {
DynamicDataSourceContextHolder.push(MASTER);
}
return invocation.proceed();
} finally {
DynamicDataSourceContextHolder.clear();
}
}
private boolean shouldRouteToMaster(String methodId) {
// 实时性要求高的查询走主库
return methodId.contains("getRealTime")
|| methodId.contains("getCurrent");
}
}
3.2 事务处理的正确姿势
多数据源环境下的事务管理是个雷区。我们推荐以下策略:
- 跨库事务 :使用
@DSTransactional注解
@Service
public class OrderService {
@DSTransactional
public void createOrder(OrderDTO order) {
// 操作主库
orderMapper.insert(order);
// 操作其他业务库
inventoryMapper.deduct(order.getSkuId(), order.getQuantity());
}
}
- 读写分离事务 :强制读操作走主库
@Transactional
public OrderDetail getOrderDetail(Long orderId) {
// 方法开始时已经绑定到主库事务
Order order = orderMapper.selectById(orderId);
// 即使内部有查询也走主库
User user = userService.getUser(order.getUserId());
return assembleDetail(order, user);
}
3.3 监控与故障转移
在生产环境中,必须实现完善的监控:
@Slf4j
@Component
public class DataSourceHealthChecker {
@Scheduled(fixedRate = 60000)
public void checkDataSourceHealth() {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
ds.getDataSources().forEach((key, value) -> {
try (Connection conn = value.getConnection()) {
if (!conn.isValid(5)) {
log.error("数据源 {} 连接异常", key);
// 触发告警
}
} catch (SQLException e) {
log.error("数据源 {} 健康检查失败", key, e);
}
});
}
}
4. 性能优化实战技巧
4.1 连接池参数调优
不同场景下的推荐配置:
| 参数 | 主库推荐值 | 从库推荐值 | 说明 |
|---|---|---|---|
| maximumPoolSize | 20-30 | 30-50 | 从库通常需要更多读连接 |
| minimumIdle | 5 | 10 | 保持一定数量的常驻连接 |
| connectionTimeout | 30000 | 30000 | 避免长时间等待 |
| idleTimeout | 600000 | 600000 | 10分钟空闲后回收 |
| maxLifetime | 1800000 | 1800000 | 30分钟后强制重建连接 |
| validationTimeout | 5000 | 5000 | 连接验证超时时间 |
4.2 从库负载均衡策略
默认的轮询策略可能不适合所有场景,可以自定义负载均衡器:
@Configuration
public class DataSourceConfig {
@Bean
public LoadBalanceStrategy roundRobinWithWeight() {
return (group, providers) -> {
// 根据服务器配置权重选择
if ("slave_group".equals(group)) {
int totalWeight = providers.stream()
.mapToInt(p -> getWeight(p.getName()))
.sum();
int random = ThreadLocalRandom.current().nextInt(totalWeight);
int sum = 0;
for (DataSourceProvider p : providers) {
sum += getWeight(p.getName());
if (random < sum) {
return p;
}
}
}
return providers.get(0);
};
}
private int getWeight(String name) {
// 根据服务器配置返回权重
if (name.contains("slave-1")) return 3;
if (name.contains("slave-2")) return 2;
return 1;
}
}
4.3 慢查询自动路由
当从库出现慢查询时,可以自动切换到其他从库:
public class SlowQueryInterceptor {
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object monitorQueryTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
if (cost > 1000) { // 超过1秒视为慢查询
String currentDs = DynamicDataSourceContextHolder.peek();
if (currentDs != null && currentDs.startsWith("slave")) {
log.warn("慢查询检测: {}ms on {}", cost, currentDs);
// 将该从库标记为可疑,暂时从负载均衡池中移除
DataSourceHealthMonitor.markUnhealthy(currentDs);
}
}
}
}
}
在电商系统重构后,我们实现了以下优化效果:
- 数据库平均负载从90%降至35%
- 查询响应时间P99从5s降至200ms
- 主库写操作吞吐量提升40%
更多推荐
所有评论(0)