MySQL主从复制与读写分离实战

MySQL主从复制与读写分离实战

一、主从复制原理

MySQL主从复制是实现高可用性和读写分离的基础,通过将主库的数据同步到从库,实现数据备份和负载均衡。

1.1 复制架构

复制代码
┌─────────────────┐      二进制日志      ┌─────────────────┐
│    Master       │ ──────────────────> │    Slave 1      │
│  (写操作)       │                     │   (读操作)       │
└─────────────────┘                     └─────────────────┘
       │
       │                    ┌─────────────────┐
       └──────────────────> │    Slave 2      │
                            │   (读操作)       │
                            └─────────────────┘

1.2 复制流程

  1. Master:将数据变更写入二进制日志(binlog)
  2. Slave:I/O线程读取Master的binlog,写入中继日志(relay log)
  3. Slave:SQL线程执行中继日志中的SQL语句

二、主从复制配置

2.1 Master 配置

ini 复制代码
[mysqld]
server-id = 1
log_bin = /var/log/mysql/mysql-bin.log
binlog_format = ROW
binlog_do_db = example_db
binlog_ignore_db = mysql
expire_logs_days = 7
max_binlog_size = 500M

2.2 创建复制用户

sql 复制代码
CREATE USER 'repl'@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;

2.3 获取Master状态

sql 复制代码
FLUSH TABLES WITH READ LOCK;
SHOW MASTER STATUS;

记录File和Position的值:

复制代码
+------------------+----------+--------------+------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000001 | 154      | example_db   | mysql            |
+------------------+----------+--------------+------------------+

2.4 Slave 配置

ini 复制代码
[mysqld]
server-id = 2
relay_log = /var/log/mysql/relay-bin.log
log_slave_updates = 1
read_only = 1

2.5 启动复制

sql 复制代码
CHANGE MASTER TO
  MASTER_HOST='master_ip',
  MASTER_USER='repl',
  MASTER_PASSWORD='password',
  MASTER_LOG_FILE='mysql-bin.000001',
  MASTER_LOG_POS=154;

START SLAVE;

2.6 检查复制状态

sql 复制代码
SHOW SLAVE STATUS\G

关键状态字段:

  • Slave_IO_Running: Yes
  • Slave_SQL_Running: Yes
  • Seconds_Behind_Master: 0

三、读写分离实现

3.1 使用 MySQL Proxy

lua 复制代码
function read_query(packet)
    if packet:byte() == proxy.COM_QUERY then
        local query = packet:sub(2)
        
        if string.upper(query):match('^SELECT') then
            proxy.queries:append(1, packet, {resultset_is_needed = true})
            return proxy.PROXY_SEND_QUERY
        else
            proxy.backends[1].state = proxy.BACKEND_STATE_CURRENT
            proxy.queries:append(1, packet, {resultset_is_needed = true})
            return proxy.PROXY_SEND_QUERY
        end
    end
end

function read_result(inj)
    return proxy.PROXY_SEND_RESULT
end

3.2 使用 Spring JDBC 路由

java 复制代码
public class RoutingDataSource extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceType();
    }
}

public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    
    public static void setDataSourceType(String type) {
        contextHolder.set(type);
    }
    
    public static String getDataSourceType() {
        return contextHolder.get();
    }
    
    public static void clearDataSourceType() {
        contextHolder.remove();
    }
}

3.3 AOP 切面配置

java 复制代码
@Aspect
@Component
public class DataSourceAspect {
    
    @Pointcut("execution(* com.example.service.*.select*(..))")
    public void readPointcut() {}
    
    @Pointcut("execution(* com.example.service.*.insert*(..)) || " +
              "execution(* com.example.service.*.update*(..)) || " +
              "execution(* com.example.service.*.delete*(..))")
    public void writePointcut() {}
    
    @Before("readPointcut()")
    public void setReadDataSource() {
        DataSourceContextHolder.setDataSourceType("slave");
    }
    
    @Before("writePointcut()")
    public void setWriteDataSource() {
        DataSourceContextHolder.setDataSourceType("master");
    }
    
    @After("readPointcut() || writePointcut()")
    public void clearDataSource() {
        DataSourceContextHolder.clearDataSourceType();
    }
}

3.4 配置文件

yaml 复制代码
spring:
  datasource:
    master:
      url: jdbc:mysql://master:3306/example_db
      username: admin
      password: password
      driver-class-name: com.mysql.cj.jdbc.Driver
    slave:
      url: jdbc:mysql://slave:3306/example_db
      username: readonly
      password: password
      driver-class-name: com.mysql.cj.jdbc.Driver

四、主从复制优化

