canal是什么
摘自项目github
基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了mysql
原理相对比较简单:
canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
mysql master收到dump请求,开始推送binary log给slave(也就是canal)
canal解析binary log对象(原始为byte流)
canal解决了什么问题
起源:
早期,阿里巴巴B2B公司因为存在杭州和美国双机房部署,存在跨机房同步的业务需求。不过早期的数据库同步业务,主要是基于trigger的方式获取增量变更,不过从2010年开始,阿里系公司开始逐步的尝试基于数据库的日志解析,获取增量变更进行同步,由此衍生出了增量订阅&消费的业务,从此开启了一段新纪元。
本文在探讨什么
canal的基础操作,本文不再赘述,可参考github上的quick start和client api,包含了demo
本文探讨的是,基于canal的流式api的消息分发,以及如何防止消息丢失和重复处理
什么是流式API
摘自项目github
流式api设计:
image
- 每次get操作都会在meta中产生一个mark,mark标记会递增,保证运行过程中mark的唯一性
- 每次的get操作,都会在上一次的mark操作记录的cursor继续往后取,如果mark不存在,则在last ack cursor继续往后取
- 进行ack时,需要按照mark的顺序进行数序ack,不能跳跃ack. ack会删除当前的mark标记,并将对应的mark位置更新为last ack cursor
- 一旦出现异常情况,客户端可发起rollback情况,重新置位:删除所有的mark, 清理get请求位置,下次请求会从last ack cursor继续往后取
image.png
关注点
异步的ack带来了更好的性能,也带来了一些问题
rollback后,mark会清空,回到上次ack的位置。如果get的速度比ack快,当rollback()后,就会出现重复消息
本文针对这个问题,给出一个较为简单的解决方案
思路
mark清空后,再次get,获取到的batchId会继续递增(保存在服务端),但是消息是已经处理过的,此时我们不希望消息继续被分发或者处理
如何判断消息是否消费过,或者说,该次数据库变更,是否已经解析过
1.在业务上进行判断
2.更好的方式,通过当前处理的消息在binlog中的位置进行判断
String logFileName = entry.getHeader().getLogfileName();
long offset = entry.getHeader().getLogfileOffset();
使用这两行代码,就可以方便的获取当前消息在具体哪个binlog文件的哪个位置,输出类似如下
logfileName = mysql-bin.000001,offset = 41919
代码
测试代码,请勿用于生产
public static void main(String args[]) {
new Thread(SimpleCanalClient::receiver).start();
new Thread(SimpleCanalClient::ack).start();
}
开局启动两个线程,一个接收,一个确认
private static void receiver() {
// 创建链接
CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(AddressUtils.getHostIp(),
11111), "example", "", "");
int batchSize = 1;
int count = 0;
try {
connector.connect();
connector.subscribe(".*\\..*");
connector.rollback();
int total = 20;
while (count < total) {
count++;
Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
long batchId = message.getId();
int size = message.getEntries().size();
if (batchId != -1 && size != 0) {
printEntry(message.getEntries(),batchId);
}
try {