最近帮一个订单系统接Canal,用它监听MySQL的binlog日志,把数据准实时同步到Redis和ES,整个过程踩了不少坑。整理一下这整套方案的思路、配置细节和排障经验,给后面接手类似"业务数据库变一下,Redis缓存和ES索引马上跟着变"需求的朋友做个参考。
先说清楚这套东西是什么:Canal 是阿里巴巴开源的一个 MySQL binlog 增量订阅组件,它把自己伪装成一个 MySQL 从库,通过主从复制协议去拉取 binlog,然后解析成结构化事件,下游无论是写 Redis、ES,还是丢进 MQ 里慢慢消费,都很顺手。适合的场景很典型——缓存里的热点数据、搜索引擎里的商品或订单数据,不能老靠定时任务刷全量,也不能在业务代码里双写,那就让 Canal 在中间做"数据搬运工"。
这套方案适合谁?后端开发、数据同步开发、搞中间件的人都能用上。即使你还没接触过 binlog,看完也能照着搭起来,知道每一步为什么这么配,遇到问题往哪个方向查。
1. 业务侧的真实痛点:缓存和搜索为何总是慢半拍
1.1 缓存失效与搜索索引更新的死结
大多数业务系统里,MySQL 是数据的最终归宿,但 MySQL 扛不住高并发读,所以前面挡一层 Redis;而复杂查询、模糊搜索、聚合条件,MySQL 的 like 和联合查询又很吃力,于是又上 Elasticsearch。
问题就出在这:同样一份数据,在 MySQL、Redis、ES 里各存了一份,谁来保证它们之间的一致性?
我见过很多团队的做法是业务代码双写。更新数据库的时候,顺手把 Redis 删掉、把 ES 更新一下。听起来没什么,实际跑起来全是坑。第一,双写导致业务逻辑里到处都是缓存代码和搜索同步代码,换数据库或者换缓存的成本极高;第二,双写不是原子的,数据库提交成功、ES 写入失败,库存和搜索就永远不一致了;第三,为了补偿失败,还得再加消息队列重试,逻辑越来越复杂。
另一种常用方案是定时任务扫表。每隔一分钟把变更的数据捞出来同步一次。这个方案在小数据量下能凑合,但实时性太差,而且每次全表扫描对 MySQL 压力很大,扫到刚更新过的数据还好,扫不到就会有窗口期。数据量一上来,定时任务还没跑完,业务数据又变了。
Canal 能破这个局,是因为它站在数据库外面,通过 binlog 感知每一次行级变更,把"数据库自己产生的日志"变成"下游存储的更新指令"。业务代码不用改一行,缓存和搜索的同步完全剥离出去。
1.2 binlog 为什么能当"数据总线"
MySQL 本身就有一套日志机制叫 binlog,二进制日志,专门用来记录所有数据变更。主从复制就是靠它实现的:主库把 binlog 发给从库,从库拿到后重放,就得到了和主库一样的数据。
binlog 有三种格式:STATEMENT、ROW、MIXED。STATEMENT 记录的是 SQL 原句,比如UPDATE user SET name='张三' WHERE id=1,从库拿到后重新执行一遍。ROW 记录的是每一行变更前后的完整字段值,对 Canal 来说这才是最有价值的东西——它不需要知道你的 SQL 长什么样,只需要拿到id=1这行数据更新前是什么、更新后是什么,就能直接把结果塞给 Redis 或 ES。
所以 Canal 设计上做了一件很妙的事:它把自己伪装成一个从库,向 MySQL 发送 dump 请求,MySQL 就把 binlog 推给它。Canal 拿到日志后解析成结构化消息,下游想怎么消费怎么消费。
这和我们直接监听主从复制的区别在于,主从复制是把 binlog 应用到另一台 MySQL,而 Canal 是把它变成"事件流",喂给异构存储。相当于把 MySQL 的日志能力扩展成了一根数据总线,接多少个下游都行,下游挂了也不影响 MySQL 本身。
2. 整体方案设计:先画一条数据流转链路
2.1 模式对比:Canal Adapter 还是自研 Client
Canal 官方除了 Server 本体,还提供了一个 Adapter 组件,可以理解成"开箱即用的消费者",它直接从 Canal Server 拿增量数据,再通过配置把数据写到 Redis、ES、RDB 等目标端。
使用 Canal Adapter 的好处很直接:不用写代码。表结构定了以后,写个映射配置文件,启动 Adapter,数据就开始同步了。尤其适合那种字段不复杂、SCHEMA 比较稳的表。
但它的短板也很明显。Adapter 的同步逻辑是内置好的,按表映射来做"全字段覆盖"。如果你想做字段裁剪、数据清洗、调用下游接口、做复杂幂等判断,Adapter 就不太够用了。这种时候需要写一个自己的消费端,使用 Canal 提供的 Java Client 从 Server 拉取数据,自己解析 Entry,再按业务规则去更新 Redis 和 ES。
两种模式并不互斥。我的建议是:快速验证阶段先上 Adapter,跑通链路、观察效果;如果后面出现复杂场景,比如同一个表要同步成两种不同的 Redis 结构,或者要把几条 binlog 聚合后一起写 ES,就切到自研 Client。
2.2 引入 MQ 与否,资源如何权衡
有一种常见架构是 Canal Server 把 binlog 直接投递到 Kafka 或 RocketMQ,下游再各自消费。这样做有几个好处:一是 Canal 和 Target 存储解耦,Canal 只负责把事件写进 MQ,写入 Redis/ES 的消费端可以独立扩缩容;二是削峰填谷,瞬间大批量更新时,MQ 会帮下游把压力摊平,避免 Redis 和 ES 被同时打爆;三是 MQ 自带位点管理,消费端重启后能接着上次的位置消费,比起自己维护 offset 省事。
但引入 MQ 也意味着多一个中间件要运维,链路更长,排查问题也更麻烦。数据量不大、对延迟容忍度较高、团队规模小的时候,直接用 Adapter 或 Client 直连 Canal Server 完全够用。
我在实际操作里一般这样判断:单表日均变更量低于几十万,直接用 Adapter 或简单 Client;变更量很大,或者下游消费逻辑很重,就上 MQ。前者图省事,后者图安稳,没有绝对最优,只有当前阶段合不合适。
2.3 同步粒度与存储策略的划分
不是所有表都要同步,也不是所有表同步后 Redis 和 ES 都要一份。我习惯先把表按用途分三类。
第一类,热点查询型。比如商品基本信息、用户资料,这些数据高频读、低频写,适合同步到 Redis,用 hash 或 string 缓存,key 设计成表名:id。这类表不需要同步 ES。
第二类,搜索筛选型。比如订单表、商品 SPU 表,查询条件多、需要分词、需要排序,这类表必须同步到 ES。同步时可以只同步需要的字段,通过 SQL 把关联表拼好再写入一个文档,没必要把 MySQL 原表机械地镜像成一个 index。
第三类,宽表型。比如一张订单宽表,既要做详情缓存又要做列表搜索,那么 Redis 和 ES 都需要。同步的时候更新 Redis 一份完整 JSON,更新 ES 一份文档模型,两份数据的字段可以不同,只要保证主键一致就行。
把表分好类之后,Canal 的订阅过滤也更好配。比如在 Canal Server 的 instance 里只订阅某个库的某几张表,或者在 Client 里做正则过滤,减少无效日志的解析开销。
3. 环境准备:MySQL 开启 binlog 的完整姿势
3.1 检查状态与修改 MySQL 配置
网上 MySQL 安装教程很多,这里不重复,我直接讲 binlog 相关配置。先确认当前实例是否已经开启 binlog:
SHOW VARIABLES LIKE 'log_bin';如果返回值是OFF,就需要改配置文件。Linux 下一般是/etc/my.cnf或/etc/mysql/mysql.conf.d/mysqld.cnf,Windows 下是my.ini。在[mysqld]段下面加上:
[mysqld] server-id=10 log-bin=mysql-bin binlog_format=ROW binlog_row_image=FULL expire_logs_days=7 gtid_mode=ON enforce_gtid_consistency=ON逐行解释一下为什么这么配。server-id在复制协议里是实例的唯一标识,Canal 伪装从库时也需要一个不一样的值,不能和现存的主从拓扑里任何节点的 server-id 重复,否则 MySQL 会踢掉其中一个连接。log-bin=mysql-bin指定日志文件前缀。binlog_format=ROW是最关键的一项,上面说过,ROW 格式才有行级变更的字段值,STATEMENT 让 Canal 没法高效构造目标数据。
binlog_row_image=FULL很容易被忽略,它表示在 ROW 格式下记录整行所有字段的镜像。如果改成 MINIMAL,日志里只包含主键和发生变化的列,对某些场景能省日志量,但对同步 Redis 或 ES 来说,我们往往需要完整的字段值,所以才设成 FULL。
expire_logs_days=7控制日志保留天数,避免磁盘被灌满。gtid_mode=ON和enforce_gtid_consistency=ON是给 Canal 一个更稳定的位点管理方式,后面配置 instance 时会用到。
改完配置需要重启 MySQL。重启后再次查SHOW VARIABLES LIKE 'log_bin';确认是 ON,然后执行:
SHOW MASTER STATUS;会看到类似mysql-bin.000001 | 154 | ...这样的输出,这就是当前 binlog 文件和 position 点位。后面配置 Canal 时如果不想从最新开始消费,就可以填写这个点位。
注意:不要手贱去服务器上直接
rmbinlog 文件。如果日志占满磁盘,用 MySQL 自带的清理命令,比如PURGE BINARY LOGS TO 'mysql-bin.000100';,或者通过expire_logs_days自动过期。
3.2 创建最小权限的 Canal 专用账号
Canal 需要连上 MySQL 拉取 binlog,但它不应该有业务库的写权限。给它建一个专用账号,权限收窄到最小集合:
CREATE USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY '你的强密码'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;SELECT权限是 Canal Adapter 做全量数据初始化(ETL)时要读取表数据用的,普通增量同步其实只需要后面两个REPLICATION权限。REPLICATION SLAVE让 Canal 可以请求 binlog,REPLICATION CLIENT让它可以查看 master 状态和 server 信息。
如果你的 MySQL 是 8.0 以上,默认认证插件是caching_sha2_password,老版本 Canal 可能连不上,报Access denied或者Public Key Retrieval is not allowed。上面建用户时我特意写了IDENTIFIED WITH mysql_native_password,就是为了兼容。如果你已经是 Canal 1.1.5 以上版本,理论上也支持 caching_sha2,但为了省事,很多生产环境还是沿用 native 密码。注意这是权衡,不是绝对要求。
验证账号能不能用,在另一台机器上执行:
mysql -h 127.0.0.1 -ucanal -p -P3306需要确认宿主机防火墙放行了 3306,且 MySQL 的bind-address没有只绑 127.0.0.1。
4. Canal Server 与 Adapter 配置实录
4.1 部署 Canal Server:instance 是关键
到 GitHub 的 Release 页面下载canal.deployer-1.1.5.tar.gz,解压后的目录结构很清晰:bin放启动脚本,conf放配置,lib放依赖,logs放日志。
Canal Server 本身有一套全局配置conf/canal.properties,但大多数情况下不需要改,重点是conf/example/instance.properties,这个文件定义了一个名为 example 的 canal instance,它决定了要连哪个 MySQL、从哪里开始消费。
需要改的核心配置如下:
# MySQL 主库地址 canal.instance.master.address=127.0.0.1:3306 # 刚才创建的 MySQL 账号 canal.instance.dbUsername=canal canal.instance.dbPassword=你的强密码 # 连接编码 canal.instance.connectionCharset=UTF-8 # 如果 MySQL 开了 GTID,这里必须打开 canal.instance.gtidon=true # 指定要订阅的库表,正则写法 canal.instance.filter.regex=mydb\\..*filter.regex的规则是库名\.表名,支持正则。如果只想监听 user 和 order 两张表,可以写成:
canal.instance.filter.regex=mydb\\.user|mydb\\.order如果你没开 GTID,Canal 默认会从当前位点开始,但也可以通过canal.instance.master.journal.name和canal.instance.master.position指定从哪个 binlog 文件的哪个位置开始消费,对应我们刚才查到的SHOW MASTER STATUS结果。
启动:
bin/startup.sh看日志:
tail -f logs/example/example.log如果看到类似dump address ...或者success to connect的字样,说明 Canal 已经和 MySQL 正常握手了。接着你去业务表插入一条数据,日志里会打印解析到的 CanalEntry,这一步通了就说明 binlog 链路没问题。
4.2 使用 Adapter 同步到 Redis
Canal Adapter 需要单独下载canal.adapter-1.1.5.tar.gz,里面同样有conf目录,主配置是conf/application.yml。它负责定义数据源、Canal Server 地址、以及多个外部适配器。
一个简化可用的配置长这样:
canal.conf: mode: tcp flatMessage: true consumerProperties: canal.tcp.server.host: 127.0.0.1:11111 canal.tcp.batch.size: 500 srcDataSources: defaultDS: url: jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false&useUnicode=true&characterEncoding=UTF-8 username: canal password: 你的强密码 canalAdapters: - instance: example groups: - groupId: g1 outerAdapters: - name: logger - name: redis key: redis hosts: 127.0.0.1:6379 mode: single其中logger适配器是打印日志用的,方便调试;redis适配器会把事件写到 Redis。Redis 的mode有single、cluster、sentinel几种,本地测试先 single 就行。
真正决定表怎么映射成 Redis key 的是表映射文件,放在conf/redis/目录下。假设用户表是user,新建一个user.yml:
dataSourceKey: defaultDS destination: example groupId: g1 outerAdapterKey: redis redisMapping: key: user:{id} value: json targetTable: user这里key模板里的{id}会被替换成行数据里的 id 字段,所以插入 id=10 的用户后,Redis 里会出现一个 key 为user:10的字符串,值是整行数据的 JSON。
注意targetTable和文件名、destination、groupId 都要对应上,否则 Adapter 找不到映射关系。还有一点,如果flatMessage: false,Adapter 收到的是 protobuf 格式,需要额外配置 proto 反序列化,没必要,默认 true 就够了。
4.3 使用 Adapter 同步到 ES
ES 适配器同样在主配置的outerAdapters里加一项:
- name: es hosts: http://127.0.0.1:9200 properties: mode: restES 的 mapping 文件放在conf/es/下,比如user_index.yml:
dataSourceKey: defaultDS destination: example groupId: g1 outerAdapterKey: es esMapping: _index: user _type: _doc _id: id sql: "select id, name, age, phone from user" etlCondition: "where id > 0" commitBatch: 1000sql是 Adapter 用来做全量 ETL 和增量关联的查询语句。当 user 表有变化时,Adapter 会拿变更行的 id 值带进去,查出完整字段,再写入 ES 的user索引。
这里的_type在 ES 7.x 里统一是_doc,ES 8 里 type 已经被移除了,如果你用的是 ES 8,建索引时不需要 type,映射文件里也不要再写_type。
如果表的数据需要关联其他表,比如user关联user_ext的 bio 字段,你的sql可以写成:
select u.id, u.name, u.age, e.bio from user u left join user_ext e on u.id = e.user_idCanal Adapter 会在每次 user 表变更时,拿变化的 id 执行这条 SQL,把关联数据取出来,拼成一个完整文档后写入 ES。这个能力非常实用,相当于把宽表建索引的工作交给了同步层。
启动 Adapter:
bin/startup.sh观察日志,如果报连接错误,多半是 ES hosts 地址不对,或者 Adapter 版本和 ES 版本不兼容。我踩过的一个坑是 ES 7.16 之后强制要求Content-Type,老版本 Adapter 用的 restlet 会报406 Not Acceptable,升级 Adapter 到 1.1.5 基本能解决。
4.4 自定义消费端:把控制权拿回自己手里
当 Adapter 满足不了需求时,我一般会写一个独立的 Java 服务,用 Canal Client 直接消费 binlog 事件。这种方式最核心的一点是:你亲自掌控 ack 和 rollback。
先引入依赖:
<dependency> <groupId>com.alibaba.otter</groupId> <artifactId>canal.client</artifactId> <version>1.1.5</version> </dependency>然后写一个循环拉取逻辑:
CanalConnector connector = CanalConnectors.newSingleConnector( new InetSocketAddress("127.0.0.1", 11111), "example", "", ""); connector.connect(); connector.subscribe("mydb\\..*"); connector.rollback(); while (true) { Message message = connector.getWithoutAck(100); long batchId = message.getId(); if (batchId == -1 || message.getEntries().isEmpty()) { Thread.sleep(1000); continue; } try { for (CanalEntry.Entry entry : message.getEntries()) { if (entry.getEntryType() != CanalEntry.EntryType.ROWDATA) { continue; } CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue()); CanalEntry.EventType eventType = rowChange.getEventType(); String tableName = entry.getHeader().getTableName(); for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) { String operation = eventType.name(); // INSERT / UPDATE / DELETE List<CanalEntry.Column> columns = eventType == CanalEntry.EventType.DELETE ? rowData.getBeforeColumnsList() : rowData.getAfterColumnsList(); Map<String, Object> data = columns.stream().collect( Collectors.toMap(CanalEntry.Column::getName, c -> c.getIsNull() ? null : c.getValue())); syncToRedisAndEs(tableName, operation, data); } } connector.ack(batchId); } catch (Exception e) { connector.rollback(batchId); // 这里要做好本地补偿日志,至少记录一条失败消息 } }代码里几个关键点:
getWithoutAck(100)表示一次最多拉 100 条事件,但不确认;处理成功后调用ack(batchId)告诉 Canal 可以清掉这批位点;处理异常时调用rollback(batchId),Canal 会把这一批重新投递。
DELETE事件里afterColumns往往是空的,所以要用beforeColumns拿主键,否则你连删的是哪条数据都不知道。INSERT和UPDATE用afterColumns,也就是变更后的新值。
写 Redis 的时候,我用 Jackson 把 Map 转成 JSON 后 set 进去。写 ES 时,根据操作类型分别调用IndexRequest或DeleteRequest。更稳妥的做法是统一用 upsert,更新不存在就插入,存在就覆盖。
5. 生产级避坑清单与稳定性兜底
5.1 缓存更新顺序与数据回源的坑
用 Canal 同步 Redis 的时候,很多人会问:直接把缓存删掉不就行了?读的时候回源数据库。这种思路在一致性要求不高时没问题,但有一个条件:回源读到的是最新数据。如果 Canal 延迟了,读请求先到,发现缓存没有,就去数据库查,查到的是旧值(因为 Canal 还没消费到最新 binlog),然后这个旧值又被写回缓存,等 Canal 消费到新值再更新缓存,中间就会有一段旧值窗口。
我处理这个问题的做法是:Canal 同步到 Redis 时直接写新值,而不是删 key。Canal 本身能拿到变更后的字段,把它序列化后 set 进去,读请求在 Canal 异常延迟时最多读到旧 JSON,不会触发"读了更旧的值回填"的问题。
另一种常见方案是延迟双删:先删缓存,间隔几百毫秒再删一次。但双删治标不治本,间隔时间长了还是有窗口,间隔短了删除顺序又无法保证。既然我们已经用了 binlog,不如让缓存始终跟着数据库走。
5.2 ES 同步幂等与并发
写 ES 时最容易出问题的场景是并发更新。两个线程同时拿到一条数据的 binlog,一个写旧版本,一个写新版本,如果顺序颠倒,ES 里的最终值可能就是错的。
ES 自带版本号机制,可以在请求里带version参数,用外部版本号来控制。Canal 解析出来的每条 binlog 不带业务版本,但你可以用变更发生的时间戳ts作为版本:写入时指定versionType=external,ES 内部会维护一个_version,只有新版本的 ts 更大才能写入成功。
如果不引入版本号,另一个办法是保证单条数据只被一个消费者处理。用 MQ 的话,按主键 Hash 到固定分区,这样同一条记录的 binlog 一定会被同一个消费者顺序处理,天然规避了并发交错。
批量更新时要用 BulkRequest,把几百条写操作合成一个请求发过去,吞吐量能提升很多。我一般每次攒 1000 条左右,或者每隔一秒 flush 一次。攒太多会导致单次请求过大,ES 内存压力高;攒太少又浪费网络往返。
5.3 积压、重复消费与位点管理
Canal 通过 ack 机制保证"至少一次"投递,所以消费端必须接受可能重复。第一次写缓存时可能服务刚起来,或者 ES 临时不可用,rollback 之后同一批数据会重新投递。所以你的消费逻辑必须是幂等的:写 Redis 的 set 操作天然幂等;写 ES 的 upsert 也是幂等;但如果消费逻辑里有"累加库存"这种计算,就不幂等,需要额外做去重,比如维护一个最近处理过的 eventId 集合。
位点管理还有一个容易忽略的问题:Canal Server 本身也会挂。如果配置了 zookeeper,Canal 会把位点信息放 zk,恢复后自动续传。如果没上 zk,重启后有可能从当前 binlog 最新位置开始,中间一段数据就丢了。我的建议是至少在单机测试时启动tsdb=enable,也就是让 Canal 自己保存位点表,避免每次重启都丢数据。
延迟监控方面,Canal Server 内置了 metrics,可以用 Prometheus 拉取;如果上了 MQ,看 MQ 堆积数更直观。没有监控系统的话,我习惯写一个简单的"心跳表":业务侧每隔几秒更新一条heartbeat记录的某个字段,Canal 消费到这条事件时记录时间戳,和当前时间比一下,就知道端到端延迟是几秒。
5.4 DDL 与敏感字段
binlog 里不只包含 DML,还会包含 DDL。当某个表加了一列,Canal 会向消费者推送一个 DDL 事件。Adapter 对这个事件基本是忽略的,它不会自动帮你修改 Redis 结构或 ES mapping。所以如果业务表结构变了,需要人工介入,重建索引或者改 Adapter 的映射 SQL,不能指望同步层自己搞定。
还有敏感字段问题。如果你的 binlog 里有手机号、身份证这类数据,全量镜像到 ES 会有数据合规风险。两个思路:一是做字段裁剪,ES 映射 SQL 里只 select 需要索引的字段;二是在消费端做脱敏,比如手机号只保留前三位后四位再写 ES,或者加一层加密。这个必须在同步链路里提前设计好,不然后面删数据很痛苦。
对于 binlog 日志本身的安全,日志文件保留天数要合理,且存放目录的权限要收紧。MySQL 账号我们已经做了最小权限,不需要再给业务开发开放 binlog 的本地文件读取权限。
5.5 问题排查速查表
| 现象 | 可能原因 | 排查命令或手段 |
|---|---|---|
| Canal 连接被拒 | 账号权限不足、认证插件不符、3306未放开 | SHOW GRANTS FOR canal@'%';远程mysql -ucanal -p试连 |
| binlog 已开但抓不到事件 | filter 正则写错、表不在匹配范围 | 看 example.log 里订阅的 Filter,手动插入测试 |
| Adapter 连不上 Redis | hosts 配置错误、Redis 设置了密码但未配置 password | application.yml 的 redis 节点补password |
| ES 写入报 406 | Adapter 版本和 ES 版本兼容问题 | 升级 Adapter 1.1.5+,或换 ES 7.x |
| 同步延迟越来越大 | 消费能力不足、下游慢查询、批量太小 | 看 MQ 堆积,开启 bulk,增加消费线程 |
| UPDATE 拿不到新字段值 | binlog_row_image=MINIMAL | 改成 FULL 并重启 MySQL,重新生成日志 |
| DELETE 事件没有数据 | 错用了 afterColumns | 用 beforeColumns 拿主键 |
6. 一个收尾的小建议
这套方案做到最后,我发现自己收获最大的不是把 Canal 配置调通了,而是养成了一套验证同步链路的习惯。每次上线一个新的同步任务,不管用 Adapter 还是自研 Client,我第一步不是对着文档抄配置,而是先在测试库手动插一条明显的数据,比如把某个字段写成SN_TEST_001,然后依次去看三个地方:MySQL 的 binlog 里有没有这条变更、Canal 的日志里有没有解析出来、Redis/ES 里有没有出现对应记录。三步串通了,再把这个测试数据清掉。这套人工观测链路比任何监控都直白,出了问题一眼能看出断在哪个环节。
还有一个细节想多说一句:如果你也遇到 Redis 里同步出来的数据总是少字段,先别怀疑 Canal,回去看一眼 binlog_row_image 是不是 FULL。我在这上面浪费了整整一个下午,当时只改了 binlog_format=ROW,以为万事大吉,结果 UPDATE 事件里只有主键和修改列,ES 和 Redis 拿不到完整字段,匹配不上映射关系。这个配置位置在 MySQL 服务器,不在 Canal,排查方向错了就会一直绕圈。
Canal 这套组合拳用顺了之后,业务侧几乎无感,数据从 MySQL 流向 Redis 和 ES 就像一条安静的水管。希望你也能把它接得稳稳的,少踩几个我曾经踩过的坑。