本文介绍如何应用Canal实现异步、解耦的架构,后续有空再写文章分析Canal原理和源代码。

Canal简介

Canal是用来获取数据库变更的中间件。

伪装自己为MySQL从库,拉取主库binlog并解析、处理。处理结果可发送给MQ,方便其他服务获取数据库变更消息,这一点非常有用。下面介绍一些典型用途。

其中,Canal+MQ作为一个整体,从外界看来就是一个数据管道服务服务,如下图。

Canal典型用途

异构数据(如ES、HBase、不同路由key的DB)

通过Canal自带的adapter,同步异构数据至ES、HBase,而不用自行实现繁琐的数据转换、同步操作。这里的adapter就是典型的适配器模式,把数据转成相应格式,并写入异构的存储系统。

当然,也可以同步数据至DB,甚至构建一份按不同字段分片路由的数据库。

比如:下单时按用户id分库分表订单记录,然后借助Canal数据通道,构建一份按商家id分库分表的订单记录,用于B端业务(如商家查询自己接到哪些订单)。

缓存刷新

缓存刷新的常规做法是,先更新DB,再删除缓存,再延迟删除(即cache-aside pattern+延迟双删),这种多步操作可能失败,而且实现相对复杂。借助Canal刷新缓存,使主服务、主流程无需关心缓存更新等一致性问题,保证最终一致性。

价格变化等重要业务消息

下游服务可立即感知价格变化。

常规做法是,先修改价格,再发出消息,此处的难点是要保证消息一定发送成功,以及如果发送不成功时如何处理。借助Canal,不用在业务层面担心消息丢失的问题。

数据库迁移

  • 多机房数据同步
  • 拆库

    虽然可以自己在代码中实现双写逻辑,然后对历史数据做处理,但是历史数据也可能被更新,需要不断迭代对比、更新,总之很复杂。

实时对账

常规做法是定时任务跑对账逻辑,时效性低,不能及时发现不一致问题。借助Canal,可实时触发对账逻辑。

大致流程如下:

  • 接收数据变更消息
  • 写入hbase作为流水记录
  • 一段窗口时间过后,触发比较与对端数据做比较

Canal客户端demo代码分析

以下示例是客户端连接Canal的例子,修改自官方github示例,楼主做了一些优化,并且在关键代码行中加入了注释。如果Canal把数据变更消息发送至MQ,写法有所不同,不同之处只是一个是订阅Canal,一个是订阅MQ,但是解析和处理逻辑基本相同。

