主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费
基于日志增量订阅和消费的业务包括:
1.数据库镜像
2.数据库实时备份
3.索引构建和实时维护(拆分异构索引、倒排索引等)
4.业务 cache 刷新
5.带业务逻辑的增量数据处理
当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x
canal 工作原理:
修改mysql my.cnf文件
[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
(因为cancal是作为从库监听主库)
查看Binlog开启状态,登陆mysql后: ON为开启
show variables like '%log_bin%';
adapter: canal自带的适配器,通过配置修改就可以实现向MQ、DB、ES中同步数据
admin: canal的WEB面板,为canal提供整体配置管理、节点运维等面向运维的功能
deployer: canal服务端,也就是canal监听数据源的服务
example:这是客户端代码的demo例子
修改解压后conf/example 下的 instance.properties 文件:
## mysql serverId 不要和监听的mysql server_id一样
canal.instance.mysql.slaveId = 1234
## 需要改成自己的数据库地址
canal.instance.master.address = 127.0.0.1:3306
# 这三个配置和同步信息位置有关 增量同步不需要管
#canal.instance.master.journal.name =
#canal.instance.master.position =
#canal.instance.master.timestamp =
#username/password,需要改成自己的数据库信息
canal.instance.dbUsername = canal
canal.instance.dbPassword = canal
canal.instance.defaultDatabaseName =
canal.instance.connectionCharset = UTF-8
#这个是监听的库表Perl正则表达式 (可以选择性监听需要的库表)
#也可以不配,在代码中设置(两边都配了,则以代码的配置为准)
canal.instance.filter.regex = .\*\\\\..\*
常见例子:
1. 所有表:.* or .*\\..*
2. canal schema下所有表: canal\\..*
3. canal下的以canal打头的表:canal\\.canal.*
4. canal schema下的一张表:canal.test1
5. 多个规则组合使用:canal\\..*,mysql.test1,mysql.test2 (逗号分隔)
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.protocol</artifactId>
<version>1.1.5</version>
</dependency>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.5</version>
</dependency>
// 主逻辑代码
public void canalClient(){
// 地址、端口、example默认就好(就是instance.properties文件所在文件夹名称)
CanalConnector connector = CanalConnectors.newSingleConnector(new
InetSocketAddress("192.168.1.105",
11111), "example", "", "");
int batchSize = 1000;
try {
connector.connect();
connector.subscribe("test\\..*"); // 监听的库表Perl正则表达式(我这里是test库下所有表)
connector.rollback();
try {
while (true) {
//尝试从master那边拉去数据batchSize条记录,有多少取多少
Message message = connector.getWithoutAck(batchSize);
long batchId = message.getId();
int size = message.getEntries().size();
if (batchId == -1 || size == 0) {
Thread.sleep(1000); //没有数据则等待1s
} else {
dataHandle(message.getEntries()); //数据处理
}
connector.ack(batchId);
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (InvalidProtocolBufferException e) {
connector.connect();
e.printStackTrace();
}
} finally {
connector.disconnect();
}
}
/**
* 数据处理
*
* @param entrys
*/
private void dataHandle(List<Entry> entrys) throws
InvalidProtocolBufferException {
for (Entry entry : entrys) {
if (EntryType.ROWDATA == entry.getEntryType()) {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
EventType eventType = rowChange.getEventType();
if (eventType == EventType.DELETE) {
saveDeleteSql(entry);
} else if (eventType == EventType.UPDATE) {
saveUpdateSql(entry);
} else if (eventType == EventType.INSERT) {
saveInsertSql(entry);
}
}
}
}
/**
* 保存更新语句
*
* @param entry
*/
private void saveUpdateSql(Entry entry) {
try {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
List<RowData> rowDatasList = rowChange.getRowDatasList();
for (RowData rowData : rowDatasList) {
List<Column> newColumnList = rowData.getAfterColumnsList();
Map<String, String> data = new HashMap<>();
newColumnList.stream().forEach(item -> {
if(StringUtils.isNotEmpty(item.getValue())){
data.put(lineToHump(item.getName()), item.getValue());
}
});
List<Column> oldColumnList = rowData.getBeforeColumnsList();
CanalVO update = CanalVO.ok(entry.getHeader().getTableName(), "UPDATE", data, oldColumnList.get(0).getValue());
System.out.println("更新返回 : " + JSON.toJSONString(update));
}
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
}
/**
* 保存删除语句
*
* @param entry
*/
private void saveDeleteSql(Entry entry) {
try {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
List<RowData> rowDatasList = rowChange.getRowDatasList();
for (RowData rowData : rowDatasList) {
List<Column> oldColumnList = rowData.getBeforeColumnsList();
CanalVO delete = CanalVO.ok(entry.getHeader().getTableName(), "DELETE", null, oldColumnList.get(0).getValue());
System.out.println("删除返回 : " + JSON.toJSONString(delete));
}
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
}
/**
* 保存插入语句
*
* @param entry
*/
private void saveInsertSql(Entry entry) {
try {
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
List<RowData> rowDatasList = rowChange.getRowDatasList();
for (RowData rowData : rowDatasList) {
List<Column> columnList = rowData.getAfterColumnsList();
Map<String, String> data = new HashMap<>();
columnList.stream().forEach(item -> {
if(StringUtils.isNotEmpty(item.getValue())){
data.put(lineToHump(item.getName()), item.getValue());
}
});
CanalVO insert = CanalVO.ok(entry.getHeader().getTableName(), "INSERT", data, null);
System.out.println("插入返回 : " + JSON.toJSONString(insert));
}
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
}
/**
* 下划线转驼峰
*
* @param str
*/
private static Pattern linePattern = Pattern.compile("_(\\w)");
public static String lineToHump(String str) {
str = str.toLowerCase();
Matcher matcher = linePattern.matcher(str);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
}
matcher.appendTail(sb);
return sb.toString();
}
// CanalVO 如下:
public class CanalVO {
private String tableName; // 表名
private String type; // 类型(更新、删除、插入)
private Map<String,String> data; // 数据JSON 自己转对应表格实体类
private String id; // 更新或删除都是根据ID来
public static CanalVO ok(String tableName,String type,Map<String,String> data,String id){
CanalVO canalVO=new CanalVO();
canalVO.setId(id);
canalVO.setTableName(tableName);
canalVO.setType(type);
canalVO.setData(data);
return canalVO;
}
public String getTableName() {
return tableName;
}
public void setTableName(String tableName) {
this.tableName = tableName;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Map<String, String> getData() {
return data;
}
public void setData(Map<String, String> data) {
this.data = data;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
# 配置log文件与log位点 或者 配置时间戳
#bin-log文件 与log位点构成要开始同步的位置
canal.instance.master.journal.name = mysql-bin.000009 //从000009 bin-log文件0位点开始同步
#bin-log位点
canal.instance.master.position = 0
#指定一个时间戳,canal会自动遍历mysql binlog,找到对应时间戳的binlog位点后,进行启动
canal.instance.master.timestamp =
注意
1.修改同步位置时,如果该目录下已生成meta.dat文件,要把这个删掉,因为这个文件记录了已经同步的位置
2.bin-log文件一定要是存在的,这个可以去mysql bin-log文件生成地方去确认,不然example的log会报找不到bin-log文件的错误
1. 两个相同配置客户端服务同时连接了同一个服务端会怎样?
只有一个服务能收到消息
2.一个服务端有没有办法同时监听不同环境的数据库?
可以,上述的exmaple文件夹就等于是一个数据库,如果要监听多个可以把这个文件夹复制一份,同时修改instance.properties文件配置,注意要删除meta.dat,同时要修改conf/canal.properties,找到destinations配置处
修改canal.destinations 多个用,隔开
列如: canal.destinations = example1,example2
上述只是一个非常简单的demo列子,实际中我们可以通过工厂优雅的处理不同表的各种消息,或者直接交给spring容器管理,我们只需要关注具体的业务数据处理即可,更多的还可以去看canal官方使用指南