点击上方 "田守枝的技术博客", 关注我
在当今互联网行业, 大多数人互联网从业者对 "单元化","异地多活" 这些词汇已经耳熟能详. 而 数据同步 是异地多活的基础, 所有具备数据存储能力的组件如: 数据库, 缓存, MQ 等, 数据都可以进行同步, 形成一个庞大而复杂的数据同步拓扑.
本文将先从概念上介绍单元化, 异地多活, 就近访问等基本概念. 之后, 将以数据库为例, 讲解在数据同步的情况下, 如何解决数据回环, 数据冲突, 数据重复等典型问题.
1 什么是单元化
如果仅仅从 "单元化" 这个词汇的角度来说, 我们可以理解为将数据划分到多个单元进行存储."单元" 是一个抽象的概念, 通常与数据中心 (IDC) 概念相关, 一个单元可以包含多个 IDC, 也可以只包含一个 IDC. 本文假设一个单元只对应一个 IDC.
考虑一开始只有一个 IDC 的情况, 所有用户的数据都会写入同一份底层存储中, 如下图所示:
这种架构是大多数据中小型互联网公司采用的方案, 存在以下几个问题:
1 不同地区的用户体验不同. 一个 IDC 必然只能部署在一个地区, 例如部署在北京, 那么北京的用户访问将会得到快速响应; 但是对于上海的用户, 访问延迟一般就会大一点, 上海到北京的一个 RTT 可能有 20ms 左右.
2 容灾问题. 这里容灾不是单台机器故障, 而是指机房断电, 自然灾害, 或者光纤被挖断等重大灾害. 一旦出现这种问题, 将无法正常为用户提供访问, 甚至出现数据丢失的情况. 这并不是不可能, 例如: 2015 年, 支付宝杭州某数据中心的光缆就被挖断过; 2018 年 9 月, 云栖大会上, 蚂蚁金服当场把杭州两个数据中心的网线剪断.
为了解决这些问题, 我们可以将服务部署到多个不同的 IDC 中, 不同 IDC 之间的数据互相进行同步. 如下图:
通过这种方式, 我们可以解决单机房遇到的问题:
1 用户体验. 不同的用户可以选择离自己最近的机房进行访问
2 容灾问题. 当一个机房挂了之后, 我们可以将这个机房用户的流量调度到另外一个正常的机房, 由于不同机房之间的数据是实时同步的, 用户流量调度过去后, 也可以正常访问数据 (故障发生那一刻的少部分数据可能会丢失).
需要注意的是, 关于容灾, 存在一个容灾级别的划分, 例如: 单机故障, 机架 (Rack) 故障, 机房故障, 城市级故障等. 我们这里只讨论机房故障和城市故障.
机房容灾 : 上面的案例中, 我们使用了 2 个 IDC, 但是 2 个 IDC 并不能具备机房容灾能力. 至少需要 3 个 IDC, 例如, 一些基于多数派协议的一致性组件, 如 zookeeper,Redis,etcd,consul 等, 需要得到大部分节点的同意. 例如我们部署了 3 个节点, 在只有 2 个机房的情况下, 必然是一个机房部署 2 个节点, 一个机房部署一个节点. 当部署了 2 个节点的机房挂了之后, 只剩下一个节点, 无法形成多数派. 在 3 机房的情况下, 每个机房部署一个节点, 任意一个机房挂了, 还剩 2 个节点, 还是可以形成多数派. 这也就是我们常说的 "两地三中心".
城市级容灾: 在发生重大自然灾害的情况下, 可能整个城市的机房都无法访问. 一些组件, 例如蚂蚁的 ocean base, 为了达到城市级容灾的能力, 使用的是 "三地五中心" 的方案. 这种情况下, 3 个城市分别拥有 2,2,1 个机房. 当整个城市发生灾难时, 其他两个城市依然至少可以保证有 3 个机房依然是存活的, 同样可以形成多数派.
小结: 如果仅仅是考虑不同地区的用户数据就近写入距离最近的 IDC, 这是纯粹意义上的 "单元化". 不同单元的之间数据实时进行同步, 相互备份对方的数据, 才能做到真正意义上 "异地多活". 实现单元化, 技术层面我们要解决的事情很多, 例如: 流量调度, 即如何让用户就近访问附近的 IDC; 数据互通, 如何实现不同机房之间数据的相互同步. 流量调度不在本文的讨论范畴内, 数据同步是本文讲解的重点.
2 如何实现数据同步
需要同步的组件有很多, 例如数据库, 缓存等, 这里以多个 MySQL 集群之间的数据同步为例进行讲解, 实际上缓存的同步思路也是类似.
2.1 基础知识
为了了解如何对不同 MySQL 的数据相互进行同步, 我们先了解一下 MySQL 主从复制的基本架构, 如下图所示:
通常一个 MySQL 集群有一主多从构成. 用户的数据都是写入主库 Master,Master 将数据写入到本地二进制日志 binary log 中. 从库 Slave 启动一个 IO 线程 (I/O Thread) 从主从同步 binlog, 写入到本地的 relay log 中, 同时 slave 还会启动一个 SQL Thread, 读取本地的 relay log, 写入到本地, 从而实现数据同步.
基于这个背景知识, 我们就可以考虑自己编写一个组件, 其作用类似与 MySQL slave, 也是去主库上拉取 binlog, 只不过 binlog 不是保存到本地, 而是将 binlog 转换成 sql 插入到目标 MySQL 集群中, 实现数据的同步.
这并非是一件不可能完成的事, MySQL 官网上已经提供好所有你自己编写一个 MySQL slave 同步 binlog 所需的相关背景知识, 访问这个链接: https://dev.mysql.com/doc/internals/en/client-server-protocol.html , 你将可以看到 MySQL 客户端与服务端的通信协议. 下图红色框中展示了 MySQL 主从复制的相关协议:
当然, 笔者的目的并不是希望读者真正的按照这里的介绍尝试编写一个 MySQL 的 slave, 只是想告诉读者, 模拟 MySQL slave 拉取 binlog 并非是一件很神奇的事, 只要你的网络基础知识够扎实, 完全可以做到. 然而, 这是一个庞大而复杂的工作. 以一人之力, 要完成这个工作, 需要占用你大量的时间. 好在, 现在已经有很多开源的组件, 已经实现了按照这个协议可以模拟成一个 MySQL 的 slave, 拉取 binlog. 例如:
阿里巴巴开源的 canal
美团开源的 puma
linkedin 开源的 databus ...
你可以利用这些组件来完成数据同步, 而不必重复造轮子. 假设你采用了上面某个开源组件进行同步, 需要明白的是这个组件都要完成最基本的 2 件事: 从源库拉取 binlog 并进行解析, 笔者把这部分功能称之为 binlog syncer ; 将获取到的 binlog 转换成 SQL 插入目标库, 这个功能称之为 sql writer .
为什么划分成两块独立的功能? 因为 binlog 订阅解析的实际应用场景并不仅仅是数据同步, 如下图:
如图所示, 我们可以通过 binlog 来做很多事, 如:
实时更新搜索引擎, 如 es 中的索引信息
实时更新 Redis 中的缓存
发送到 kafka 供下游消费, 由业务方自定义业务逻辑处理等
...
因此, 通常我们把 binlog syncer 单独作为一个模块, 其只负责解析从数据库中拉取并解析 binlog, 并在内存中缓存(或持久化存储). 另外, binlog syncer 另外提一个 sdk, 业务方通过这个 sdk 从 binlog syncer 中获取解析后的 binlog 信息, 然后完成自己的特定业务逻辑处理.
显然, 在数据同步的场景下, 我们可以基于这个 sdk, 编写一个组件专门用于将 binlog 转换为 sql, 插入目标库, 实现数据同步, 如下图所示:
北京用户的数据不断写入离自己最近的机房的 DB, 通过 binlog syncer 订阅这个库 binlog, 然后下游的 binlog writer 将 binlog 转换成 SQL, 插入到目标库. 上海用户类似, 只不过方向相反, 不再赘述. 通过这种方式, 我们可以实时的将两个库的数据同步到对端. 当然事情并非这么简单, 我们有一些重要的事情需要考虑.
2.2 如何获取全量 + 增量数据?
通常, MySQL 不会保存所有的历史 binlog. 原因在于, 对于一条记录, 可能我们会更新多次, 这依然是一条记录, 但是针对每一次更新操作, 都会产生一条 binlog 记录, 这样就会存在大量的 binlog, 很快会将磁盘占满. 因此 DBA 通常会通过一些配置项, 来定时清理 binlog, 只保留最近一段时间内的 binlog.
例如, 官方版的 MySQL 提供了 expire_logs_days 配置项, 可以设置保存 binlog 的天数, 笔者这里设置为 0, 表示默认不清空, 如果将这个值设置大于 0, 则只会保存指定的天数.
另外一些 MySQL 的分支, 如 percona server, 还可以指定保留 binlog 文件的个数. 我们可以通过 show binary logs 来查看当前 MySQL 存在多少个 binlog 文件, 如下图:
通常, 如果 binlog 如果从来没被清理过, 那么 binlog 文件名字后缀通常是 000001, 如果不是这个值, 则说明可能已经被清理过. 当然, 这也不是绝对, 例如执行 "reset master" 命令, 可以将所有的 binlog 清空, 然后从 000001 重新开始计数.
Whatever! 我们知道了, binlog 可能不会一直保留, 所以直接同步 binlog, 可能只能获取到部分数据. 因此, 通常的策略是, 由 DBA 先 dump 一份源库的完整数据快照, 增量部分, 再通过 binlog 订阅解析进行同步.
2.2 如何解决重复插入
考虑以下情况下, 源库中的一条记录没有唯一索引. 对于这个记录的 binlog, 通过 sql writer 将 binlog 转换成 sql 插入目标库时, 抛出了异常, 此时我们并不知道知道是否插入成功了, 则需要进行重试. 如果之前已经是插入目标库成功, 只是目标库响应时网络超时 (socket timeout) 了, 导致的异常, 这个时候重试插入, 就会存在多条记录, 造成数据不一致.
因此, 通常, 在数据同步时, 通常会限制记录必须有要有主键或者唯一索引.
2.3 如何解决唯一索引冲突
由于两边的库都存在数据插入, 如果都使用了同一个唯一索引, 那么在同步到对端时, 将会产生唯一索引冲突. 对于这种情况, 通常建议是使用一个全局唯一的分布式 ID 生成器来生成唯一索引, 保证不会产生冲突.
另外, 如果真的产生冲突了, 同步组件应该将冲突的记录保存下来, 以便之后的问题排查.
2.4 对于 DDL 语句如何处理
如果数据库表中已经有大量数据, 例如千万级别, 或者上亿, 这个时候对于这个表的 DDL 变更, 将会变得非常慢, 可能会需要几分钟甚至更长时间, 而 DDL 操作是会锁表的, 这必然会对业务造成极大的影响.
因此, 同步组件通常会对 DDL 语句进行过滤, 不进行同步. DBA 在不同的数据库集群上, 通过一些在线 DDL 工具(如 gh-ost), 进行表结构变更.
2.5 如何解决数据回环问题
数据回环问题, 是数据同步过程中, 最重要的问题. 我们针对 INSERT,UPDATE,DELETE 三个操作来分别进行说明:
INSERT 操作
假设在 A 库插入数据, A 库产生 binlog, 之后同步到 B 库, B 库同样也会产生 binlog. 由于是双向同步, 这条记录, 又会被重新同步回 A 库. 由于 A 库应存在这条记录了, 产生冲突.
UPDATE 操作
先考虑针对 A 库某条记录 R 只有一次更新的情况, 将 R 更新成 R1, 之后 R1 这个 binlog 会被同步到 B 库, B 库又将 R1 同步会 A 库. 对于这种情况下, A 库将不会产生 binlog. 因为 A 库记录当前是 R1,B 库同步回来的还是 R1, 意味着值没有变.
在一个更新操作并没有改变某条记录值的情况下, MySQL 是不会产生 binlog, 相当于同步终止. 下图演示了当更新的值没有变时, MySQL 实际上不会做任何操作:
上图演示了, 数据中原本有一条记录(1,"tianshouzhi"), 之后执行一个 update 语句, 将 id=1 的记录的 name 值再次更新为 "tianshouzhi", 意味着值并没有变更. 这个时候, 我们看到 MySQL 返回的影响的记录函数为 0, 也就是说, 并不会产生真是的更新操作.
然而, 这并不意味 UPDATE 操作没有问题, 事实上, 其比 INSERT 更加危险. 考虑 A 库的记录 R 被连续更新了 2 次, 第一次更新成 R1, 第二次被更新成 R2; 这两条记录变更信息都被同步到 B 库, B 也产生了 R1 和 R2. 由于 B 的数据也在往 A 同步, B 的 R1 会被先同步到 A, 而 A 现在的值是 R2, 由于值不一样, 将会被更新成 R1, 并产生新的 binlog; 此时 B 的 R2 再同步会 A, 发现 A 的值是 R1, 又更新成 R2, 也产生 binlog. 由于 B 同步回 A 的操作, 让 A 又产生了新的 binlog,A 又要同步到 B, 如此反复, 陷入无限循环中.
DELETE 操作
同样存在先后顺序问题. 例如先插入一条记录, 再删除. B 在 A 删除后, 又将插入的数据同步回 A, 接着再将 A 的删除操作也同步回 A, 每次都会产生 binlog, 陷入无限回环.
关于数据回环问题, 笔者有着血的教训, 曾经因为笔者的误操作, 将一个库的数据同步到了自身, 最终也导致无限循环, 原因分析与上述提到的 UPDATE,DELETE 操作类似, 读者可自行思考.
针对上述数据同步到过程中可能会存在的数据回环问题, 最终会导致数据无限循环, 因此我们必须要解决这个问题. 由于存在多种解决方案, 我们将在稍后统一进行讲解.
2.6 数据同步架构设计
现在, 让我们先把思路先从解决数据同步的具体细节问题转回来, 从更高的层面讲解数据同步的架构应该如何设计. 稍后的内容中, 我们将讲解各种避免数据回环的各种解决方案.
前面的架构中, 只涉及到 2 个 DB 的数据同步, 如果有多个 DB 数据需要相互同步的情况下, 架构将会变得非常复杂. 例如:
这个图演示的是四个 DB 之间数据需要相互同步, 这种拓扑结构非常复杂. 为了解决这种问题, 我们可以将数据写入到一个数据中转站, 例如 MQ 中进行保存, 如下:
我们在不同的机房各部署一套 MQ 集群, 这个机房的 binlog syncer 将需要同步的 DB binlog 数据写入 MQ 对应的 Topic 中. 对端机房如果需要同步这个数据, 只需要通过 binlog writer 订阅这个 topic, 消费 topic 中的 binlog 数据, 插入到目标库中即可. 一些 MQ 支持 consumer group 的概念, 不同的 consumer group 的消费位置 offset 相互隔离, 从而达到一份数据, 同时供多个消费者进行订阅的能力.
当然, 一些 binlog 订阅解析组件, 可能实现了类似于 MQ 的功能, 此时, 则不需要独立部署 MQ.
那么 MQ 应该选择什么呢? 别问, 问就是 Kafka, 具体原因问厮大.
3 数据据回环问题解决方案
数据回环问题有多种解决方案, 通过排除法, 一一进行讲解.
3.1 同步操作不生成 binlog
在 MySQL 中, 我们可以设置 session 变量, 来控制当前会话上的更新操作, 不产生 binlog. 这样当往目标库插入数据时, 由于不产生 binlog, 也就不会被同步会源库了. 为了演示这个效果, 笔者清空了本机上的所有 binlog(执行 reset master), 现在如下图所示:
忽略这两个 binlog event,binlog 文件格式最开始就是这两个 event.
接着, 笔者执行 set sql_log_bin=0, 然后插入一条语句, 最后可以看到的确没有产生新的 binlog 事件:
通过这种方式, 貌似可以解决数据回环问题. 目标库不产生 binlog, 就不会被同步会源库. 但是, 答案是否定的. 我们是往目标库的 master 插入数据, 如果不产生 binlog, 目标库的 slave 也无法同步数据, 主从数据不一致. 所以, 需要排除这种方案.
提示: 如果恢复 set sql_log_bin=1, 插入语句是会产生 binlog, 读者可以自行模拟.
3.2 控制 binlog 同步方向
既然不产生 binlog 不能解决问题. 那么换一种思路, 可以产生 binlog. 当把一个 binlog 转换成 sql 时, 插入某个库之前, 我们先判断这条记录是不是原本就是这个库产生的, 如果是, 那么就抛弃, 也可以避免回环问题.
现在问题就变为, 如何给 binlog 加个标记, 表示其实那个 MySQL 集群产生的. 这也有几种方案, 下面一一讲述.
3.2.1 ROW 模式下的 SQL
MySQL 主从同步, binlog 复制一般有 3 种模式. STATEMENT,ROW,MIXED. 默认情况下, STATEMENT 模式只记录 SQL 语句, ROW 模式只记录字段变更前后的值, MIXED 模式是二者混合. binlog 同步一般使用的都是 ROW 模式, 高版本 MySQL 主从同步默认也是 ROW 模式.
我们想采取的方案是, 在执行的 SQL 之前加上一段特殊标记, 表示这个 SQL 的来源. 例如
/*IDC1:DB1*/insert into users(name) values("tianbowen")
其中 /*IDC1:DB1*/ 是一个注释, 表示这个 SQL 原始在是 IDC1 的 DB1 中产生的. 之后, 在同步的时候, 解析出 SQL 中的 IDC 信息, 就能判断出是不是自己产生的数据.
然而, ROW 模式下, 默认只记录变更前后的值, 不记录 SQL. 所以, 我们要通过一个开关, 让 MySQL 在 ROW 模式下也记录 INSERT,UPDATE,DELETE 的 SQL 语句. 具体做法是, 在 MySQL 的配置文件中, 添加以下配置:
binlog_rows_query_log_events =1
这个配置可以让 MySQL 在 binlog 中产生 ROWS_QUERY_LOG_EVENT 类型的 binlog 事件, 其记录的就是执行的 SQL.
通过这种方式, 我们就记录下的一个 binlog 最初是由哪一个集群产生的, 之后在同步的时候, sql writer 判断目标机房和当前 binlog 中包含的机房相同, 则抛弃这条数据, 从而避免回环.
这种思路, 功能上没问题, 但是在实践中, 确非常麻烦. 首先, 让业务对执行的每条 sql 都加上一个这样的标识, 几乎不可能. 另外, 如果忘记加了, 就不知道数据的来源了. 如果采用这种方案, 可以考虑在数据库访问层中间件层面添加支持在 sql 之前增加 /*..*/ 的功能, 统一对业务屏蔽. 即使这样, 也不完美, 不能保证所有的 sql 都通过中间件来来写入, 例如 DBA 的一些日常运维操作, 或者手工通过 MySQL 命令行来操作数据库时, 肯定会存在没有添加机房信息的情况.
总的来说, 这个方案不是那么完美.
3.2.2 通过附加表
这种方案目前很多知名互联网公司在使用. 大致思路是, 在 db 中都加一张额外的表, 例如叫 direction, 记录一个 binlog 产生的源集群的信息. 例如
- CREATE TABLE `direction` (
- `idc` varchar(255) not null,
- `db_cluster` varchar(255) not null,
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
idc 字段用于记录某条记录原始产生的 IDC,db_cluster 用于记录原始产生的数据库集群(注意这里要使用集群的名称, 不能是 server_id, 因为可能会发生主从切换).
假设用户在 IDC1 的库 A 插入的一条记录(也可以在事务中插入多条记录, 单条记录, 即使不开启事务, MySQL 默认也会开启事务):
- BEGIN;
- insert into users(name) values("tianshouzhi");
- COMMIT;
那么 A 库数据 binlog 通过 sql writer 同步到目标库 B 时, sql writer 可以提前对事务中的信息可以进行一些修改,, 如下所示:
- BEGIN;
- # 往目标库同步时, 首先额外插入一条记录, 表示这个事务中的数据都是 A 产生的.
- insert into direction(idc,db_cluster) values("IDC1","DB_A")
- # 插入原来的记录信息
- insert into users(name) values("tianshouzhi");
- COMMIT;
之后 B 库的数据往 A 同步时, 就可以根据 binlog 中的第一条记录的信息, 判断这个记录原本就是 A 产生的, 进行抛弃, 通过这种方式来避免回环. 这种方案已经已经过很多的公司的实际验证.
3.2.3 通过 GTID
MySQL 5.6 引入了 GTID(全局事务 id)的概念, 极大的简化的 DBA 的运维. 在数据同步的场景下, GTID 依然也可以发挥极大的威力.
GTID 由 2 个部分组成:
其中 server_uuid 是 MySQL 随机生成的, 全局唯一. transaction_id 事务 id, 默认情况下每次插入一个事务, transaction_id 自增 1. 注意, 这里并不会对 GTID 进行全面的介绍, 仅说明其在数据同步的场景下, 如何避免回环, 数据重复插入的问题.
GTID 提供了一个会话级变量 gtid_next, 指示如何产生下一个 GTID. 可能的取值如下:
AUTOMATIC: 自动生成下一个 GTID, 实现上是分配一个当前实例上尚未执行过的序号最小的 GTID.
ANONYMOUS: 设置后执行事务不会产生 GTID, 显式指定的 GTID.
默认情况下, 是 AUTOMATIC, 也就是自动生成的, 例如我们执行 sql:
产生的 binlog 信息如下:
可以看到, GTID 会在每个事务 (Query->...->Xid) 之前, 设置这个事务下一次要使用到的 GTID.
从源库订阅 binlog 的时候, 由于这个 GTID 也可以被解析到, 之后在往目标库同步数据的时候, 我们可以显示的的指定这个 GTID, 不让目标自动生成. 也就是说, 往目标库, 同步数据时, 变成了 2 条 SQL:
- SET GTID_NEXT='09530823-4f7d-11e9-b569-00163e121964:1'
- insert into users(name) values("tianbowen")
由于我们显示指定了 GTID, 目标库就会使用这个 GTID 当做当前事务 ID, 不会自动生成. 同样, 这个操作也会在目标库产生 binlog 信息, 需要同步回源库. 再往源库同步时, 我们按照相同的方式, 先设置 GTID, 在执行解析 binlog 后得到的 SQL, 还是上面的内容
- SET GTID_NEXT= '09530823-4f7d-11e9-b569-00163e121964:1'
- insert into users(name) values("tianbowen")
由于这个 GTID 在源库中已经存在了, 插入记录将会被忽略, 演示如下:
- MySQL> SET GTID_NEXT= '09530823-4f7d-11e9-b569-00163e121964:1';
- Query OK, 0 rows affected (0.00 sec)
- MySQL> insert into users(name) values("tianbowen");
- Query OK, 0 rows affected (0.01 sec) #注意这里, 影响的记录行数为 0
注意这里, 对于一条 insert 语句, 其影响的记录函数居然为 0, 也就会插入并没有产生记录, 也就不会产生 binlog, 避免了循环问题.
如何做到的呢? MySQL 会记录自己执行过的所有 GTID, 当判断一个 GTID 已经执行过, 就会忽略. 通过如下 sql 查看:
上述 value 部分, 冒号 ":" 前面的是 server_uuid, 冒号后面的 1-5, 是一个范围, 表示已经执行过 1,2,3,4,5 这个几个 transaction_id. 这里就能解释了, 在 GTID 模式的情况下, 为什么前面的插入语句影响的记录函数为 0 了.
显然, GTID 除了可以帮助我们避免数据回环问题, 还可以帮助我们解决数据重复插入的问题, 对于一条没有主键或者唯一索引的记录, 即使重复插入也没有, 只要 GTID 已经执行过, 之后的重复插入都会忽略.
当然, 我们还可以做得更加细致, 不需要每次都往目标库设置 GTID_NEXT, 这毕竟是一次网络通信. sql writer 在往目标库插入数据之前, 先判断目标库的 server_uuid 是不是和当前 binlog 事务信息携带的 server_uuid 相同, 如果相同, 则可以直接丢弃. 查看目标库的 gtid, 可以通过以下 sql 执行:
GTID 应该算是一个终极的数据回环解决方案, MySQL 原生自带, 比添加一个辅助表的方式更轻量, 开销也更低. 需要注意的是, 这倒并不是一定说 GTID 的方案就比辅助表好, 因为辅助表可以添加机房等额外信息. 在一些场景下, 如果下游需要知道这条记录原始产生的机房, 还是需要使用辅助表.
4 开源组件介绍 canal/otter
前面深入讲解了单元化场景下数据同步的基础知识. 读者可能比较感兴趣的是, 哪些开源组件在这些方面做的比较好. 笔者建议的首选, 是 canal/otter 组合.
canal 的作用就是类似于前面所述的 binlog syncer, 拉取解析 binlog.otter 是 canal 的客户端, 专门用于进行数据同步, 类似于前文所讲解的 sql writer. 并且, canal 的最新版本已经实现了 GTID.
另外, 笔者也在自己的博客上写了一个 canal 系列的源码分析教程, 完成了大部分. 相信对于需要深入阅读 canal 源码的用户有一定的借鉴意义. 点击 阅读原文 , 即可查看.
识别二维码关注我
来源: http://www.tuicool.com/articles/a2EnQ3F