安装包下载地址:https://github.com/alibaba/canal/releases
1、MySQL配置
1、1开启mysql的log_bin
canal是基于MySQL数据库增量日志解析的
在mysql中执行
show variables like '%log_bin%';
正常是这样的
如果未开启则需要mysql的my.ini文件,使用的版本是mysql-5.7.38
[mysqld]
# 选择ROW(行)模式
binlog-format=row
# 打开binlog
log_bin=mysql-bin
1.2、创建一个专用的用户
-- 使用命令登录:mysql -u root -p
-- 创建用户 用户名:canal 密码:Canal@123456
create user 'canal'@'%' identified by 'Canal@123456';
-- 授权 *.*表示所有库
grant SELECT, REPLICATION SLAVE, REPLICATION CLIENT on *.* to 'canal'@'%' identified by 'Canal@123456';
2、安装Canal
2.1、解压文件启动服务canal\bin下startup.bat
conf/canal.properties
配置不需要数据同步的库、表黑名单canal\conf\example下instance.properties
# table regex .*\\..*
#canal.instance.filter.regex=test_01.login_record
canal.instance.filter.regex=
# table black regex
# 过滤aaa开头的库;bbb表
canal.instance.filter.black.regex=aaa\\..*,.*\\bbb.*
# table field filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)
#canal.instance.filter.field=test1.t_product:id/subject/keywords,test2.t_company:id/name/contact/ch
# table field black filter(format: schema1.tableName1:field1/field2,schema2.tableName2:field1/field2)
#canal.instance.filter.black.field=test1.t_product:subject/product_image,test2.t_company:id/name/contact/ch
3、集成到项目中
3.1、配置yml文件
canal:
ip: 127.0.0.1
port: 11111
username: canal
password: canal
destination: example
batch-size: 1500
subscribe: .*\..*
添加依赖
<!--canal-->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.6</version>
</dependency>
<!--我这边不手动引入会报找不到com.alibaba.otter.canal.protocol.Message-->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.protocol</artifactId>
<version>1.1.6</version>
</dependency>
创建canal服务的文件夹
1、CanalRunClient启动类
package xxx.canal.client;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.Message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import seata.canal.message.MessageHandler;
import javax.annotation.Resource;
import java.net.InetSocketAddress;
/**
* @Description: CanalTask启动类
* @Author: yyl
* @Date: 2022/7/13
*/
@Component
@Slf4j
public class CanalRunClient implements ApplicationRunner {
@Value("${canal.ip}")
private String ip;
@Value("${canal.port}")
private Integer port;
@Value("${canal.username}")
private String username;
@Value("${canal.password}")
private String password;
@Value("${canal.destination}")
private String destination;
@Value("${canal.batch-size}")
private Integer batchSize;
@Value("${canal.subscribe}")
private String subscribe;
@Resource
MessageHandler messageHandler;
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("----->>>>>>>>启动canal");
startCanal();
}
private void startCanal() {
// 创建链接
CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(ip, port), destination, username, password);
try {
//打开连接
connector.connect();
//订阅数据库表,全部表
connector.subscribe(subscribe);
// connector.subscribe("seata_account\\..*");
//回滚到未进行ack的地方,下次fetch的时候,可以从最后一个没有ack的地方开始
connector.rollback();
while (true) {
//获取指定数量的数据
Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
//获取批量ID
long batchId = message.getId();
//获取批量的数量
int size = message.getEntries().size();
//如果没有数据
if (batchId == -1 || size == 0) {
try {
//现成休眠1s
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//如果有数据,处理数据
messageHandler.handler(message);
}
connector.ack(batchId); // 提交确认
// connector.rollback(batchId); // 处理失败, 回滚数据
}
} finally {
connector.disconnect();
}
}
}
2、AbstractEntryHandler
package xxx.canal.handler;
import com.alibaba.otter.canal.protocol.CanalEntry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Description: 获取到数据后进行相应的处理
* @Author: yyl
* @Date: 2022/7/13
*/
@Service
@Slf4j
public class AbstractEntryHandler {
public final void handler(CanalEntry.Entry entry) {
CanalEntry.RowChange rowChage = null;
try {
rowChage = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(), e);
}
CanalEntry.EventType eventType = rowChage.getEventType();
boolean isDdl = rowChage.getIsDdl();
log.info("----------库名:{}--------表名:{}--------", entry.getHeader().getSchemaName(), entry.getHeader().getTableName());
String operation = null;
Map<String, String> map = new HashMap<>();
switch (eventType) {
case INSERT:
rowChage.getRowDatasList().forEach(rowData -> {
List<CanalEntry.Column> columns = rowData.getAfterColumnsList();
for (CanalEntry.Column column : columns) {
map.put(camelName(column.getName()), column.getValue());
}
});
operation = "添加";
break;
case UPDATE:
rowChage.getRowDatasList().forEach(rowData -> {
List<CanalEntry.Column> columns = rowData.getAfterColumnsList();
for (CanalEntry.Column column : columns) {
map.put(camelName(column.getName()), column.getValue());
}
Map<String, String> map1 = new HashMap<>();
List<CanalEntry.Column> columns1 = rowData.getBeforeColumnsList();
for (CanalEntry.Column column : columns1) {
map1.put(camelName(column.getName()), column.getValue());
}
log.info("---------更新之前map={}----------", map1);
});
operation = "更新";
break;
case DELETE:
rowChage.getRowDatasList().forEach(rowData -> {
List<CanalEntry.Column> columns = rowData.getBeforeColumnsList();
for (CanalEntry.Column column : columns) {
map.put(camelName(column.getName()), column.getValue());
}
});
operation = "删除";
break;
default:
break;
}
log.info("---------操作:{},数据={}----------", operation, map);
}
/**
* 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。</br>
* 例如:HELLO_WORLD->HelloWorld
*
* @param name 转换前的下划线大写方式命名的字符串
* @return 转换后的驼峰式命名的字符串
*/
public static String camelName(String name) {
StringBuilder result = new StringBuilder();
// 快速检查
if (name == null || name.isEmpty()) {
// 没必要转换
return "";
} else if (!name.contains("_")) {
// 不含下划线,仅将首字母小写
return name.substring(0, 1).toLowerCase() + name.substring(1);
}
// 用下划线将原始字符串分割
String camels[] = name.split("_");
for (String camel : camels) {
// 跳过原始字符串中开头、结尾的下换线或双重下划线
if (camel.isEmpty()) {
continue;
}
// 处理真正的驼峰片段
if (result.length() == 0) {
// 第一个驼峰片段,全部字母都小写
result.append(camel.toLowerCase());
} else {
// 其他的驼峰片段,首字母大写
result.append(camel.substring(0, 1).toUpperCase());
result.append(camel.substring(1).toLowerCase());
}
}
return result.toString();
}
}
3、MessageHandler
package xxx.canal.message;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import seata.canal.handler.AbstractEntryHandler;
import javax.annotation.Resource;
import java.util.List;
@Service
@Slf4j
public class MessageHandler {
@Resource
private AbstractEntryHandler abstractEntryHandler;
public void handler(Message message) {
List<CanalEntry.Entry> entries = message.getEntries();
for (CanalEntry.Entry entry : entries) {
if (entry.getEntryType().equals(CanalEntry.EntryType.ROWDATA)) {
log.info("----->>>>>>>开始处理CanalEntry");
abstractEntryHandler.handler(entry);
}
}
}
}
正常启动后conf\example文件夹下会多出h2.mv.db和meta.dat