返回 筑基・数据元府藏真

主从复制与读写分离实战

博主
大约 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从)提升
主库CPU100%35%65%
查询QPS2,0008,0004x
写入TPS5001,2002.4x
平均查询延迟500ms50ms10x
连接池使用率95%40%55%
系统可用性99%99.9%0.9%

2. 主从延迟统计

场景平均延迟P99延迟最大延迟
正常写入10ms50ms200ms
大批量写入500ms2s5s
DDL操作2s10s30s
网络抖动100ms500ms2s

3. 故障恢复时间

故障类型检测时间切换时间总恢复时间
从库故障5s1s6s
主库故障3s10s13s
网络分区10s5s15s

经验总结

✅ 最佳实践

  1. 写主读从,关键读走主

    • 写操作必须走主库
    • 普通查询走从库
    • 刚写入的数据查询走主库
  2. 监控主从延迟

    • 实时监控Seconds_Behind_Master
    • 延迟过大告警
    • 延迟过大时自动切换读操作到主库
  3. 从库故障自动切换

    • 健康检查机制
    • 故障从库自动剔除
    • 恢复后自动加入
  4. 延迟过大告警

    • 延迟超过1秒告警
    • 延迟超过5秒切换读操作到主库
    • 延迟超过10秒排查原因
  5. 连接池分离

    • 主库连接池和从库连接池分离
    • 避免从库问题影响主库
    • 不同业务使用不同连接池

❌ 常见错误

  1. 所有读都走从库

    // 错误:刚创建的订单走从库,可能查不到
    public Order getOrder(Long orderId) {
        return orderMapper.selectById(orderId);  // 默认走从库
    }
    
    // 正确:新订单走主库
    public Order getOrder(Long orderId) {
        if (isNewOrder(orderId)) {
            return getOrderFromMaster(orderId);
        }
        return orderMapper.selectById(orderId);
    }
    
  2. 不处理主从延迟

    // 错误:不处理延迟
    @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());
    }
    
  3. 从库故障无感知

    // 错误:从库故障不处理
    public List<Product> listProducts() {
        return productMapper.selectList();  // 从库故障时查询失败
    }
    
    // 正确:故障转移
    public List<Product> listProducts() {
        try {
            return productMapper.selectList();
        } catch (Exception e) {
            // 切换到主库
            return getProductsFromMaster();
        }
    }
    
  4. 复制中断不处理

    # 错误:不监控复制状态
    # 从库复制中断数天未被发现
    
    # 正确:监控告警
    # 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 功能