Canal的搭建
一. mysql的配置
修改 my.ini 文件
mysqld]
character-set-server=utf8
basedir=E:\mysql\mysql-5.7.21-winx64
datadir=E:\mysql\mysql-5.7.21-winx64\data
# servier-id 是一个数字
server-id=2
# 开启mysql的binlog
log-bin=mysql-bin
# binlog记录的操作的格式
binlog-format=ROW
# 表示记录那个表,对于我们配置就是 es 这个库下的所有的表
binlog-do-db=es
修改配置之后,重启mysql
需要创建一个用户
canal/canal
, 命令如下:
# 授权
grant select, replication slave, replication client on *.* to 'canel'@'%' identified by 'canal';
# 让权限生效
flush privileges;
二. canal的使用
一. 下载地址:https://github.com/alibaba/canal/releases
二. canal必须在Linux系统上运行,首页加压
三. 修改
CANAL_HOME/conf/example/instance.properties
配置文件
四. 启动 canal, 进入到
CANAL_HOME/bin
, 执行如下命令
./startup.sh
三. Java程序连接Canal
3.1 引入依赖
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.4</version>
</dependency>
3.1 创建Canal的连接对象
/**
* DisposableBean 这个接口提供了一个 destroy() 方法, 如果我们某个 bean 实现了这个接口,
* 在bean被销毁的时候, 会自动执行这个方法, 通常的作用是用来释放资源.
* 对于我们目前的目的来说: 就是当该bean销毁的时候, 释放canal的连接用的.
*/
@Configuration
public class CanalConfig implements DisposableBean {
private CanalConnector canalConnector;
@Bean
public CanalConnector canalConnector() {
this.canalConnector = CanalConnectors.newSingleConnector(
new InetSocketAddress("192.168.52.137", 11111), // ip 和 端口号
"example", // 目标, 就是 CANAL_HOME/config/example
"canal", // 连接到 canal 的用户名
"canal"
);
// 正式建立连接
this.canalConnector.connect();
// 如果什么都不传入, 表示获取到 canal 拿到的所有记录; es.t_goods 表示我们该应用只获取 t_goods 的操作记录
// this.canalConnector.subscribe(".*\\..*"); // grant all on * . *
this.canalConnector.subscribe("es.t_goods");
// 回到上一次的拉去操作记录的位置
this.canalConnector.rollback();
// System.out.println(canalConnector);
return canalConnector;
}
@Override
public void destroy() throws Exception {
if(null != canalConnector) {
canalConnector.disconnect(); // 关闭连接
}
}
}
3.3 准实时获取数据的修改
通过定时器来完成的
@Component
public class SyncDataTask {
private CanalConnector canalConnector;
public SyncDataTask(CanalConnector canalConnector) {
this.canalConnector = canalConnector;
}
// num 表示每次从canal中获取多少个操作, 当然这个操作中包含有事务的命令.
private int num = 200;
/**
* fixedRate: 固定频率
* fixedDelay: 固定时间间隔
*/
@Scheduled(fixedDelay = 100)
public void read() {
// 每次获取 200 条操作, 统一被 Canal 封装成一个 Message
Message message = canalConnector.getWithoutAck(num);
// 获取当前所拉去的数据的第一个操作的id, 如果没有操作返回 -1
long batchId = message.getId();
if(batchId != -1 && message.getEntries().size() > 0) {
/**
* CanalEntry.Entry 指的是一个个操作, 一个操作中可能影响多条数据
*/
List<CanalEntry.Entry> entries = message.getEntries();
for(Entry entry : entries) {
/**
* 如果这条操作是事务的开启或者回滚操作. begin commit(rollback)
*/
if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
continue;
}
try {
/**
* RowChange 就表示用户的这条操作记录的信息
*/
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
// 每个操作的操作类型, 可能是 Select、insert 、delete、update
EventType eventType = rowChange.getEventType();
// rowChange.getRowDatasList() 表示的意思, 是当前这个操作所影响所有的数据行
// 因为delete或者update, 可能改变了很多条记录
for(RowData rowData : rowChange.getRowDatasList()) {
processRowData(rowData, eventType);
}
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
}
}
}
/**
* 对于删除操作, 只存在之前的数据.
* 对于插入操作, 只存在之后的数据.
* 对于更新操作, 只存在之后、之后的数据.
*/
private void processRowData(RowData rowData, EventType eventType) {
if(EventType.DELETE == eventType) { // 表示删除操作
System.out.println("删除操作");
// getBeforeColumnsList() 拿到的是一条记录中 一列 一列的
List<CanalEntry.Column> columns = rowData.getBeforeColumnsList();
printInfo(columns);
}else if(EventType.INSERT == eventType) {
List<CanalEntry.Column> columns = rowData.getAfterColumnsList();
printInfo(columns);
}else if(EventType.UPDATE == eventType) {
List<CanalEntry.Column> columns = rowData.getAfterColumnsList();
printInfo(columns);
}
}
private void printInfo(List<CanalEntry.Column> columns) {
for(Column col : columns) {
System.out.println(col.getName() + "=" + col.getValue());
}
}
}
四. 自定义starter
自定义starter的目的,就是作为一个 “轮子” 可以被任何人拿来直接使用。自定义starter的流程:
- 不是作为一个工程来使用了,所以不能引入入
spring-boot-starter-web
- 包名要保持独立,不能和项目的包是一样的。原因在于,如果包名,一致那么springboot在启动的过程可以直接扫描到了,就不具备了可移植。报名不一致,就可用通过自动装配的方式来进行加载。
- 在
resources
目录下创建一个文件夹META-INF
, 然后在下面新建一个文件spring.factories
, 文件的内容跟properties
有些类似(都是键值对),不一样在于文件中可以有多个值,使用逗号分隔。说明,文件中\
是换行,是为了兼容考虑,这种写法在window
和Linux
都是兼容的。spring.factories
文件中的键org.springframework.boot.autoconfigure.EnableAutoConfiguration
, 值就是整个装配的入口类。- 当一个springboot工程引入该starter,启动的时候,会扫描 Jar包下
META-INF/spring.factories
文件,然后加载EnableAutoConfiguration
对应的值,放到IOC容器中。
4.1 引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.4</version>
</dependency>
</dependencies>
4.2 需要配置spring.factories
文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.canal.autoconfig.CanalAutoConfig
4.3 编写自动装配的类
@Configuration
@EnableConfigurationProperties(CanalProperties.class)
public class CanalAutoConfig {
private CanalConnector canalConnector;
@Autowired
private CanalProperties canalProperties;
/**
* ConditionalOn -> 当 什么 什么 的时候
* @ConditionalOnMissingBean(CanalConnector.class) 表示IOC容器中如果不存在
* CanalConnector的实例的bean的时候, 那么我们这个Bean才生效, 换句话来说,就是有可能
* 用户自己定义 CanalConnector 的实例的时候, 我们这里定义的bean是无效的.
*/
@Bean
@ConditionalOnMissingBean(CanalConnector.class)
public CanalConnector canalConnector() {
this.canalConnector = CanalConnectors.newSingleConnector(
new InetSocketAddress(canalProperties.getHost(), canalProperties.getPort()), // ip 和 端口号
canalProperties.getInstanceName(), // 目标, 就是 CANAL_HOME/config/example
canalProperties.getUsername(), // 连接到 canal 的用户名
canalProperties.getPassword()
);
this.canalConnector.connect();
this.canalConnector.subscribe(canalProperties.getSubscribe());
this.canalConnector.rollback();
return canalConnector;
}
@Bean
public CanalListenerProcessor canalListenerProcessor() {
return new CanalListenerProcessor();
}
}
4.4 BeanPostProcessor
Post 在 spring 中有特殊的方法, 凡事方法中带有 Post 都是表示 “在什么之后.” .
Spring中有两类接口非常特殊:
- XXXXXPostProcessor: BeanPostProcessor、BeanFactoryPostProcessor
- XXXXXAware: ApplicationContextAware
// 该接口是Bean的后置处理,当某个Bean被纳入到IOC容器中就是,那么对应的两个方法都会得到执行,
// 并且会将刚刚纳入到IOC容器的bean, 作为两个方法的参数传人进去
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
// 我们在自定义注解,或者对bean进行增强的时候,会调用这个方法。
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}