MySQL主从复制


主从复制原理

MySQL利用binlog实现主从复制,大致流程如下:

  1. 主节点每一次执行SQL都会写binlog日志
  2. 从节点IO线程与主节点建立起长连接,不断的从主节点读取binlog
  3. IO线程将读到的binlog写到replay log中
  4. SQL线程读取replay log并执行

示意图如下:

配置主从复制

主节点设置

1.主节点开启binlog日志

修改配置文件my.cnf:

[mysqld]
log-bin=mysql-bin
server-id=1

为了方便搭建环境,我使用docker演示:

docker run -d \
	-p 3306:3306 \
 	-e MYSQL_ROOT_PASSWORD=123456 \
	--name=master-mysql \
  	mysql:5.6 \
  	--log-bin=mysql-bin \
  	--server-id=1 

在命令末尾使用--加参数,直接传入要修改的配置,而不用修改配置文件。参数列表可以使用以下命令查看:

docker run -it --rm mysql:5.6 --verbose --help

2.创建一个专门用于复制的账号

进入mysql容器并执行:

docker exec -it master-mysql bash
mysql -uroot -p123456
mysql> GRANT REPLICATION SLAVE ON *.*  TO  'repl'@'%'  identified by '123456';

迁移已有数据

我们需要将主库已有数据导出并迁移到从库中。

为了演示,先在主库中创建数据库,建表,并插入一条数据:

mysql> create database mydb;
mysql> use mydb;
mysql> create table t(id int, name varchar(32));
mysql> insert into t values(1, 'zhangsan');
mysql> select * from t;
+------+----------+
| id   | name     |
+------+----------+
|    1 | zhangsan |
+------+----------+

导出数据:

mysqldump -uroot -p123456 --all-databases --master-data=1 > dump.db

将dump.db文件拷贝到从节点

查询主节点当前binlog及位置点:

mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000004 |      743 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+

记住binlog文件名mysql-bin.000004以及position的值743,从节点复制数据时需要从这个位置点开始复制。

从节点设置

从节点配置my.cnf:

[mysqld]
server-id=2

此处server-id一定要和主节点不一样。

为了方便演示,使用docker启动从节点,并且端口设为3307:

docker run -d \
	--name=slave-mysql \
	-p 3307:3306 \
 	-e MYSQL_ROOT_PASSWORD=123456 \
  	mysql:5.6 \
  	--server-id=2 

导入从主节点搬来的dump.db文件。我的演示环境里,两个mysql部署在同一台机器上,dump.db文件要先从主mysql容器中搬出来,再拷贝到主mysql容器中:

docker cp master-mysql:/dump.db .
docker cp ./dump.db slave-mysql:/

导入数据:

mysql -uroot -p123456
mysql> source /dump.db;

配置master并开始复制:

mysql> change master to master_host='192.168.0.100',master_user='repl',master_password='123456',master_log_file='mysql-bin.000004',master_log_pos=743;
mysql> start slave;

注意:命令中的参数要根据你的实际情况修改。

查询主从复制情况:

mysql> show slave status \G;
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 192.168.0.100
                  Master_User: repl
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000004
          Read_Master_Log_Pos: 743
               Relay_Log_File: mysqld-relay-bin.000002
                Relay_Log_Pos: 283
        Relay_Master_Log_File: mysql-bin.000004
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 0
                   Last_Error: 
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 743
              Relay_Log_Space: 457
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File: 
           Master_SSL_CA_Path: 
              Master_SSL_Cert: 
            Master_SSL_Cipher: 
               Master_SSL_Key: 
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 0
               Last_SQL_Error: 
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 1
                  Master_UUID: 96d15f65-9477-11eb-b5a6-0242ac110002
             Master_Info_File: /var/lib/mysql/master.info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Slave has read all relay log; waiting for the slave I/O thread to update it
           Master_Retry_Count: 86400
                  Master_Bind: 
      Last_IO_Error_Timestamp: 
     Last_SQL_Error_Timestamp: 
               Master_SSL_Crl: 
           Master_SSL_Crlpath: 
           Retrieved_Gtid_Set: 
            Executed_Gtid_Set: 
                Auto_Position: 0

