做Web开发,数据库是核心。随着网站流量增长,单台MySQL服务器很快就会成为瓶颈——读请求太多,数据库响应变慢;写操作锁定表,影响读性能;数据库挂了,整个网站就瘫了。

MySQL主从复制(Master-Slave Replication)是解决这些问题的最常用方案。它的原理是:主库(Master)负责写操作(INSERT、UPDATE、DELETE),从库(Slave)复制主库的数据,负责读操作(SELECT)。这样有几个好处:

第一,读写分离。读请求分发到从库,写请求留在主库,减轻主库的压力,提升数据库的并发能力。一般来说,Web应用的读操作远多于写操作(比例大约8:2甚至9:1),把读操作分散到多台从库,能大幅提升整体性能。

第二,数据备份。从库实时复制主库的数据,相当于一个实时的备份。主库的数据损坏或丢失,可以从从库恢复。而且可以在从库上做备份,不影响主库的性能。

第三,高可用。主库故障时,可以把一台从库提升为主库,继续提供服务,减少停机时间。配合负载均衡和自动故障转移,可以实现数据库的高可用。

第四,数据分析。可以在从库上做复杂的查询和数据分析,不影响主库的在线业务。

主从复制是MySQL最经典、最成熟的高可用方案,几乎所有中大型网站都在用。今天就来分享MySQL主从复制的原理和配置实战。

主从复制的原理

MySQL主从复制的原理是基于二进制日志(Binary Log,简称binlog)。主库把数据变更操作记录到binlog中,从库读取主库的binlog,然后在自己身上重放这些操作,从而实现数据同步。

具体过程分为三步:

  1. 主库记录binlog:主库在执行完数据变更操作(INSERT、UPDATE、DELETE、CREATE等)后,把这些操作记录到binlog文件中。binlog有三种格式:STATEMENT(记录SQL语句)、ROW(记录行的变化)、MIXED(混合模式,默认用STATEMENT,特殊情况自动切换到ROW)。
  1. 从库IO线程读取binlog:从库启动一个IO线程,连接到主库,请求读取binlog。主库启动一个binlog dump线程,把binlog的内容发送给从库。从库IO线程把接收到的binlog写入到自己的中继日志(Relay Log)中。
  1. 从库SQL线程重放中继日志:从库启动一个SQL线程,读取中继日志中的内容,在从库上重放这些操作,从而实现数据同步。

这个过程是异步的——主库不需要等待从库复制完成,就可以继续处理请求。所以主从复制有一定的延迟(通常在毫秒到秒级),从库的数据不是实时的,而是"准实时"的。对于大多数应用来说,这个延迟可以接受;但对于要求强一致性的场景(如支付、库存),读操作应该走主库。

配置前的准备

在配置主从复制之前,需要做一些准备工作:

  1. 主库和从库的MySQL版本:建议主从版本一致,或者从库版本比主库高(从库可以兼容低版本主库的binlog,但反过来不行)。本文以MySQL 5.6为例。
  1. 服务器之间网络互通:主库和从库之间要能互相访问,主库的3306端口要对从库开放。
  1. 主库开启binlog:主库必须开启binlog,并且设置server-id。
  1. 数据一致性:配置主从之前,主库和从库的数据应该一致。如果主库已经有数据,需要先把主库的数据导出,导入到从库,然后再配置复制。

主库配置

首先配置主库(Master)。编辑主库的MySQL配置文件my.cnf(Linux下通常在/etc/my.cnf或/etc/mysql/my.cnf):

[mysqld]
# 服务器ID,主从必须不同,主库设为1
server-id = 1

# 开启binlog
log-bin = mysql-bin

# binlog格式,推荐ROW或MIXED
binlog_format = mixed

# 同步的数据库(如果不设置,默认同步所有数据库)
# binlog-do-db = mydb

# 不同步的数据库
binlog-ignore-db = mysql
binlog-ignore-db = information_schema
binlog-ignore-db = performance_schema

# 中继日志(从库需要,主库也可以设置)
relay-log = relay-bin

# 字符集
character-set-server = utf8mb4

配置完成后,重启MySQL:

service mysql restart

然后在主库上创建一个用于复制的用户,从库用这个用户连接主库:

-- 创建复制用户
CREATE USER 'repl'@'%' IDENTIFIED BY 'repl_password';

-- 授予复制权限
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';

-- 刷新权限
FLUSH PRIVILEGES;

接下来,锁表并导出主库数据(如果主库已经有数据):

-- 锁表,防止数据变更
FLUSH TABLES WITH READ LOCK;

-- 查看主库状态,记录File和Position的值
SHOW MASTER STATUS;

记录下SHOW MASTER STATUS的输出,比如:

+------------------+----------+--------------+------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000001 |      120 |              | mysql            |
+------------------+----------+--------------+------------------+

File是mysql-bin.000001,Position是120,这两个值后面配置从库时要用。

然后在另一个终端导出主库数据:

mysqldump -u root -p --all-databases --master-data > all_db.sql

导出完成后,解锁表:

UNLOCK TABLES;

从库配置

接下来配置从库(Slave)。编辑从库的MySQL配置文件:

[mysqld]
# 服务器ID,必须和主库不同,设为2
server-id = 2

# 从库也可以开启binlog(用于级联复制或备份)
log-bin = mysql-bin

# 中继日志
relay-log = relay-bin

# 只读模式(从库设为只读,防止误写)
read-only = 1

# 字符集
character-set-server = utf8mb4

