canal 原理【官方文档】
简介
canal [kə’næl],译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费
早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务
基于日志增量订阅和消费的业务包括
数据库镜像
数据库实时备份
索引构建和实时维护(拆分异构索引、倒排索引等)
业务 cache 刷新
带业务逻辑的增量数据处理
当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x
MySQL主备复制原理
MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看)
MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log)
MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据
canal 工作原理
canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
canal 解析 binary log 对象(原始为 byte 流)
java语言使用【需要启动项目再启动redis后才能进行测试】
配置MySQL【MySQL的my.ini或my.cnf】
# 开启mysql的binlog模块
log-bin=mysql-bin
binlog-format=ROW
# server_id需保证唯一,不能和canal的slaveId重复【canal 1.1.4+版本后无关紧要,canal中的slaveId无需配置,增加了自增长机制】
server_id=6
# 需要同步的数据库名称【无此配置指定所有】
# binlog-do-db=demo
# 忽略的数据库,建议填写
# binlog-ignore-db=mysql
授权
mysql -uroot -p
##create user userName identified by 'password';【不指定默认为@'localhost'】
##create user userName@'localhost' identified by 'password';【只能本地访问】
##create user userName@'%' identified by 'password'【允许远程访问】
CREATE USER canal@'%' IDENTIFIED BY 'canal';
GRANT ALL PRIVILEGES ON demo.user TO 'canal'@'%'
FLUSH PRIVILEGES;
wget https://github.com/alibaba/canal/releases/download/canal-1.1.4/canal.deployer-1.1.4.tar.gz
cd /usr
mkdir canal
tar zxvf canal.deployer-1.1.4.tar.gz -C ./canal
canal.properties可保持不变,默认端口11111
instance.properties配置如下【如果配置用户名为canal不行直接用root吧,不行的原因一般是因为没有配置该用户允许远程访问】
启动canal: ./bin/startup.sh
查看是否正常启动成功,需要查看两个日志文件:logs/canal/canal.log 和logs/example/example.log。
**logs/canal/canal.log**文件中有如下内容:the canal server is running now ......
[main] INFO com.alibaba.otter.canal.deployer.CanalLauncher - ## start the canal server.
[main] INFO com.alibaba.otter.canal.deployer.CanalController - ## start the canal
[main] INFO com.alibaba.otter.canal.deployer.CanalLauncher - ## the canal server is running now ......
**logs/example/example.log**文件中有如下内容:start successful....
[main] INFO c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [canal.properties]
[main] INFO c.a.o.c.i.spring.support.PropertyPlaceholderConfigurer - Loading properties file from class path resource [example/instance.properties]
[main] INFO c.a.otter.canal.instance.spring.CanalInstanceWithSpring - start CannalInstance for 1-example
[main] INFO c.a.otter.canal.instance.core.AbstractCanalInstance - start successful....
证明启动成功
Java的canal客户端和jedis
redis.clients
jedis
3.0.1
com.alibaba.otter
canal.client
1.1.4
redisutils
public class RedisUtil {
private static Jedis jedis = null;
public static synchronized Jedis getJedis() {
if (jedis == null) {
jedis = new Jedis("redis IP", 6379);
jedis.auth("redis password");
}
return jedis;
}
public static boolean existKey(String key) {
return getJedis().exists(key);
}
public static void delKey(String key) {
getJedis().del(key);
}
public static String stringGet(String key) {
return getJedis().get(key);
}
public static String stringSet(String key, String value) {
return getJedis().set(key, value);
}
public static void hashSet(String key, String field, String value) {
getJedis().hset(key, field, value);
}
}
CanalClient
public class CanalClient {
public static void main(String args[]) {
CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("部署canal机器IP",
11111), "example", "", "");
int batchSize = 100;
try {
connector.connect();
connector.subscribe(".*\\..*");
connector.rollback();
while (true) {
// 获取指定数量的数据
Message message = connector.getWithoutAck(batchSize);
long batchId = message.getId();
int size = message.getEntries().size();
System.out.println("batchId = " + batchId);
System.out.println("size = " + size);
if (batchId == -1 || size == 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
printEntry(message.getEntries());
}
// 提交确认
connector.ack(batchId);
// connector.rollback(batchId); // 处理失败, 回滚数据
}
} finally {
connector.disconnect();
}
}
private static void printEntry(List entrys) {
for (Entry entry : entrys) {
if (entry.getEntryType() == EntryType.TRANSACTIONBEGIN || entry.getEntryType() == EntryType.TRANSACTIONEND) {
continue;
}
RowChange rowChage = null;
try {
rowChage = RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
e);
}
EventType eventType = rowChage.getEventType();
System.out.println(String.format("================> binlog[%s:%s] , name[%s,%s] , eventType : %s",
entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),
eventType));
for (RowData rowData : rowChage.getRowDatasList()) {
if (eventType == EventType.DELETE) {
redisDelete(rowData.getBeforeColumnsList());
} else if (eventType == EventType.INSERT) {
redisInsert(rowData.getAfterColumnsList());
} else {
System.out.println("-------> before");
printColumn(rowData.getBeforeColumnsList());
System.out.println("-------> after");
redisUpdate(rowData.getAfterColumnsList());
}
}
}
}
private static void printColumn(List columns) {
for (Column column : columns) {
System.out.println(column.getName() + " : " + column.getValue() + " update=" + column.getUpdated());
}
}
private static void redisInsert(List columns) {
JSONObject json = new JSONObject();
for (Column column : columns) {
json.put(column.getName(), column.getValue());
}
if (columns.size() > 0) {
RedisUtil.stringSet("user:" + columns.get(0).getValue(), json.toJSONString());
}
}
private static void redisUpdate(List columns) {
JSONObject json = new JSONObject();
for (Column column : columns) {
json.put(column.getName(), column.getValue());
}
if (columns.size() > 0) {
RedisUtil.stringSet("user:" + columns.get(0).getValue(), json.toJSONString());
}
}
private static void redisDelete(List columns) {
JSONObject json = new JSONObject();
for (Column column : columns) {
json.put(column.getName(), column.getValue());
}
if (columns.size() > 0) {
RedisUtil.delKey("user:" + columns.get(0).getValue());
}
}
}
基于alibaba canal-admin webUI后台管理canal与MySQL实例的关系【后台运维,无需操作linux】
注意:上述的canal-server已经搭建好了的情况下
下载canal-admin
mkdir /usr/cancal
cd /usr/cancal
wget https://github.com/alibaba/canal/releases/download/canal-1.1.4/canal.admin-1.1.4.tar.gz
解压
mkdir canal-admin
tar zxvf canal.admin-1.1.4.tar.gz -C ./canal-admin/
解压后的内容
[root@whotw cancal]# cd canal-admin/
[root@whotw canal-admin]# ls
bin conf lib logs
[root@whotw canal-admin]# cd conf
[root@whotw conf]# ls
application.yml canal_manager.sql canal-template.properties instance-template.properties logback.xml public
修改yml信息【解压后的文件操作】
vi application.yml
server:
port: 8089
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
spring.datasource:
address: 数据库ip:3306
database: canal_manager
username: canal
password: canal
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://${spring.datasource.address}/${spring.datasource.database}?useUnicode=true&characterEncoding=UTF-8&useSSL=false
hikari:
maximum-pool-size: 30
minimum-idle: 1
canal:
adminUser: admin
adminPasswd: admin
配置MySQL bin-log【上述配置canal时配置好了无需配置】
[mysqld]
log-bin=mysql-bin #添加这一行就ok
binlog-format=ROW #选择row模式
server_id=6 #配置mysql replaction需要定义,不能和canal的slaveId重复
创建canal用户【上述配置canal时配置好了无需配置】
初始化数据库【MySQL导入SQL操作】
conf/canal_manager.sql
启动canal
sh bin/startup.sh
查看 admin 日志
cat logs/admin.log
2019-08-31 15:43:38.162 [main] INFO o.s.boot.web.embedded.tomcat.TomcatWebServer - Tomcat initialized with port(s): 8089 (http)
2019-08-31 15:43:38.180 [main] INFO org.apache.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8089"]
2019-08-31 15:43:38.191 [main] INFO org.apache.catalina.core.StandardService - Starting service [Tomcat]
2019-08-31 15:43:38.194 [main] INFO org.apache.catalina.core.StandardEngine - Starting Servlet Engine: Apache Tomcat/8.5.29
....
2019-08-31 15:43:39.789 [main] INFO o.s.w.s.m.m.annotation.ExceptionHandlerExceptionResolver - Detected @ExceptionHandler methods in customExceptionHandler
2019-08-31 15:43:39.825 [main] INFO o.s.b.a.web.servlet.WelcomePageHandlerMapping - Adding welcome page: class path resource [public/index.html]
此时代表canal-admin已经启动成功,可以通过 http://127.0.0.1:8089/ 访问【如下图】,默认密码:admin/123456
sh bin/stop.sh
下面开始进行配置【注意本人搭建的是单机版的,集群搭建可以自己去研究】