返回 筑基・数据元府藏真
主从复制与读写分离实战
博主
大约 22 分钟
主从复制与读写分离实战
问题引入:读压力导致主库崩溃
去年618大促期间,我们的电商平台遭遇了严重的数据库性能危机。活动开始仅30分钟,主库CPU飙升至100%,大量查询请求堆积,订单写入超时,交易失败率飙升至15%。事后分析发现:
场景:电商大促期间数据库性能危机
问题统计:
- 主库CPU使用率:100%持续20分钟
- 活跃连接数:800/800(连接池耗尽)
- 查询堆积:5000+ 查询等待执行
- 订单写入超时:平均5秒,大量失败
- 主从延迟:从库落后主库8秒
- 用户投诉:"下单后查不到订单"
根本原因分析:
1. 读写未分离:所有读请求都打到主库
2. 从库闲置:3个从库CPU仅使用20%
3. 主从延迟:用户下单后查询走从库,看不到新订单
4. 缺乏监控:从库延迟未及时告警
// 问题代码:所有请求都走主库
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 创建订单 - 写入主库(正确)
public Order createOrder(OrderRequest request) {
Order order = new Order(request);
orderMapper.insert(order); // 写入主库
return order;
}
// 查询订单 - 也走主库(错误!应该走从库)
public Order getOrder(Long orderId) {
return orderMapper.selectById(orderId); // 打到主库,增加读压力
}
// 列表查询 - 也走主库(错误!)
public List<Order> listOrders(Long userId) {
return orderMapper.selectByUserId(userId); // 打到主库
}
}
事故影响:
- 直接经济损失:约200万订单受影响
- 用户信任度下降:大量负面评价
- 技术债务暴露:架构设计缺陷
现象描述:主从复制常见问题
案例1:主从延迟导致数据不一致
场景:用户下单后立即查看订单
@Service
public class OrderReadService {
@Autowired
private OrderMapper orderMapper;
// 创建订单
public Order createOrder(OrderRequest request) {
Order order = new Order(request);
orderMapper.insert(order); // 写入主库
// 立即查询订单
return getOrder(order.getOrderId()); // 走到从库,可能查不到!
}
// 查询订单 - 走从库
public Order getOrder(Long orderId) {
// 从库可能有延迟,查不到刚写入的数据
return orderMapper.selectById(orderId);
}
}
问题时间线:
T+0ms 用户提交订单
T+50ms 订单写入主库成功
T+100ms 用户跳转订单详情页
T+150ms 应用查询从库(走读写分离)
T+200ms 从库返回:订单不存在!(主从延迟500ms)
T+500ms 主从同步完成
用户体验:"我明明下单成功了,为什么查不到订单?"
影响:用户体验差,客服投诉激增。
案例2:从库故障导致查询失败
场景:从库宕机,读写分离未做故障转移
@Service
public class ProductService {
// 查询商品 - 走从库
@DataSource("slave")
public Product getProduct(Long productId) {
return productMapper.selectById(productId); // 从库宕机,查询失败
}
}
故障场景:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 应用服务 │ ──────→ │ 从库1 │ │ 从库2 │
│ │ 失败 │ (已宕机) │ │ (正常) │
│ │ └─────────────┘ └─────────────┘
│ │ ↑ ↑
│ │ 无故障转移! 未被使用
└─────────────┘
结果:
- 所有走从库的查询都失败
- 服务可用性下降50%
- 用户无法浏览商品
案例3:主从复制中断
场景:主库binlog清理导致复制中断
-- 主库配置(问题配置)
expire_logs_days = 1 -- binlog只保留1天
-- 从库因为网络问题断开2天
-- 重新连接时,需要的binlog已被清理
-- 从库错误日志
[ERROR] Slave I/O for channel '':
Got fatal error 1236 from master when reading data from binary log:
'Could not find first log file name in binary log index file'
-- 结果:需要重新搭建从库
影响:
- 从库数据过时
- 读写分离失效
- 单点故障风险
原因分析:主从复制原理
1. MySQL主从复制原理
MySQL主从复制架构:
┌─────────────────────────────────────────────────────────────────┐
│ 主从复制流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 主库 (Master) 从库 (Slave) │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Client │ │ │ │
│ │ DML/DDL │ │ │ │
│ │ ↓ │ │ │ │
│ │ ┌─────────┐ │ │ ┌─────────┐ │ │
│ │ │ 执行事务 │ │ │ │ IO线程 │ │ │
│ │ │ 生成Binlog│ │ │ │ │ │ │
│ │ └────┬────┘ │ │ │ 1.连接主库│ │ │
│ │ │ │ ←─────────── │ │ 2.请求Binlog│ │
│ │ ┌────▼────┐ │ Binlog传输 │ │ 3.写入Relay│ │
│ │ │ Binlog │ │ (网络传输) │ │ Log │ │ │
│ │ │ (二进制 │ │ ───────────→ │ └────┬────┘ │ │
│ │ │ 日志) │ │ │ │ │ │
│ │ └─────────┘ │ │ ┌────▼────┐ │ │
│ │ │ │ │ Relay │ │ │
│ │ │ │ │ Log │ │ │
│ │ │ │ │ (中继日志)│ │ │
│ │ │ │ └────┬────┘ │ │
│ │ │ │ │ │ │
│ │ │ │ ┌────▼────┐ │ │
│ │ │ │ │ SQL线程 │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ 重放SQL │ │ │
│ │ │ │ │ 更新数据 │ │ │
│ │ │ │ └────┬────┘ │ │
│ │ │ │ ↓ │ │
│ │ │ │ ┌─────────┐ │ │
│ │ │ │ │ 从库数据 │ │ │
│ │ │ │ └─────────┘ │ │
│ └───────────────┘ └───────────────┘ │
│ │
│ 复制模式: │
│ 1. 异步复制 (Async):主库不等待从库确认,性能最好,可能丢数据 │
│ 2. 半同步复制 (Semi-sync):主库等待至少一个从库确认,平衡方案 │
│ 3. 全同步复制 (Sync):主库等待所有从库确认,数据最安全,性能最差 │
│ │
└─────────────────────────────────────────────────────────────────┘
2. 复制延迟产生原因
主从延迟来源分析:
1. 网络延迟
┌─────────┐ 网络传输 ┌─────────┐
│ 主库 │ ─────────────────────→ │ 从库 │
│ │ 延迟:1-10ms │ │
└─────────┘ └─────────┘
2. 从库IO线程延迟
- 网络带宽不足
- 主库binlog生成速度过快
- 从库磁盘IO瓶颈
3. 从库SQL线程延迟(最常见)
┌─────────────────────────────────────────────┐
│ SQL线程单线程执行(MySQL 5.6之前) │
│ │
│ 主库:并行执行10个事务 │
│ 从库:串行执行10个事务 │
│ │
│ 结果:从库延迟 = 主库执行时间 × 10 │
└─────────────────────────────────────────────┘
4. 大事务延迟
- 一个事务包含大量SQL
- DDL操作(ALTER TABLE等)
- 从库执行时间远超主库
5. 锁等待
- 从库上有长查询
- SQL线程等待锁释放
3. 复制模式演进
| 复制模式 | 版本 | 特点 | 适用场景 |
|---|---|---|---|
| 异步复制 | 所有版本 | 主库不等待从库,性能最好 | 读多写少,容忍延迟 |
| 半同步复制 | 5.5+ | 至少一个从库确认 | 数据一致性要求高 |
| 组复制 | 5.7+ | 多主复制,自动故障转移 | 高可用场景 |
| GTID复制 | 5.6+ | 基于事务ID,自动定位 | 复杂拓扑,故障恢复 |
| 并行复制 | 5.6+ | SQL线程多线程执行 | 减少复制延迟 |
解决方案:主从复制与读写分离
1. 主从复制配置
1.1 主库配置
# my.cnf 主库配置
[mysqld]
# 服务器ID,必须唯一
server-id = 1
# 开启binlog
log_bin = mysql-bin
binlog_format = ROW # ROW格式,记录行级变更
binlog_row_image = FULL # 记录完整行数据
# binlog保留时间
expire_logs_days = 7
# 同步相关
sync_binlog = 1 # 每次事务同步binlog到磁盘
innodb_flush_log_at_trx_commit = 1 # 每次事务刷盘
# 半同步复制(可选)
plugin-load = rpl_semi_sync_master=semisync_master.so
rpl_semi_sync_master_enabled = 1
rpl_semi_sync_master_timeout = 1000 # 1秒超时,降级为异步
# GTID配置(推荐)
gtid_mode = ON
enforce_gtid_consistency = ON
# 复制过滤(可选)
binlog-ignore-db = mysql
binlog-ignore-db = information_schema
binlog-ignore-db = performance_schema
1.2 从库配置
# my.cnf 从库配置
[mysqld]
# 服务器ID,必须唯一
server-id = 2 # 每个从库不同
# 开启relay log
relay_log = mysql-relay-bin
relay_log_recovery = ON # 启动时恢复relay log
# 只读模式
read_only = ON
super_read_only = ON # 超级用户也只读
# 同步相关
sync_master_info = 1
sync_relay_log = 1
sync_relay_log_info = 1
# 半同步复制(可选)
plugin-load = rpl_semi_sync_slave=semisync_slave.so
rpl_semi_sync_slave_enabled = 1
# GTID配置
gtid_mode = ON
enforce_gtid_consistency = ON
# 并行复制(减少延迟)
slave_parallel_type = LOGICAL_CLOCK # 基于逻辑时钟
slave_parallel_workers = 4 # 并行线程数
slave_preserve_commit_order = ON # 保持提交顺序
# 复制过滤(可选)
replicate-ignore-db = test
replicate-ignore-db = tmp
1.3 主从复制搭建
-- 1. 主库创建复制用户
CREATE USER 'repl'@'%' IDENTIFIED BY 'repl_password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
-- 2. 主库获取binlog位置(如果是新库,可以跳过)
FLUSH TABLES WITH READ LOCK; -- 锁定所有表
SHOW MASTER STATUS;
-- 记录 File: mysql-bin.000001, Position: 154
-- 3. 主库导出数据(如果是已有数据)
-- mysqldump -u root -p --all-databases --master-data=2 > full_backup.sql
-- 4. 从库导入数据
-- mysql -u root -p < full_backup.sql
-- 5. 从库配置主库信息(传统方式)
CHANGE MASTER TO
MASTER_HOST = 'master_host',
MASTER_PORT = 3306,
MASTER_USER = 'repl',
MASTER_PASSWORD = 'repl_password',
MASTER_LOG_FILE = 'mysql-bin.000001',
MASTER_LOG_POS = 154;
-- 5. 从库配置主库信息(GTID方式,推荐)
CHANGE MASTER TO
MASTER_HOST = 'master_host',
MASTER_PORT = 3306,
MASTER_USER = 'repl',
MASTER_PASSWORD = 'repl_password',
MASTER_AUTO_POSITION = 1; -- 自动定位GTID
-- 6. 启动从库复制
START SLAVE;
-- 7. 检查复制状态
SHOW SLAVE STATUS\G
-- 关键字段:
-- Slave_IO_Running: Yes
-- Slave_SQL_Running: Yes
-- Seconds_Behind_Master: 0
-- Last_IO_Error:
-- Last_SQL_Error:
2. 读写分离实现
2.1 基于Spring的读写分离
// 数据源配置
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave1")
public DataSource slave1DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.slave2")
public DataSource slave2DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource routingDataSource() {
DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource());
targetDataSources.put("slave1", slave1DataSource());
targetDataSources.put("slave2", slave2DataSource());
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(masterDataSource());
return routingDataSource;
}
@Bean
public DataSource proxyDataSource(DataSource routingDataSource) {
return new LazyConnectionDataSourceProxy(routingDataSource);
}
}
// 动态数据源上下文
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSource(String dataSource) {
contextHolder.set(dataSource);
}
public static String getDataSource() {
return contextHolder.get();
}
public static void clear() {
contextHolder.remove();
}
}
// 动态路由数据源
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String dataSource = DataSourceContextHolder.getDataSource();
// 如果没有指定,根据操作类型自动选择
if (dataSource == null) {
// 写操作走主库
if (isWriteOperation()) {
dataSource = "master";
} else {
// 读操作走从库,轮询选择
dataSource = selectSlaveDataSource();
}
}
return dataSource;
}
private boolean isWriteOperation() {
// 判断当前是否是写操作
return TransactionSynchronizationManager.isActualTransactionActive();
}
private String selectSlaveDataSource() {
// 简单的轮询策略
int index = (int) (System.currentTimeMillis() % 2) + 1;
return "slave" + index;
}
}
// 数据源切换注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value() default "master";
}
// AOP拦截器
@Aspect
@Component
public class DataSourceAspect {
@Around("@annotation(dataSource)")
public Object around(ProceedingJoinPoint point, DataSource dataSource) throws Throwable {
String ds = dataSource.value();
// 检查从库健康状态
if (ds.startsWith("slave") && !isSlaveHealthy(ds)) {
log.warn("从库{}不健康,切换到主库", ds);
ds = "master";
}
DataSourceContextHolder.setDataSource(ds);
try {
return point.proceed();
} finally {
DataSourceContextHolder.clear();
}
}
private boolean isSlaveHealthy(String slave) {
// 检查从库延迟
Long delay = SlaveHealthChecker.getDelay(slave);
return delay != null && delay < 1000; // 延迟小于1秒认为健康
}
}
2.2 使用ShardingSphere实现读写分离
# application.yml
spring:
shardingsphere:
datasource:
names: master, slave0, slave1
master:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://master:3306/db?useSSL=false
username: root
password: password
slave0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://slave0:3306/db?useSSL=false
username: root
password: password
slave1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://slave1:3306/db?useSSL=false
username: root
password: password
rules:
read-write-splitting:
data-sources:
pr_ds:
type: Static
props:
write-data-source-name: master
read-data-source-names: slave0, slave1
load-balancer-name: round_robin
load-balancers:
round_robin:
type: ROUND_ROBIN
props:
sql-show: true
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 写操作自动走主库
@Transactional
public Order createOrder(OrderRequest request) {
Order order = new Order(request);
orderMapper.insert(order);
return order;
}
// 读操作自动走从库(轮询)
public Order getOrder(Long orderId) {
return orderMapper.selectById(orderId);
}
// 强制走主库
@HintManagerHint(shardingValue = "master")
public Order getOrderFromMaster(Long orderId) {
return orderMapper.selectById(orderId);
}
}
3. 主从延迟处理策略
3.1 延迟监控
@Component
public class ReplicationDelayMonitor {
@Autowired
private JdbcTemplate masterJdbcTemplate;
@Autowired
private List<JdbcTemplate> slaveJdbcTemplates;
@Scheduled(fixedRate = 5000)
public void monitorDelay() {
// 主库插入心跳
masterJdbcTemplate.update(
"INSERT INTO heartbeat (server_id, ts) VALUES (1, NOW(3)) " +
"ON DUPLICATE KEY UPDATE ts = NOW(3)"
);
// 检查各从库延迟
for (int i = 0; i < slaveJdbcTemplates.size(); i++) {
JdbcTemplate slave = slaveJdbcTemplates.get(i);
try {
Long delay = slave.queryForObject(
"SELECT TIMESTAMPDIFF(MICROSECOND, ts, NOW(3)) / 1000 " +
"FROM heartbeat WHERE server_id = 1",
Long.class
);
log.info("从库{}延迟: {} ms", i, delay);
// 延迟过大告警
if (delay != null && delay > 1000) {
alertService.sendAlert("主从延迟告警",
String.format("从库%d延迟超过1秒: %d ms", i, delay));
}
// 更新从库健康状态
SlaveHealthChecker.updateDelay("slave" + i, delay);
} catch (Exception e) {
log.error("检查从库{}延迟失败", i, e);
SlaveHealthChecker.markUnhealthy("slave" + i);
}
}
}
}
3.2 延迟处理方案
@Service
public class OrderServiceWithDelayHandling {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 方案1:关键读走主库
*/
public Order getOrderAfterCreate(Long orderId) {
// 检查是否是刚创建的订单(5分钟内)
String key = "order:recent:" + orderId;
Boolean isRecent = redisTemplate.hasKey(key);
if (Boolean.TRUE.equals(isRecent)) {
// 强制走主库
DataSourceContextHolder.setDataSource("master");
try {
return orderMapper.selectById(orderId);
} finally {
DataSourceContextHolder.clear();
}
}
// 走从库
return orderMapper.selectById(orderId);
}
/**
* 方案2:缓存补偿
*/
@Cacheable(value = "order", key = "#orderId")
public Order getOrder(Long orderId) {
return orderMapper.selectById(orderId);
}
@CachePut(value = "order", key = "#order.orderId")
public Order createOrder(Order order) {
orderMapper.insert(order);
// 标记为新创建订单
String key = "order:recent:" + order.getOrderId();
redisTemplate.opsForValue().set(key, "1", 5, TimeUnit.MINUTES);
return order;
}
/**
* 方案3:延迟等待
*/
public Order getOrderWithWait(Long orderId, Long createTime) {
// 如果订单刚创建,等待复制完成
long elapsed = System.currentTimeMillis() - createTime;
if (elapsed < 500) { // 500ms内创建的订单
try {
Thread.sleep(500 - elapsed); // 等待复制
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return orderMapper.selectById(orderId);
}
/**
* 方案4:延迟阈值控制
*/
public List<Order> listOrders(Long userId) {
// 检查从库延迟
Long delay = SlaveHealthChecker.getCurrentDelay();
if (delay != null && delay > 1000) {
// 延迟过大,走主库
log.warn("从库延迟{}ms,切换到主库查询", delay);
DataSourceContextHolder.setDataSource("master");
try {
return orderMapper.selectByUserId(userId);
} finally {
DataSourceContextHolder.clear();
}
}
// 走从库
return orderMapper.selectByUserId(userId);
}
}
4. 故障转移与高可用
4.1 从库故障自动切换
@Component
public class SlaveFailoverHandler {
@Autowired
private DynamicRoutingDataSource routingDataSource;
private Map<String, DataSource> availableSlaves = new ConcurrentHashMap<>();
@Scheduled(fixedRate = 10000)
public void healthCheck() {
for (String slaveName : Arrays.asList("slave1", "slave2")) {
boolean healthy = checkSlaveHealth(slaveName);
if (healthy && !availableSlaves.containsKey(slaveName)) {
// 从库恢复
recoverSlave(slaveName);
} else if (!healthy && availableSlaves.containsKey(slaveName)) {
// 从库故障
removeSlave(slaveName);
}
}
}
private boolean checkSlaveHealth(String slaveName) {
try {
DataSource dataSource = getDataSource(slaveName);
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
// 检查SQL执行
stmt.executeQuery("SELECT 1");
// 检查复制延迟
ResultSet rs = stmt.executeQuery(
"SHOW SLAVE STATUS"
);
if (rs.next()) {
String ioRunning = rs.getString("Slave_IO_Running");
String sqlRunning = rs.getString("Slave_SQL_Running");
long behindMaster = rs.getLong("Seconds_Behind_Master");
return "Yes".equals(ioRunning) &&
"Yes".equals(sqlRunning) &&
behindMaster < 10; // 延迟小于10秒
}
}
} catch (Exception e) {
log.error("从库{}健康检查失败", slaveName, e);
}
return false;
}
private void removeSlave(String slaveName) {
log.warn("从库{}故障,移除", slaveName);
availableSlaves.remove(slaveName);
// 更新路由数据源
Map<Object, Object> targetDataSources = new HashMap<>(routingDataSource.getTargetDataSources());
targetDataSources.remove(slaveName);
routingDataSource.setTargetDataSources(targetDataSources);
}
private void recoverSlave(String slaveName) {
log.info("从库{}恢复,重新加入", slaveName);
DataSource dataSource = getDataSource(slaveName);
availableSlaves.put(slaveName, dataSource);
// 更新路由数据源
Map<Object, Object> targetDataSources = new HashMap<>(routingDataSource.getTargetDataSources());
targetDataSources.put(slaveName, dataSource);
routingDataSource.setTargetDataSources(targetDataSources);
}
}
4.2 主库故障自动切换(MHA架构)
#!/bin/bash
# MHA主库故障切换脚本
# 1. 检测主库是否存活
mysql -h master -e "SELECT 1" > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "主库正常"
exit 0
fi
echo "主库故障,开始切换..."
# 2. 选择最新的从库作为新主库
new_master=$(mysql -h slave1 -e "SHOW SLAVE STATUS\G" | grep "Seconds_Behind_Master" | awk '{print $2}')
# 3. 提升从库为主库
mysql -h slave1 -e "STOP SLAVE; RESET SLAVE ALL; SET GLOBAL read_only=OFF;"
# 4. 更新应用配置(通过配置中心)
curl -X POST http://config-center/update \
-d "master.host=slave1"
# 5. 通知DBA
send_alert "主库故障切换完成,新主库: slave1"
实战案例:电商系统读写分离改造
改造前架构
改造前(单库):
┌─────────────────────────────────────────────────────────────┐
│ 应用服务 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 订单服务 │ │ 商品服务 │ │ 用户服务 │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
└─────────┼────────────────┼────────────────┼─────────────────┘
│ │ │
└────────────────┴────────────────┘
│
┌──────▼──────┐
│ 主库 │
│ (MySQL) │
│ CPU: 100% │
└─────────────┘
问题:
- 所有读写都打到主库
- CPU使用率100%
- 查询响应慢
- 订单写入超时
改造后架构
改造后(1主3从):
┌─────────────────────────────────────────────────────────────┐
│ 应用服务 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 订单服务 │ │ 商品服务 │ │ 用户服务 │ │
│ │ 写: 主库 │ │ 读: 从库 │ │ 读: 从库 │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
└─────────┼────────────────┼────────────────┼─────────────────┘
│ │ │
│ ┌──────┴──────┐ │
│ │ 负载均衡 │ │
│ │ (轮询/权重) │ │
│ └──────┬──────┘ │
│ │ │
┌─────▼─────┐ ┌──────▼──────┐ ┌─────▼─────┐
│ 主库 │ │ 从库1 │ │ 从库2 │
│ (写) │ │ (读) │ │ (读) │
│ CPU: 40% │ │ CPU: 60% │ │ CPU: 60% │
└───────────┘ └─────────────┘ └───────────┘
│
│ 复制
↓
┌───────────┐
│ 从库3 │
│ (备份) │
│ CPU: 20% │
└───────────┘
效果:
- 主库CPU降至40%
- 查询响应时间从500ms降至50ms
- 订单写入不再超时
- 系统容量提升3倍
核心代码实现
@Configuration
public class ReadWriteSplitConfig {
@Bean
public DataSource dataSource() {
// 主库
HikariConfig masterConfig = new HikariConfig();
masterConfig.setJdbcUrl("jdbc:mysql://master:3306/db");
masterConfig.setUsername("root");
masterConfig.setPassword("password");
masterConfig.setMaximumPoolSize(50);
HikariDataSource masterDataSource = new HikariDataSource(masterConfig);
// 从库列表
List<HikariDataSource> slaveDataSources = new ArrayList<>();
String[] slaveUrls = {
"jdbc:mysql://slave1:3306/db",
"jdbc:mysql://slave2:3306/db",
"jdbc:mysql://slave3:3306/db"
};
for (String url : slaveUrls) {
HikariConfig slaveConfig = new HikariConfig();
slaveConfig.setJdbcUrl(url);
slaveConfig.setUsername("root");
slaveConfig.setPassword("password");
slaveConfig.setMaximumPoolSize(30);
slaveDataSources.add(new HikariDataSource(slaveConfig));
}
return new ReadWriteSplittingDataSource(masterDataSource, slaveDataSources);
}
}
public class ReadWriteSplittingDataSource extends AbstractDataSource {
private final DataSource masterDataSource;
private final List<DataSource> slaveDataSources;
private final AtomicInteger counter = new AtomicInteger(0);
public ReadWriteSplittingDataSource(DataSource master, List<DataSource> slaves) {
this.masterDataSource = master;
this.slaveDataSources = slaves;
}
@Override
public Connection getConnection() throws SQLException {
// 根据上下文选择数据源
if (isWriteOperation()) {
return masterDataSource.getConnection();
} else {
return getSlaveDataSource().getConnection();
}
}
private DataSource getSlaveDataSource() {
// 轮询选择从库
int index = counter.getAndIncrement() % slaveDataSources.size();
return slaveDataSources.get(index);
}
private boolean isWriteOperation() {
// 检查当前是否在事务中
return TransactionSynchronizationManager.isActualTransactionActive();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return getConnection();
}
}
@Service
public class OrderReadWriteService {
@Autowired
private OrderMapper orderMapper;
/**
* 创建订单 - 自动走主库(因为有@Transactional)
*/
@Transactional
public Order createOrder(OrderRequest request) {
Order order = new Order(request);
orderMapper.insert(order);
// 标记为新创建订单(用于后续读操作判断)
OrderContext.markAsNewOrder(order.getOrderId());
return order;
}
/**
* 查询订单 - 智能路由
*/
public Order getOrder(Long orderId) {
// 如果是刚创建的订单,走主库
if (OrderContext.isNewOrder(orderId)) {
return getOrderFromMaster(orderId);
}
// 否则走从库
return orderMapper.selectById(orderId);
}
/**
* 强制走主库查询
*/
public Order getOrderFromMaster(Long orderId) {
DataSourceContextHolder.setDataSource("master");
try {
return orderMapper.selectById(orderId);
} finally {
DataSourceContextHolder.clear();
}
}
/**
* 列表查询 - 走从库
*/
public List<Order> listOrders(Long userId, int page, int size) {
// 列表查询对实时性要求不高,走从库
return orderMapper.selectByUserId(userId, (page - 1) * size, size);
}
}
性能测试数据
1. 读写分离前后性能对比
| 指标 | 改造前(单库) | 改造后(1主3从) | 提升 |
|---|---|---|---|
| 主库CPU | 100% | 35% | 65% |
| 查询QPS | 2,000 | 8,000 | 4x |
| 写入TPS | 500 | 1,200 | 2.4x |
| 平均查询延迟 | 500ms | 50ms | 10x |
| 连接池使用率 | 95% | 40% | 55% |
| 系统可用性 | 99% | 99.9% | 0.9% |
2. 主从延迟统计
| 场景 | 平均延迟 | P99延迟 | 最大延迟 |
|---|---|---|---|
| 正常写入 | 10ms | 50ms | 200ms |
| 大批量写入 | 500ms | 2s | 5s |
| DDL操作 | 2s | 10s | 30s |
| 网络抖动 | 100ms | 500ms | 2s |
3. 故障恢复时间
| 故障类型 | 检测时间 | 切换时间 | 总恢复时间 |
|---|---|---|---|
| 从库故障 | 5s | 1s | 6s |
| 主库故障 | 3s | 10s | 13s |
| 网络分区 | 10s | 5s | 15s |
经验总结
✅ 最佳实践
-
写主读从,关键读走主
- 写操作必须走主库
- 普通查询走从库
- 刚写入的数据查询走主库
-
监控主从延迟
- 实时监控Seconds_Behind_Master
- 延迟过大告警
- 延迟过大时自动切换读操作到主库
-
从库故障自动切换
- 健康检查机制
- 故障从库自动剔除
- 恢复后自动加入
-
延迟过大告警
- 延迟超过1秒告警
- 延迟超过5秒切换读操作到主库
- 延迟超过10秒排查原因
-
连接池分离
- 主库连接池和从库连接池分离
- 避免从库问题影响主库
- 不同业务使用不同连接池
❌ 常见错误
-
所有读都走从库
// 错误:刚创建的订单走从库,可能查不到 public Order getOrder(Long orderId) { return orderMapper.selectById(orderId); // 默认走从库 } // 正确:新订单走主库 public Order getOrder(Long orderId) { if (isNewOrder(orderId)) { return getOrderFromMaster(orderId); } return orderMapper.selectById(orderId); } -
不处理主从延迟
// 错误:不处理延迟 @Transactional public void createAndGetOrder(OrderRequest request) { Order order = createOrder(request); // 写入主库 return getOrder(order.getOrderId()); // 从库可能查不到 } // 正确:强制走主库 @Transactional public void createAndGetOrder(OrderRequest request) { Order order = createOrder(request); return getOrderFromMaster(order.getOrderId()); } -
从库故障无感知
// 错误:从库故障不处理 public List<Product> listProducts() { return productMapper.selectList(); // 从库故障时查询失败 } // 正确:故障转移 public List<Product> listProducts() { try { return productMapper.selectList(); } catch (Exception e) { // 切换到主库 return getProductsFromMaster(); } } -
复制中断不处理
# 错误:不监控复制状态 # 从库复制中断数天未被发现 # 正确:监控告警 # 1. 监控Slave_IO_Running和Slave_SQL_Running # 2. 复制中断立即告警 # 3. 自动尝试恢复
决策树:读写分离策略选择
┌─────────────────────────────────────┐
│ 业务场景分析 │
└─────────────────┬───────────────────┘
│
┌─────────────────────────┼─────────────────────────┐
↓ ↓ ↓
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ 写操作 │ │ 读操作 │ │ 混合操作 │
│ INSERT/ │ │ SELECT │ │ 事务内读写 │
│ UPDATE/ │ │ │ │ │
│ DELETE │ │ │ │ │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
↓ ↓ ↓
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ 主库 │ │ 从库 │ │ 主库 │
│ 强制 │ │ 轮询/权重 │ │ 强制 │
└───────────────┘ └───────┬───────┘ └───────────────┘
│
↓
┌───────────────┐
│ 延迟检查 │
│ 延迟大→主库 │
│ 延迟小→从库 │
└───────────────┘
检查清单
读写分离部署检查清单
- 主从复制是否配置正确?
- 复制模式是否选择合适?
- 从库是否启用只读模式?
- 读写分离规则是否配置正确?
- 延迟监控是否开启?
- 故障转移是否测试?
- 连接池是否分离配置?
- 关键读是否走主库?
- 延迟处理策略是否完善?
- 监控告警是否配置?
主从复制运维检查清单
- Slave_IO_Running是否为Yes?
- Slave_SQL_Running是否为Yes?
- Seconds_Behind_Master是否小于1秒?
- 主从数据是否一致?
- binlog是否定期清理?
- 复制是否有错误?
- 从库延迟是否可接受?
- 故障转移是否可用?
- 备份是否正常?
- 监控告警是否正常?
系列上一篇:高并发场景下的数据库应对策略
系列下一篇:分库分表设计与实践
知识点测试
读完文章了?来测试一下你对知识点的掌握程度吧!
评论区
使用 GitHub 账号登录后即可发表评论,支持 Markdown 格式。
如果评论系统无法加载,请确保:
- 您的网络可以访问 GitHub
- giscus GitHub App 已安装到仓库
- 仓库已启用 Discussions 功能