哈喽,大家好,我是一条。
书接上文,在《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
总结
以上就完成了MySql到ES的同步,其实这是异构数据同步的一种通用解决方案。
在生产环境,为了提高吞吐量,同时保证顺序性,会将同一条数据的增删改分到MQ的同一个队列或分区,供下游系统订阅消费。
本文没介绍到的canal-admin和canal-adapter感兴趣的同学可以去官网学习。
最后再提一下我的小破站,大家可以去体验文中提到的全文搜索效果