4.1 并行复制

ini 复制代码
[mysqld]
slave-parallel-type = LOGICAL_CLOCK
slave-parallel-workers = 4
master_info_repository = TABLE
relay_log_info_repository = TABLE

4.2 半同步复制

Master 配置:

ini 复制代码
[mysqld]
plugin-load-add = semisync_master.so
loose-semi_sync_master_enabled = 1
loose-semi_sync_master_timeout = 1000

Slave 配置:

ini 复制代码
[mysqld]
plugin-load-add = semisync_slave.so
loose-semi_sync_slave_enabled = 1

4.3 GTID 复制

Master 配置:

ini 复制代码
[mysqld]
gtid_mode = ON
enforce_gtid_consistency = ON

Slave 配置:

sql 复制代码
CHANGE MASTER TO
  MASTER_HOST='master_ip',
  MASTER_USER='repl',
  MASTER_PASSWORD='password',
  MASTER_AUTO_POSITION = 1;

五、故障处理

5.1 延迟问题排查

sql 复制代码
-- 查看延迟情况
SHOW SLAVE STATUS\G

-- 查看复制线程状态
SELECT * FROM performance_schema.replication_applier_status\G

-- 分析慢查询
EXPLAIN ANALYZE SELECT * FROM large_table WHERE condition;

5.2 主从数据不一致

sql 复制代码
-- 检查数据一致性
CHECKSUM TABLE table_name;

-- 使用 pt-table-checksum 检查
pt-table-checksum h=master,D=example_db

-- 使用 pt-table-sync 修复
pt-table-sync --execute h=master,D=example_db h=slave

5.3 主库故障切换

sql 复制代码
-- 在从库上提升为主库
STOP SLAVE;
RESET MASTER;

-- 修改其他从库指向新主库
CHANGE MASTER TO
  MASTER_HOST='new_master_ip',
  MASTER_AUTO_POSITION = 1;

六、监控与告警

6.1 Prometheus 监控

yaml 复制代码
scrape_configs:
  - job_name: 'mysql'
    static_configs:
      - targets: ['master:9104', 'slave1:9104', 'slave2:9104']

6.2 告警规则

yaml 复制代码
groups:
  - name: mysql_replication_alerts
    rules:
      - alert: ReplicationLag
        expr: mysql_slave_status_seconds_behind_master > 60
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "复制延迟超过60秒"
          description: "从库 {{ $labels.instance }} 延迟: {{ $value }}秒"
      
      - alert: ReplicationStopped
        expr: mysql_slave_status_slave_io_running == 0 or mysql_slave_status_slave_sql_running == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "复制停止"
          description: "从库 {{ $labels.instance }} 复制线程停止"

七、最佳实践总结

7.1 架构建议

场景 推荐架构 说明
读多写少 1主N从 N建议3-5个
高可用 MHA/PXC 自动故障转移
大数据量 分库分表 + 主从 结合ShardingSphere

7.2 配置建议

  • binlog_format: 使用 ROW 格式,保证数据一致性
  • expire_logs_days: 根据业务需求设置,建议7-30天
  • read_only: 从库设置为只读,防止误写
  • log_slave_updates: 级联复制时开启

7.3 监控指标

  • Seconds_Behind_Master: 复制延迟
  • Slave_IO_Running/Slave_SQL_Running: 复制线程状态
  • binlog_size: 二进制日志大小
  • Threads_connected: 连接数

通过合理配置主从复制和读写分离,可以显著提升MySQL的可用性和性能。

相关推荐
ch.ju4 小时前
Java Programming Chapter 4——Composition of objects
java·开发语言
灰乌鸦乌卡4 小时前
关于OA自定义接口不能解析汉字记录
java·中间件
无聊的老谢4 小时前
编译期即正义:利用 Java Lambda 构建类型安全的 SQL 表达式引擎
java·开发语言
疯狂成瘾者4 小时前
Elasticsearch 是什么?它和普通数据库查询有什么区别?
java
运维行者_4 小时前
ITOps自动化:全面解析
java·服务器·开发语言·网络·云计算
Chase_______4 小时前
【Java杂项】为什么 b += 1 可以,但 b = b + 1 会报错?类型提升与复合赋值详解
java·开发语言·python
勿忘,瞬间4 小时前
Spring日志
java·spring boot·spring
AI人工智能+电脑小能手4 小时前
【大白话说Java面试题 第62题】【JVM篇】第22题:怎么查看服务器默认的垃圾回收器是哪一个?
java·服务器·jvm·面试
yqzyy4 小时前
C#如何优雅处理引用类型的深拷贝(十一)
java·网络·nginx