基于Canal实现MySQL实时同步到ES

1,889 阅读4分钟

哈喽,大家好,我是一条。

书接上文,在《SpringBoot+Vue+ES 实现仿百度全文搜索》的结尾我们提到如何实时的将 MySql 的数据同步到 ES 供全文搜索使用是我们下一步需要解决的问题,我们要保证普通查询和全文搜索的数据一致性。

能想到的方案有如下几种:

  • 插入 MySql 的同时插入 ES,毫无疑问这么做耦合性太高,核心业务很容易被 ES 拖累,不可行。
  • 插入 MySql 的同时给 MQ 发送一条消息,再由同步服务消费消息,将数据插入 ES ,这么做虽然降低耦合度,但是会有一定延迟,取决于并发量和 MQ 的吞吐量,不够完美。
  • 使用 Canal 基于 binlog 实现数据的实时同步,可行。

综上,本文来聊一下基于 Canal 的实现方式。

Canal 简介

Canal译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费。

最早是阿里为了同步杭州和美国双机房的数据开发,后孵化为来源项目,分为服务端、客户端和管理界面:

服务端:负责解析MySQL的binlog日志,传递增量数据给客户端或者消息中间件

客户端:负责解析服务端传过来的数据,然后定制自己的业务处理。

管理界面:对集群和instance的可视化管理。

官网

其原理就是伪装成 MySql 的一个 Slave ,订阅其 binlog ,解析出变更数据后再同步到其它中间件。

配置MySql

首先需要开启 MySql 的 binlog ,选择行模式

# 是否开启binlog,ROW模式
show variables like 'log_bin%';
show variables like 'binlog_format%';
show variables like '%server_id%';

/mysql.conf.d/mysqld.cnf中配置如下:

[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
binlog-do-db=yitiao_admin # binlog-do-db 指定具体数据库,如果不配置则表示所有数据库均开启Binlog

创建cannl用户并授权

CREATE USER canal IDENTIFIED BY 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;

-- 注意如果使用 MySql8.0 需要更改密码的加密方式,否则无法连接
ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'canal';
FLUSH PRIVILEGES; #刷新权限

Canal 服务端

在github下载安装包,canal-deploy-1.1.6

一个 Server 可以配置多个实例监听 ,Canal 功能默认自带的有个 example 实例,本篇就用 example 实例 。如果增加实例,复制 example 文件夹内容到同级目录下,然后在 canal.properties 指定添加实例的名称。

instance.properties

# url
canal.instance.master.address=00000:3306
# username/password
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal

# 监听的数据库,表,可以指定,多个用逗号分割,这里正则是监听所有
canal.instance.filter.regex=.*\\..*
# canal.instance.filter.regex=yitiao_admin\\.code_note

canal.properties

# 默认端口 11111
# 默认输出model为tcp, 这里根据使用的mq类型进行修改
# tcp, kafka, RocketMQ
canal.serverMode = tcp

# canal可以有多个instance,每个实例有独立的instance.properties配置文件,
# 默认带有一个example实例,如果需要处理多个mysql数据的话,可以复制出多个example,对其重新命名,
# 命令和配置文件中指定的名称一致。然后修改canal.properties 中的 canal.destinations
# canal.destinations=实例 1,实例 2,实例 3
canal.destinations = example

启动server

./bin/startup.sh   # 如果内存不充裕,需要修改启动的JVM参数,亲测256m够用

查看canal.log,启动成功

Canal 客户端

Canal 官方提供了和各种中间件的对接的客户端,即adapter,由于我服务器的内存限制,无法再启动一个客户端服务,所以使用Java客户端,然后自己编写插入ES的逻辑。

客户端pom

<dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.client</artifactId>
            <version>1.1.5</version>
        </dependency>
        <!-- Message、CanalEntry.Entry等来自此安装包 -->
        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.protocol</artifactId>
            <version>1.1.5</version>
        </dependency>

ES处理器

@Component
@Slf4j
public class CanalClientHandler {

    @Resource
    private CodeNoteService noteService;

    @SneakyThrows
    public void run() {
        log.info("canal-client started");
        // 获取连接
        CanalConnector canalConnector = CanalConnectors.newSingleConnector(
                new InetSocketAddress("101.43.138.173", 11111), "example", "", "");
        // 连接
        canalConnector.connect();
        // 订阅数据库
        canalConnector.subscribe();
        while (true) {
            Thread.sleep(1000);
            // 获取数据
            Message message = canalConnector.get(100);
            // 获取Entry集合
            List<CanalEntry.Entry> entries = message.getEntries();
            // 判断集合是否为空
            if (CollectionUtil.isNotEmpty(entries)) {
                // 遍历entries,单条解析
                for (CanalEntry.Entry entry : entries) {
                    //1.获取表名
                    String tableName = entry.getHeader().getTableName();
                    //2.获取类型
                    CanalEntry.EntryType entryType = entry.getEntryType();
                    //3.获取序列化后的数据
                    ByteString storeValue = entry.getStoreValue();
                    //4.判断当前entryType类型是否为ROWDATA
                    if (CanalEntry.EntryType.ROWDATA.equals(entryType)) {
                        //5.反序列化数据
                        CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(storeValue);
                        //6.获取当前事件的操作类型
                        CanalEntry.EventType eventType = rowChange.getEventType();
                        //7.获取数据集
                        List<CanalEntry.RowData> rowDataList = rowChange.getRowDatasList();
                        //8.遍历rowDataList,并打印数据集
                        for (CanalEntry.RowData rowData : rowDataList) {
                            JSONObject beforeData = new JSONObject();
                            List<CanalEntry.Column> beforeColumnsList = rowData.getBeforeColumnsList();
                            for (CanalEntry.Column column : beforeColumnsList) {
                                beforeData.put(column.getName(), column.getValue());
                            }
                            JSONObject afterData = new JSONObject();
                            List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();
                            for (CanalEntry.Column column : afterColumnsList) {
                                afterData.put(column.getName(), column.getValue());
                            }
                            //数据打印
                            log.info("Table:" + tableName + ",EventType:" + eventType + ",Before:" + beforeData + ",After:" + afterData);
                            if (eventType.equals(CanalEntry.EventType.INSERT)){
                                noteService.saveNoteToEs(JSONObject.toJavaObject(afterData, EsCodeNote.class));
                            }else if (eventType.equals(CanalEntry.EventType.DELETE)){
                                noteService.delNoteFromEs(JSONObject.toJavaObject(beforeData, EsCodeNote.class));
                            }else if (eventType.equals(CanalEntry.EventType.UPDATE)){
                                noteService.updateNoteFromEs(JSONObject.toJavaObject(afterData, EsCodeNote.class));
                            }
                        }
                    }
                }
            }

        }
    }
}

监听到binlog

image.png

总结

以上就完成了MySql到ES的同步,其实这是异构数据同步的一种通用解决方案。

在生产环境,为了提高吞吐量,同时保证顺序性,会将同一条数据的增删改分到MQ的同一个队列或分区,供下游系统订阅消费。

本文没介绍到的canal-admincanal-adapter感兴趣的同学可以去官网学习。

最后再提一下我的小破站,大家可以去体验文中提到的全文搜索效果

小破站