`

public void process() {
// 每批次处理的条数
int batchSize = 1024;
while (running) {
try {
// 连上Canal服务
connector.connect();
// 订阅数据(比如某个表)
connector.subscribe("table_xxx");
while (running) {
// 批量获取数据变更记录
Message message = connector.getWithoutAck(batchSize);
long batchId = message.getId();
int size = message.getEntries().size();
if (batchId == -1 || size == 0) {
// 非预期情况,需做异常处理
} else {
// 打印数据变更明细
printEntry(message.getEntries());
} if (batchId != -1) {
// 使用batchId做ack操作:表明该批次处理完成,更新Canal侧消费进度
connector.ack(batchId);
}
}
} catch (Throwable e) {
logger.error("process error!", e);
try {
Thread.sleep(1000L);
} catch (InterruptedException e1) {
// ignore
} // 处理失败, 回滚进度
connector.rollback();
} finally {
// 断开连接
connector.disconnect();
}
}
} private void printEntry(List<Entry> entrys) {
for (Entry entry : entrys) {
long executeTime = entry.getHeader().getExecuteTime();
long delayTime = new Date().getTime() - executeTime;
Date date = new Date(entry.getHeader().getExecuteTime());
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 只关心数据变更的类型
if (entry.getEntryType() == EntryType.ROWDATA) {
RowChange rowChange = null;
try {
// 解析数据变更对象
rowChange = RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
throw new RuntimeException("parse event has an error , data:" + entry.toString(), e);
} EventType eventType = rowChange.getEventType(); logger.info(row_format,
new Object[] { entry.getHeader().getLogfileName(),
String.valueOf(entry.getHeader().getLogfileOffset()), entry.getHeader().getSchemaName(),
entry.getHeader().getTableName(), eventType,
String.valueOf(entry.getHeader().getExecuteTime()), simpleDateFormat.format(date),
entry.getHeader().getGtid(), String.valueOf(delayTime) }); // 不关心查询,和DDL变更
if (eventType == EventType.QUERY || rowChange.getIsDdl()) {
logger.info("ddl : " + rowChange.getIsDdl() + " , sql ----> " + rowChange.getSql() + SEP);
continue;
} for (RowData rowData : rowChange.getRowDatasList()) {
if (eventType == EventType.DELETE) {
// 数据变更类型为 删除 时,打印变化前的列值
printColumn(rowData.getBeforeColumnsList());
} else if (eventType == EventType.INSERT) {
// 数据变更类型为 插入 时,打印变化后的列值
printColumn(rowData.getAfterColumnsList());
} else {
// 数据变更类型为 其他(即更新) 时,打印变化前后的列值
printColumn(rowData.getBeforeColumnsList());
printColumn(rowData.getAfterColumnsList());
}
}
}
}
} // 打印列值
private void printColumn(List<Column> columns) {
for (Column column : columns) {
StringBuilder builder = new StringBuilder();
try {
if (StringUtils.containsIgnoreCase(column.getMysqlType(), "BLOB")
|| StringUtils.containsIgnoreCase(column.getMysqlType(), "BINARY")) {
// get value bytes
builder.append(column.getName() + " : "
+ new String(column.getValue().getBytes("ISO-8859-1"), "UTF-8"));
} else {
builder.append(column.getName() + " : " + column.getValue());
}
} catch (UnsupportedEncodingException e) {
}
builder.append(" type=" + column.getMysqlType());
if (column.getUpdated()) {
builder.append(" update=" + column.getUpdated());
}
builder.append(SEP);
logger.info(builder.toString());
}
}

`

架构师必备:巧用Canal实现异步、解耦的架构的更多相关文章

  1. Java架构师必备技能:docker使用大全

    前言   java工程师成长为架构师是一个艰难且耗费心力的过程,不仅仅需要熟悉java体系内相关的技术,同时要掌握许多运维相关的操作技能,随着k8s逐渐成为微服务持续集成开发难以越过的基础设施之后,d ...

  2. 架构师必备:系统容量现状checklist

    正如飞机在起飞前,机长.副机长要过一遍checklist检查,确认没问题了才能起飞.楼主也整理了一个系统容量现状checklist,方便对照检查.本文搭配架构师必备:如何做容量预估和调优,食用更佳. ...

  3. 高焕堂《android从程序员到架构师之路》 YY讲坛直面大师学习架构设计

    <android从程序员到架构师之路>YY讲坛活动:  sundy携手高焕堂老师全程YY答疑 与大师一起,分享android技术 时间:7月21日下午2:00   报名联系QQ:22243 ...

  4. 架构师成长之路5.4-Saltstack配置管理(LAMP架构案例)

    点击架构师成长之路 架构师成长之路5.4-Saltstack配置管理(LAMP架构案例) 配置管理工具: Pupper:1. 采用ruby编程语言:2. 安装环境相对较复杂:3.不支持远程执行,需要F ...

  5. 架构师必备:MySQL主从同步原理和应用

    日常工作中,MySQL数据库是必不可少的存储,其中读写分离基本是标配,而这背后需要MySQL开启主从同步,形成一主一从.或一主多从的架构,掌握主从同步的原理和知道如何实际应用,是一个架构师的必备技能. ...

  6. .NET架构师必备知识

    .NET架构师,我归纳一下要学的知识: 成为优秀程序员,需要学好的知识: 1. 面向对象编程.UML画图.设计模式.代码重构 2. 常用ORM工具 3.  MVC,WCF,XMl, JQuery ,S ...

  7. 你真的了解微服务架构吗?听听八年阿里架构师怎样讲述Dubbo和Spring Cloud微服务架构

    微服务架构是互联网很热门的话题,是互联网技术发展的必然结果.它提倡将单一应用程序划分成一组小的服务,服务之间互相协调.互相配合,为用户提供最终价值.虽然微服务架构没有公认的技术标准和规范或者草案,但业 ...

  8. 听听八年阿里架构师怎样讲述Dubbo和Spring Cloud微服务架构

    转自:https://baijiahao.baidu.com/s?id=1600174787011483381&wfr=spider&for=pc 微服务架构是互联网很热门的话题,是互 ...

  9. 迅雷首席架构师刘智聪:微信小程序的架构与系统设计的几点观感

    笔者注:本文来自于迅雷首席工程师刘智聪的个人分享,他毕业于南昌大学化学系,加入迅雷后设计开发了多款迅雷核心产品,凭借“大规模网络流媒体服务关键支撑技术”项目获得2015年国家科学技术进步奖二等奖,同时 ...

