各位观众老爷,晚上好!我是你们的老朋友,今天咱们聊聊MySQL逻辑复制里头的那些事儿,特别是Binlog的解析以及Row-Based复制的性能问题。保证让大家听得懂,学得会,还能拿出去吹牛逼!
一、开场白:什么是Logical Replication?
啥是逻辑复制?简单来说,就是把数据库的变更(增删改)以逻辑的形式记录下来,然后应用到其他的数据库上。这跟物理复制(比如基于磁盘镜像)不一样,它更灵活,可以跨版本、跨平台。MySQL的逻辑复制主要依赖的就是Binlog(Binary Log)。
二、Binlog:一切故事的起点
Binlog,顾名思义,就是二进制日志,记录了数据库里所有的数据变更操作。要想搞明白逻辑复制,就得先搞懂Binlog。
-
Binlog的格式
Binlog有三种格式:
- Statement-Based Replication (SBR):记录SQL语句。
- Row-Based Replication (RBR):记录每一行数据的变化。
- Mixed-Based Replication (MBR):混合模式,MySQL自行决定使用SBR还是RBR。
可以通过
show variables like 'binlog_format';
查看当前的Binlog格式。mysql> show variables like 'binlog_format'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | binlog_format | ROW | +---------------+-------+ 1 row in set (0.00 sec)
-
Binlog的内容
Binlog里面记录了啥?举个例子:
# 创建表 CREATE TABLE `users` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `age` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; # 插入数据 INSERT INTO `users` (`name`, `age`) VALUES ('张三', 20); # 更新数据 UPDATE `users` SET `age` = 21 WHERE `id` = 1; # 删除数据 DELETE FROM `users` WHERE `id` = 1;
这些操作都会被记录到Binlog里。 具体记录的内容取决于Binlog的格式。
-
如何查看Binlog?
MySQL自带了一个工具:
mysqlbinlog
,可以用来查看Binlog的内容。mysqlbinlog mysql-bin.000001 # 查看第一个Binlog文件 mysqlbinlog --start-datetime="2023-10-26 00:00:00" --stop-datetime="2023-10-26 23:59:59" mysql-bin.000001 #查看指定时间段内的Binlog
不过直接用
mysqlbinlog
看,内容比较晦涩,不太直观。可以使用一些图形化的工具,比如canal
或者Maxwell
,它们可以把Binlog解析成更易读的格式。
三、Binlog解析:从二进制到可理解
Binlog是二进制的,要想利用它做逻辑复制,就得先把它解析成能理解的数据结构。这个过程就是Binlog解析。
-
Binlog解析的原理
Binlog解析器会读取Binlog文件,按照Binlog的格式(SBR、RBR或MBR)解析出其中的事件。每个事件都包含了数据库变更的信息,比如表名、操作类型(INSERT、UPDATE、DELETE)以及变更的数据。
-
常用的Binlog解析工具
- Canal:阿里巴巴开源的,专门用于MySQL Binlog解析。
- Maxwell:Java编写的,可以将Binlog数据转换成JSON格式。
- Debezium:一个分布式变更数据捕获平台,支持多种数据库。
这些工具都提供了API,方便开发者集成到自己的应用中。
-
一个简单的Canal Demo
这里简单演示一下Canal的使用(需要安装Canal Server):
(1) 配置Canal Client
import com.alibaba.otter.canal.client.CanalConnector; import com.alibaba.otter.canal.client.CanalConnectors; import com.alibaba.otter.canal.common.utils.AddressUtils; import com.alibaba.otter.canal.protocol.CanalEntry; import com.alibaba.otter.canal.protocol.Message; import java.net.InetSocketAddress; import java.util.List; public class SimpleCanalClient { public static void main(String[] args) { // 创建连接 CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(AddressUtils.getHostAddress(), 11111), "example", "", ""); try { connector.connect(); connector.subscribe(".*\..*"); // 订阅所有数据库的所有表 while (true) { Message message = connector.getWithoutAck(100); // 获取指定数量的数据 long batchId = message.getId(); int size = message.getEntries().size(); if (batchId == -1 || size == 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { // ignore } } else { printEntry(message.getEntries()); } connector.ack(batchId); // 提交确认 // connector.rollback(batchId); // 处理失败, 回滚数据 } } finally { connector.disconnect(); } } private static void printEntry(List<CanalEntry.Entry> entrys) { for (CanalEntry.Entry entry : entrys) { if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) { continue; } CanalEntry.RowChange rowChange; try { rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue()); } catch (Exception e) { throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(), e); } CanalEntry.EventType eventType = rowChange.getEventType(); System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s", entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(), entry.getHeader().getSchemaName(), entry.getHeader().getTableName(), eventType)); for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) { if (eventType == CanalEntry.EventType.DELETE) { printColumn(rowData.getBeforeColumnsList()); } else if (eventType == CanalEntry.EventType.INSERT) { printColumn(rowData.getAfterColumnsList()); } else { System.out.println("-------> before"); printColumn(rowData.getBeforeColumnsList()); System.out.println("-------> after"); printColumn(rowData.getAfterColumnsList()); } } } } private static void printColumn(List<CanalEntry.Column> columns) { for (CanalEntry.Column column : columns) { System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated()); } } }
(2) 运行上面的代码,就可以实时监听数据库的变更,并打印出来。
四、Row-Based Replication (RBR):性能的爱与恨
RBR,也就是基于行的复制,记录的是每一行数据的变化。相对SBR,它有优点也有缺点。
-
RBR的优点
- 更可靠:避免了SBR中SQL语句执行结果不一致的问题(比如使用了
NOW()
函数)。 - 适用性更广:对于复杂的存储过程、触发器等,RBR也能很好地支持。
- 减少锁竞争:在某些场景下,RBR可以减少锁的竞争,提高并发性能。
- 更可靠:避免了SBR中SQL语句执行结果不一致的问题(比如使用了
-
RBR的缺点
- Binlog体积更大:记录每一行数据的变化,Binlog体积会比SBR大很多。
- 网络传输压力更大:Binlog体积大,导致网络传输的压力也更大。
- 解析成本更高:解析RBR的Binlog,需要更多的CPU和内存资源。
-
RBR的性能优化
- 减少不必要的Binlog记录:可以通过
binlog_ignore_db
参数,忽略不需要复制的数据库。 - 优化网络传输:使用压缩算法,减少网络传输的数据量。
- 选择合适的Binlog解析工具:不同的解析工具,性能差异很大,需要根据实际情况选择。
- 调整MySQL参数:例如
binlog_cache_size
,max_binlog_size
等。
下面是一些关键参数的说明:
参数名 描述 默认值 建议 binlog_cache_size
用于在内存中缓存Binlog的事务大小,如果事务较大,需要增加这个值。 32K 根据实际情况调整,过大会占用过多内存,过小会频繁写入磁盘。 max_binlog_size
单个Binlog文件的最大大小,超过这个值会自动切换到下一个Binlog文件。 1G 根据磁盘空间和备份策略调整,过大会导致恢复时间变长,过小会导致Binlog文件过多。 sync_binlog
控制Binlog写入磁盘的频率, sync_binlog=1
表示每次事务都写入磁盘,sync_binlog=0
表示由操作系统决定。0 建议设置为1,保证数据安全,但会降低性能。如果对数据安全性要求不高,可以设置为0,但有丢失数据的风险。 binlog_row_image
控制RBR模式下,Binlog记录哪些行数据, FULL
表示记录所有列,MINIMAL
表示只记录修改的列。FULL 如果只需要记录修改的列,可以设置为 MINIMAL
,减少Binlog体积。binlog_transaction_dependency_tracking
用于控制事务依赖关系追踪,可以优化并行复制的性能。 COMMIT_ORDER 根据实际情况调整,可以提高并行复制的性能。 - 减少不必要的Binlog记录:可以通过
-
RBR应用场景案例分析
假设我们有一个电商平台的订单系统,需要将订单数据同步到数据仓库进行分析。订单表
orders
结构如下:CREATE TABLE `orders` ( `order_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID', `user_id` bigint(20) NOT NULL COMMENT '用户ID', `product_id` bigint(20) NOT NULL COMMENT '商品ID', `order_time` datetime NOT NULL COMMENT '下单时间', `order_amount` decimal(10,2) NOT NULL COMMENT '订单金额', `order_status` tinyint(4) NOT NULL COMMENT '订单状态', PRIMARY KEY (`order_id`), KEY `idx_user_id` (`user_id`), KEY `idx_product_id` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
由于订单数据量非常大,我们需要使用RBR模式进行数据同步,保证数据的一致性和可靠性。
(1) 配置MySQL主库开启Binlog,并设置为RBR模式。
[mysqld] log-bin=mysql-bin binlog_format=ROW server-id=1 sync_binlog=1 innodb_flush_log_at_trx_commit=1
(2) 使用Canal或者Debezium等工具监听Binlog,并将数据同步到数据仓库。
(3) 在数据仓库中,我们可以对订单数据进行各种分析,例如用户购买行为分析、商品销售情况分析等。
在这个场景下,使用RBR模式可以保证订单数据的完整性和准确性,避免因SQL语句执行结果不一致导致的数据错误。
五、SBR、RBR、MBR,该选哪个?
这三种模式各有优缺点,该选哪个,取决于你的具体场景。
特性 | SBR | RBR | MBR |
---|---|---|---|
Binlog大小 | 小 | 大 | 介于SBR和RBR之间 |
可靠性 | 较低,可能存在数据不一致 | 较高,保证数据一致性 | 较高,但仍可能存在数据不一致 |
适用性 | 简单SQL语句,不包含不确定函数的SQL语句 | 复杂存储过程、触发器等,所有场景适用 | 大部分场景适用,MySQL自行决定使用SBR或RBR |
性能 | 较高 | 较低,解析和传输成本较高 | 介于SBR和RBR之间 |
- 如果你的数据库操作很简单,对数据一致性要求不高,可以选择SBR。
- 如果你的数据库操作很复杂,对数据一致性要求很高,可以选择RBR。
- 如果你不知道该选哪个,或者想兼顾性能和可靠性,可以选择MBR。
六、总结:复制的艺术
MySQL的逻辑复制是一个非常强大的功能,可以用于数据备份、数据同步、读写分离等多种场景。但是,要想用好它,需要深入理解Binlog的原理,掌握Binlog解析的方法,以及了解RBR的性能特点。
记住,没有银弹,只有根据实际情况选择最合适的方案。
七、彩蛋:一些实用的Tips
-
定期清理Binlog:Binlog会占用大量的磁盘空间,需要定期清理。可以通过
PURGE BINARY LOGS BEFORE 'yyyy-mm-dd hh:mm:ss';
命令清理指定时间之前的Binlog。 -
监控复制延迟:可以使用
SHOW SLAVE STATUSG
命令查看复制的状态,包括复制延迟。 -
合理配置GTID:GTID(Global Transaction Identifier)可以保证事务的唯一性,避免重复执行。建议开启GTID模式。
-
使用Semi-Synchronous Replication:半同步复制可以提高数据的安全性,但会降低性能。
好了,今天的讲座就到这里。希望大家有所收获,下次再见! 记得点赞、收藏、转发哦!