注意:read-only参数只对普通用户生效,root用户和有SUPER权限的用户仍然可以写。如果要完全禁止写,可以用super-read-only(MySQL 5.7+支持)。

配置完成后,重启MySQL:

service mysql restart

然后把主库导出的数据导入到从库:

mysql -u root -p < all_db.sql

导入完成后,在从库上配置复制,连接主库:

CHANGE MASTER TO
  MASTER_HOST='主库IP地址',
  MASTER_USER='repl',
  MASTER_PASSWORD='repl_password',
  MASTER_LOG_FILE='mysql-bin.000001',
  MASTER_LOG_POS=120;

MASTERLOGFILE和MASTERLOGPOS就是之前在主库上SHOW MASTER STATUS得到的值。

然后启动从库复制:

START SLAVE;

查看从库复制状态:

SHOW SLAVE STATUS\G

重点看这两个值:

  • SlaveIORunning: Yes
  • SlaveSQLRunning: Yes

如果两个都是Yes,说明复制正常运行。如果有一个是No,说明复制出错了,需要查看LastIOError或LastSQLError的错误信息,排查问题。

验证主从复制

配置完成后,验证一下主从复制是否正常工作。

在主库上创建一个测试表,插入一条数据:

CREATE DATABASE test_repl;
USE test_repl;
CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50));
INSERT INTO users (name) VALUES ('test1');

然后在从库上查询:

USE test_repl;
SELECT * FROM users;

如果能查到刚才插入的数据,说明主从复制正常工作。

再测试更新和删除:

-- 主库
UPDATE users SET name = 'test2' WHERE id = 1;
DELETE FROM users WHERE id = 1;

从库上应该能同步看到更新和删除的结果。

读写分离配置

主从复制配置好之后,就可以实现读写分离了——写操作走主库,读操作走从库。

读写分离有几种实现方式:

  1. 应用层实现:在代码中判断SQL类型,SELECT走从库,INSERT/UPDATE/DELETE走主库。可以用数据库中间件(如MyCat、Atlas),或者在代码中用多个数据源。
  1. 代理层实现:用MySQL代理(如ProxySQL、MaxScale),自动解析SQL,路由到对应的数据库。应用只需要连接代理,不需要关心读写分离。
  1. 负载均衡器实现:用HAProxy或LVS做负载均衡,读请求分发到从库,写请求发到主库。但这种方式不能解析SQL,需要配置端口或规则区分。

对于PHP应用,最简单的方式是在代码中用两个数据库连接——一个主库连接(用于写),一个从库连接(用于读)。示例代码:

<?php
// 主库连接(写操作)
$master = new PDO('mysql:host=主库IP;dbname=mydb;charset=utf8mb4', 'user', 'pass');

// 从库连接(读操作)
$slave = new PDO('mysql:host=从库IP;dbname=mydb;charset=utf8mb4', 'user', 'pass');

// 写操作走主库
$stmt = $master->prepare("INSERT INTO users (name) VALUES (?)");
$stmt->execute(['test']);

// 读操作走从库
$stmt = $slave->query("SELECT * FROM users");
$users = $stmt->fetchAll();

如果有多个从库,可以在从库之间做负载均衡,把读请求分散到不同的从库。

需要注意的是,对于要求强一致性的读操作(如刚写完就查询、支付查询、库存查询),应该走主库,因为主从复制有延迟,从库可能还没同步到最新数据。

常见问题

第一,SlaveIORunning: No。IO线程没启动,通常是因为:主库连接信息错误(IP、端口、用户名、密码)、主库的binlog文件或位置错误、网络不通、主库的防火墙没开放3306端口、复制用户权限不对。排查方法:检查CHANGE MASTER TO的参数是否正确、测试从库能否连接主库(mysql -h主库IP -u repl -p)、查看LastIOError的错误信息。

第二,SlaveSQLRunning: No。SQL线程出错,通常是因为:主从数据不一致(从库上有主库没有的数据或表结构)、SQL语句在从库执行失败(如主键冲突、表不存在)、binlog格式问题。排查方法:查看LastSQLError的错误信息、根据错误信息修复从库数据、然后用SET GLOBAL SQLSLAVESKIP_COUNTER = 1跳过错误事务,再START SLAVE。如果错误很多,建议重新配置主从(导出主库数据,重新导入从库)。

第三,主从延迟大。从库同步慢,延迟高。原因可能是:从库性能差(CPU、内存、磁盘IO不足)、网络带宽不够、主库写操作太多、从库上有慢查询占用资源、单线程复制(MySQL 5.6之前从库是单线程复制,5.6+支持多线程复制)。优化方法:提升从库硬件性能、优化网络、在从库上优化慢查询、开启多线程复制(MySQL 5.6+,设置slaveparallelworkers > 0)。

第四,主库故障切换。主库挂了,需要把从库提升为主库。步骤:停止从库复制(STOP SLAVE)、重置从库(RESET MASTER)、修改应用配置连接新主库、其他从库重新指向新主库(CHANGE MASTER TO)。这个过程可以手动操作,也可以用工具(如MHA、Orchestrator)实现自动故障转移。

总结

MySQL主从复制是最常用、最成熟的数据库高可用和性能优化方案。它基于binlog实现数据同步,配置简单,稳定可靠。通过主从复制,可以实现读写分离、数据备份、高可用、数据分析等功能。

如果你的网站流量增长,单台MySQL扛不住了,不妨试试主从复制。它能帮你提升数据库性能,保障数据安全,实现高可用。