随机推荐

  1. EPUB文件格式简单解析

    最近热衷于看轻小说,奈何某些网站样式排版属实糟糕,移动端体验极度不友好,实在无法忍受,于是希望能将网站内容爬取下来制作成EPUB格式的电子书. 抛开爬取网站内容不谈,通过解析EPUB文件后,大致掌握了 ...

  2. 从网络通信的演进过程彻底搞懂Redis高性能通信的原理(全网最详细,建议收藏)

    我们一直说Redis的性能很快,那为什么快?Redis为了达到性能最大化,做了哪些方面的优化呢? 在深度解析Redis的数据结构 这篇文章中,其实从数据结构上分析了Redis性能高的一方面原因. 在目 ...

  3. C# 提取PDF中的表格

    本文介绍在C#程序中(附VB.NET代码)提取PDF中的表格的方法,调用Spire.PDF for .NET提供的提取表格的类以及方法等来获取表格单元格中的文本内容:代码内容中涉及到的主要类及方法归纳 ...

  4. 设置elementUI的table组件滚动条位置

    1.设置table的ref为tableList 2.设置滚动至顶部 this.$refs.tableList.bodyWrapper.scrollTop =0; 3.设置滚动至底部 this.$ref ...

  5. 【c++ Prime 学习笔记】第16章 模板与泛型编程

    面向对象编程(OOP)和泛型编程(GP)都能处理在编写程序时类型未知的情况 OOP能处理运行时获取类型的情况 GP能处理编译期可获取类型的情况 标准库的容器.迭代器.算法都是泛型编程 编写泛型程序时独 ...

  6. ffmpeg剪视频

    ffmpeg裁剪合并视频   ffmpeg提供简单的命令参数: ffmpeg -ss START -t DURATION -i INPUT -vcodec copy -acodec copy OUTP ...

  7. 表单编辑时el-form的validate方法执行无效,阻塞代码运行 - Element UI踩坑记录

    今天在用element-ui写管理后台需求时,遇到一个奇怪的问题 一个正常带校验的表单,在新增列表数据时表单校验功能正常: 但是在新增之后再去编辑数据时,表单校验却失效了,甚至阻塞了后续的代码执行,控 ...

  8. 2021.7.27考试总结[NOIP模拟25]

    罕见的改完了题 T1 random 一堆概率,一堆函数,一堆递归,一眼不可做, 但它只有一个参数,所以.. 熠神本着"只有20太难看"的心态,通过样例三个出规律,口胡了一波$\fr ...

  9. STM32串口USART的使用方法和程序

    通用同步异步收发器(USART)提供了一种灵活的方法来与使用工业标准NR 异步串行数据格式的外部设备之间进行全双工数据交换. USART利用分数波特率发生器提供宽范围的波特率选择,支持同步单向通信和半 ...

  10. Python:Ubuntu上使用pip安装opencv-python出现错误

    Ubuntu 18.04 上 使用 pip 安装 opencv-python,出现的错误如下: 1 ~$: pip install opencv-python -i https://pypi.tuna ...