从查询结果里可以看到Slave_IO_Running和Slave_SQL_Running的值都是Yes,说明主从复制成功了。可以在主库修改数据,在从库验证。

Binlog格式

binlog有statement,raw两种格式。还有一种mixed,mixed是前两种格式的混合

binlog格式使用binlog-format配置项设置

statement格式

statement格式的binlog,就是直接记录了执行的SQL语句,这种方式有极大的风险,同一条SQL,在主库执行的结果与在从库执行的结果未必一致。

我们可以使用mysqlbinlog工具查看binlog文件的内容。

基于上面配置的数据库,我们进行个小实验。

首先使用show master logs查看当前binlog位置:

mysql> show master logs;
+------------------+-----------+
| Log_name         | File_size |
+------------------+-----------+
| mysql-bin.000001 |     69432 |
| mysql-bin.000002 |   1640526 |
| mysql-bin.000003 |   7021288 |
| mysql-bin.000004 |      1584 |
+------------------+-----------+

记住最后一行:mysql-bin.000004以及1584

利用上面例子中建立的mydb库以及t表,先新增一条记录:

mysql> insert into t values(100, 'abc');
Query OK, 1 row affected (0.01 sec)

再将其删除:

mysql> delete from t where id=100;
Query OK, 1 row affected (0.01 sec)

使用mysqlbinlog工具查看binlog文件,默认目录在:/var/lib/mysql:

cd /var/lib/mysql
mysqlbinlog  mysql-bin.000004 --start-position=1584

参数解析:

  • –start-position:从binlog指定的位置开始,设置为上面查出来的值1584

从执行结果可以清楚看到刚刚执行的SQL语句:

除了SQL外,还能看到binlog中有SET TIMESTAMP=1618024381/*!*/;,这是为了记录执行这条SQL的时间。

有什么用呢?比如SQL中使用到了NOW()函数,那么即使主从延迟了,从库也会使用binlog中的TIMESTAMP作为当前时间来执行SQL,避免主从数据不一致。

但可能导致主从不一致的情况不仅这一种,比如同一条SQL,在主库和从库执行时,优化器选择了不同的索引,那么就可能导致执行结果不一致。

因此statement格式的binlog是不靠谱的

row格式

row格式记录的是被更新的数据:

  • 如果是insert操作,则直接记录插入的数据
  • 如果是update操作,记录更新前和更新后的数据
  • 如果是delete操作,记录被删除的数据

为了演示,先将binlog临时修改为row:

mysql> set session binlog_format=row;
Query OK, 0 rows affected (0.01 sec)

mysql> show variables like 'binlog_format';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| binlog_format | ROW   |
+---------------+-------+

分别写入insert,update,delete三条语句:

mysql> insert into t values(10, 'javajun.net');
Query OK, 1 row affected (0.01 sec)

mysql> update t set name='www.javajun.net' where id=10;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> delete from t where name='www.javajun.net';
Query OK, 1 row affected (0.00 sec)

注意,delete语句时根据name来删除

首先看insert语句执行的binlog,这次需要加上-vv参数,可以看到很多细节

mysqlbinlog -vv  mysql-bin.000004

执行结果:

上图中,Table_map说明了当前要操作的库表,第二个红框中@1和@2分别指定了表的第一个字段和第二个字段是什么值

再看update语句的结果:

可以看到,更新前和更新后的数据都被记录下来了

delete语句的执行结果:

delete语句的binlog记录了要删除的数据,而不是根据你的where条件来删除,也保证了主从的一致性。

mixed格式

row格式能够保证主从数据的一致,但是生成的binlog比较占空间。比如一条批量更新的SQL,如果是statement格式,只要记一条SQL就好了,row格式则需要记录所有被更新的数据。

所以后来有了mixed格式,mysql会自动判断SQL是否可能产生主从不一致的情况,如果会,则用row记录,不会则用statement记录。

推荐使用row或者mixed

文章作者: 周君
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 周君 !
评论